Skip to content

๐Ÿš€ CKEditor 5 for Phoenix - smooth WYSIWYG integration for Elixir apps! โšก Works seamlessly with LiveView or traditional forms. ๐Ÿ’ก Easy setup, supports custom builds, dynamic loading, and localization. ๐Ÿ”ง Plug-and-play modules, JS hooks, and full customization support. ๐ŸŽฏ Ready for both open-source and commercial use!

License

Notifications You must be signed in to change notification settings

Mati365/ckeditor5-phoenix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

CKEditor 5 Phoenix Integration โœจ

License: MIT PRs Welcome GitHub code size in bytes GitHub issues Elixir Coverage TS Coverage NPM Version Hex.pm Version

CKEditor 5 integration library for Phoenix (Elixir) applications. Provides web components and helper functions for seamless editor integration with support for classic, inline, balloon, and decoupled editor types.

Important

This package is unofficial and not maintained by CKSource. For official CKEditor 5 documentation, visit ckeditor.com. If you encounter any issues in the editor, please report them on the GitHub repository.

CKEditor 5 Classic Editor in Phoenix (Elixir) application

Table of Contents

Installation ๐Ÿš€

Choose between two installation methods based on your needs. Both approaches provide the same functionality but differ in how CKEditor 5 assets are loaded and managed.

๐Ÿ  Self-hosted

Bundle CKEditor 5 with your application for full control over assets, custom builds, and offline support. This method is recommended for advanced users or production applications with specific requirements. It's also GPL-compliant.

Complete setup:

  1. Add dependency to your mix.exs:

    def deps do
      [
        {:ckeditor5_phoenix, "~> 1.15.6"}
      ]
    end
  2. Install CKEditor 5

    mix ckeditor5.install # --premium --version 47.3.0
    # ... or: npm install ckeditor5 --prefix assets
  3. Add ckeditor5.install to assets.setup in mix.exs (if using Mix installer):

    "assets.setup": ["ckeditor5.install", ... ]
  4. Register JavaScript hook in your app.js:

    import { Hooks } from 'ckeditor5_phoenix';
    
    const liveSocket = new LiveSocket('/live', Socket, {
      hooks: Hooks,
    });
  5. Import styles in your assets/css/app.css:

    @import "../../deps/ckeditor5/dist/ckeditor5.css";
    /* ... or: @import "../node_modules/ckeditor5/dist/ckeditor5.css"; */
  6. Import module in View

    defmodule MyAppWeb.PageHTML do
      # ... your other uses
      use CKEditor5
    end
  7. Use in templates (no CDN assets needed):

    <.ckeditor id="editor" type="classic" value="<p>Hello world!</p>" />

๐Ÿ“ก CDN Distribution

Load CKEditor 5 directly from CKSource's CDN - no build configuration required. This method is ideal for most users who want quick setup and don't need custom builds.

Complete setup:

  1. Add dependency to your mix.exs:

    def deps do
      [
        {:ckeditor5_phoenix, "~> 1.15.6"}
      ]
    end
  2. Register JavaScript hook in your app.js:

    import { Hooks } from 'ckeditor5_phoenix';
    
    const liveSocket = new LiveSocket('/live', Socket, {
      hooks: Hooks,
    });
  3. Exclude CKEditor from bundler in your config/config.exs:

    config :my_app, MyAppWeb.Endpoint,
      watchers: [
        esbuild: {Esbuild, :install_and_run, [
          :my_app,
          ~w(--external:ckeditor5 --external:ckeditor5-premium-features)
        ]}
      ]
  4. Add license key (see Providing the License Key ๐Ÿ—๏ธ section)

  5. Import module in View

    defmodule MyAppWeb.PageHTML do
      # ... your other uses
      use CKEditor5
    end
  6. Use in templates:

    <%!-- Load CDN assets in <head> (based on `default` preset) --%>
    <.cke_cloud_assets />
    
    <%!-- or with specific features (overrides `default` preset) --%>
    <.cke_cloud_assets translations={["pl", "de", "fr"]} premium />
    
    <%!-- or with specific preset --%>
    <.cke_cloud_assets preset="inline" />
    
    <%!-- Use editor anywhere in <body> --%>
    <.ckeditor id="editor" type="classic" value="<p>Hello world!</p>" />

That's it! ๐ŸŽ‰

Basic Usage ๐Ÿ

Get started with the most common usage patterns. These examples show how to render editors in your templates and handle real-time content changes.

Simple Editor โœ๏ธ

Create a basic editor with default toolbar and features. Perfect for simple content editing without server synchronization.

<%!-- CDN only: Load assets in <head> --%>
<.cke_cloud_assets />

<%!-- Render editor with initial content --%>
<.ckeditor
  id="editor"
  type="classic"
  value="<p>Initial content</p>"
  editable_height="300px"
/>

Watchdog prop ๐Ÿถ

By default, the <.ckeditor> component uses a built-in watchdog mechanism to automatically restart the editor if it crashes (e.g., due to a JavaScript error). The watchdog periodically saves the editor's content and restores it after a crash, minimizing the risk of data loss for users.

Disabling the watchdog ๐Ÿšซ

The watchdog is enabled by default. To disable it, set the watchdog prop to false:

<.ckeditor
  type="classic"
  value="<p>Initial content</p>"
  watchdog={false}
/>

With LiveView Sync ๐Ÿ”„

Enable real-time synchronization between the editor and your LiveView. Content changes are automatically sent to the server with configurable debouncing for performance optimization.

CKEditor 5 Live Sync example

Focus and blur events ๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ

To handle focus and blur events, you can use the focus_event and blur_event attributes in the component. This allows you to capture when the editor gains or loses focus, which can be useful for tracking user interactions or saving content.

<.ckeditor
  id="editor"
  value={@content}
  focus_event
  blur_event
/>
def handle_event("ckeditor5:focus", %{"data" => data}, socket) do
  {:noreply, assign(socket, content: data["main"])}
end

def handle_event("ckeditor5:blur", %{"data" => data}, socket) do
  {:noreply, assign(socket, content: data["main"])}
end

These events are sent immediately when the editor gains or loses focus, allowing you to perform actions like saving content or updating UI elements.

Two-way Communication ๐Ÿ”„

CKEditor 5 Phoenix supports bidirectional communication between your LiveView server and the JavaScript editor instance. This allows you to both receive updates from the editor and programmatically control it from your Elixir code.

From JavaScript to Phoenix (Client โ†’ Server) ๐Ÿ“ค

The editor automatically sends events to your LiveView when content changes, focus changes, or other interactions occur. These events are handled in your LiveView module using standard handle_event/3 callbacks.

<.ckeditor
  id="editor"
  value={@content}
  change_event
/>
defmodule MyAppWeb.EditorLive do
  use MyAppWeb, :live_view
  use CKEditor5

  def mount(_params, _session, socket) do
    {:ok, assign(socket, content: "<p>Initial content</p>", focused?: false)}
  end

  # Receive content updates from editor
  def handle_event("ckeditor5:change", %{"data" => data}, socket) do
    {:noreply, assign(socket, content: data["main"])}
  end
end
From Phoenix to JavaScript (Server โ†’ Client) ๐Ÿ“ฅ

You can programmatically update the editor content from your LiveView by pushing events to the client. This is useful for scenarios like:

<.ckeditor
  id="editor"
  value={@content}
  change_event
/>

<button phx-click="load_template">Load Template</button>
<button phx-click="reset_content">Reset</button>
defmodule MyAppWeb.EditorLive do
  use MyAppWeb, :live_view
  use CKEditor5

  def mount(_params, _session, socket) do
    {:ok, assign(socket, content: "<p>Initial content</p>")}
  end

  # Update editor content from server
  def handle_event("load_template", _params, socket) do
    template_content = """
    <h1>Article Template</h1>
    <p>Start writing your article here...</p>
    <h2>Section 1</h2>
    <p>Content goes here.</p>
    """

    {:noreply,
     socket
     |> push_event("ckeditor5:set-data", %{
       editorId: "editor",
       data: template_content
     })}
  end

  def handle_event("reset_content", _params, socket) do
    {:noreply,
     socket
     |> push_event("ckeditor5:set-data", %{
       editorId: "editor",
       data: "<p>Reset to empty state</p>"
     })}
  end

  # Still handle incoming changes from editor
  def handle_event("ckeditor5:change", %{"data" => data}, socket) do
    {:noreply, assign(socket, content: data["main"])}
  end
end

Editor Types ๐Ÿ–Š๏ธ

CKEditor 5 Phoenix supports four distinct editor types, each designed for specific use cases. Choose the one that best fits your application's layout and functionality requirements.

Classic editor ๐Ÿ“

Traditional WYSIWYG editor with a fixed toolbar above the editing area. Best for standard content editing scenarios like blog posts, articles, or forms.

CKEditor 5 Classic Editor in Elixir Phoenix application with Menubar

<%!-- CDN assets in <head> --%>
<.cke_cloud_assets />

<%!-- Classic editor in <body> --%>
<.ckeditor
  type="classic"
  value="<p>Initial content here</p>"
  editable_height="300px"
/>

Multiroot editor ๐ŸŒณ

Advanced editor supporting multiple independent editable areas within a single editor instance. Perfect for complex layouts like page builders, newsletters, or multi-section content management.

CKEditor 5 Multiroot Editor in Elixir Phoenix application

<%!-- CDN assets in <head> --%>
<.cke_cloud_assets />

<%!-- Editor container --%>
<.ckeditor type="multiroot" />

<%!-- Shared toolbar --%>
<.cke_ui_part name="toolbar" />

<%!-- Multiple editable areas --%>
<div class="flex flex-col gap-4">
  <.cke_editable
    root="header"
    value="<h1>Main Header</h1>"
    class="border border-gray-300"
  />
  <.cke_editable
    root="content"
    value="<p>Main content area</p>"
    class="border border-gray-300"
  />
  <.cke_editable
    root="sidebar"
    value="<p>Sidebar content</p>"
    class="border border-gray-300"
  />
</div>

Inline editor ๐Ÿ“

Minimalist editor that appears directly within content when clicked. Ideal for in-place editing scenarios where the editing interface should be invisible until needed.

CKEditor 5 Inline Editor in Elixir Phoenix application

<%!-- CDN assets in <head> --%>
<.cke_cloud_assets />

<%!-- Inline editor --%>
<.ckeditor
  type="inline"
  value="<p>Click here to edit this content</p>"
  editable_height="300px"
/>

Note: Inline editors don't work with <textarea> elements and may not be suitable for traditional form scenarios.

Decoupled editor ๐ŸŒ

Flexible editor where toolbar and editing area are completely separated. Provides maximum layout control for custom interfaces and complex applications.

CKEditor 5 Decoupled Editor in Elixir Phoenix application

<%!-- CDN assets in <head> --%>
<.cke_cloud_assets />

<%!-- Decoupled editor container --%>
<.ckeditor id="your-editor" type="decoupled">
  <div class="flex flex-col gap-4">
    <%!-- Toolbar can be placed anywhere --%>
    <.cke_ui_part name="toolbar" />

    <%!-- Editable area with custom styling --%>
    <.cke_editable
      value="<p>Initial content here</p>"
      class="border border-gray-300 p-4 rounded"
      editable_height="300px"
    />
  </div>
</.ckeditor>

Forms Integration ๐Ÿงพ

Seamlessly integrate CKEditor 5 with Phoenix forms and LiveView for robust content management. Learn how to handle form submissions and real-time updates.

Phoenix Form Helper ๐Ÿง‘โ€๐Ÿ’ป

The editor automatically creates hidden input fields for form integration. Content is synchronized with form fields using the field attribute, making it compatible with standard Phoenix form helpers.

How it works:

  • Hidden input field is created automatically
  • Field name is derived from the field attribute
  • Content is synchronized on form submission
<.form for={@form} phx-submit="save">
  <.ckeditor id="content-editor" field={@form[:content]} />

  <button type="submit">Save</button>
</.form>

LiveView Handler โšก

Complete LiveView integration with event handling for both real-time updates and form processing.

defmodule MyApp.PageLive do
  use MyAppWeb, :live_view
  use CKEditor5  # Adds event handlers

  def mount(_params, _session, socket) do
    form = to_form(%{"content" => ""}, as: :form)
    {:ok, assign(socket, form: form)}
  end

  # Handle real-time content changes
  def handle_event("ckeditor5:change", %{"data" => data}, socket) do
    # Update content in real-time
    updated_params = Map.put(socket.assigns.form.params, "content", data["main"])
    {:noreply, assign(socket, form: to_form(updated_params, as: :form))}
  end

  # Handle form validation
  def handle_event("validate", %{"form" => params}, socket) do
    {:noreply, assign(socket, form: to_form(params, as: :form))}
  end

  # Handle form submission
  def handle_event("save", %{"form" => params}, socket) do
    # Process and save form data
    case save_content(params) do
      {:ok, _} ->
        {:noreply, put_flash(socket, :info, "Content saved successfully!")}
      {:error, _} ->
        {:noreply, put_flash(socket, :error, "Failed to save content")}
    end
  end
end

Configuration โš™๏ธ

You can configure the editor presets in your config/config.exs file. The default preset is :default, which provides a basic configuration with a toolbar and essential plugins. The preset is a map that contains the editor configuration, including the toolbar items and plugins. There can be multiple presets, and you can switch between them by passing the preset keyword argument to the ckeditor component.

Custom Presets ๐Ÿงฉ

In order to override the default preset or add custom presets, you can add the following configuration to your config/config.exs file:

# config/config.exs
config :ckeditor5_phoenix,
  presets: %{
    minimal: %{
      cloud: %{
        version: "46.0.0",
        premium: true,
        translations: ["pl"],
        ckbox: %{
          version: "1.0.0"
        }
      },
      config: %{
        toolbar: [:bold, :italic, :link],
        plugins: [:Bold, :Italic, :Link, :Essentials, :Paragraph]
      }
    },
    full: %{
      config: %{
        toolbar: [
          :heading, :|, :bold, :italic, :underline, :|,
          :link, :insertImage, :insertTable, :|,
          :bulletedList, :numberedList, :blockQuote
        ],
        plugins: [
          :Heading, :Bold, :Italic, :Underline, :Link,
          :ImageBlock, :ImageUpload, :Table, :List, :BlockQuote,
          :Essentials, :Paragraph
        ]
      }
    }
  }

In template:

<.ckeditor preset="minimal" value="<p>Simple editor</p>" />

Dynamic presets ๐ŸŽฏ

You can also create dynamic presets that can be modified at runtime. This is useful if you want to change the editor configuration based on user input or other conditions.

defmodule MyApp.PageLive do
  use MyAppWeb, :live_view
  use CKEditor5

  alias CKEditor5.Preset

  def mount(_params, _session, socket) do
    preset = Preset.Parser.parse!(%{
      config: %{
        toolbar: [:bold, :italic, :link],
        plugins: [:Bold, :Italic, :Link, :Essentials, :Paragraph]
      }
    })

    {:ok, assign(socket, preset: preset)}
  end
end

In template:

<.ckeditor preset={@preset} />

Providing the License Key ๐Ÿ—๏ธ

CKEditor 5 requires a license key when using the official CDN or premium features. You can provide the license key in two simple ways:

  1. Environment variable: Set the CKEDITOR5_LICENSE_KEY environment variable before starting your Phoenix app. This is the easiest and most common way.

  2. Preset config: You can also set the license key directly in your preset configuration in config/config.exs:

    config :ckeditor5_phoenix,
      presets: %{
        default: %{
          license_key: "your-license-key-here"
        }
      }

If you use CKEditor 5 under the GPL license, you do not need to provide a license key. However, if you choose to set one, it must be set to GPL.

If both are set, the preset config takes priority. For more details, see the CKEditor 5 licensing guide.

Referencing DOM Elements in Config ๐Ÿท๏ธ

You can reference DOM elements directly in your editor configuration using the special { $element: "selector" } format. This is useful when you want to attach the editor's UI parts (like toolbars or editable areas) to specific elements in your HTML.

How to use ๐Ÿ› ๏ธ

  • In your config object, use { $element: "CSS_SELECTOR" } wherever a DOM element is expected.
  • The selector will be resolved to the actual DOM element before initializing the editor.

Example ๐Ÿ“„

# config/config.exs
config :ckeditor5_phoenix,
  presets: %{
    # ... other presets
    minimal: %{
      config: %{
        # ... other config
        yourPlugin: %{
          toolbar: %{ $element: "#my-toolbar" },
          editable: %{ $element: "#my-editable" }
        },
      }
    }
  }

This will find the elements with IDs my-toolbar and my-editable in the DOM and use them for the editor's UI.

โš ๏ธ If the element is not found, a warning will be shown in the console.

Localization ๐ŸŒ

Support multiple languages in the editor UI and content. Learn how to load translations via CDN or configure them globally.

CDN Translation Loading ๐ŸŒ

Depending on your setup, you can preload translations via CDN or let your bundler handle them automatically using lazy imports.

<%!-- CDN only: Load specific translations --%>
<.cke_cloud_assets translations={["pl", "de", "fr"]} />

<.ckeditor
  language="pl"
  content_language="en"
  value="<p>Content in English, UI in Polish</p>"
/>

Global Translation Config ๐Ÿ› ๏ธ

You can also configure translations globally in your config/config.exs file. This is useful if you want to load translations for multiple languages at once or set a default language for the editor. Keep in mind that this configuration is only used when loading translations via CDN. If you are using self-hosted setup, translations are handled by your bundler automatically.

# config/config.exs
config :ckeditor5_phoenix,
  presets: %{
    default: %{
      cloud: %{
        translations: ["pl", "de", "fr"]  # CDN only
      }
    }
  }

Note: For self-hosted setups, translations are handled by your bundler automatically.

Custom translations ๐ŸŒ

You can also provide custom translations for the editor. This is useful if you want to override existing translations or add new ones. Custom translations can be provided in the preset configuration.

# config/config.exs
config :ckeditor5_phoenix,
  presets: %{
    default: %{
      custom_translations: %{
        en: %{
          Bold: "Custom Bold",
          Italic: "Custom Italic"
        },
        pl: %{
          Bold: "Pogrubiony",
          Italic: "Kursywa"
        }
      }
    }
  }

Custom plugins ๐Ÿงฉ

To register a custom plugin, use the registerCustomEditorPlugin function. This function takes the plugin name and the plugin reader that returns a class extending Plugin.

import { CustomEditorPluginsRegistry as Registry } from 'ckeditor5_phoenix';

const unregister = Registry.the.register('MyCustomPlugin', async () => {
  // It's recommended to use lazy import to
  // avoid bundling ckeditor code in your application bundle.
  const { Plugin } = await import('ckeditor5');

  return class extends Plugin {
    static get pluginName() {
      return 'MyCustomPlugin';
    }

    init() {
      console.log('MyCustomPlugin initialized');
      // Custom plugin logic here
    }
  };
});

In order to use the plugin you need to extend your config in config/config.exs:

config :ckeditor5_phoenix,
  presets: %{
    default: %{
      config: %{
        plugins: [:MyCustomPlugin, :Essentials, :Paragraph],
        # ... other config options
      }
    }
  }

It must be called before the editor is initialized. You can unregister the plugin later by calling the returned function:

unregister();
// or CustomEditorPluginsRegistry.the.unregister('MyCustomPlugin');

If you want to de-register all registered plugins, you can use the unregisterAll method:

import { CustomEditorPluginsRegistry } from 'ckeditor5_phoenix';

CustomEditorPluginsRegistry.the.unregisterAll();

Context ๐Ÿค

The context feature is designed to group multiple editor instances together, allowing them to share a common context. This is particularly useful in collaborative editing scenarios, where users can work together in real time. By sharing a context, editors can synchronize features such as comments, track changes, and presence indicators across different editor instances. This enables seamless collaboration and advanced workflows in your Phoenix application.

For more information about the context feature, see the CKEditor 5 Context documentation.

CKEditor 5 Context in Elixir Phoenix application

Basic usage ๐Ÿ”ง

Define your context in configuration:

config :ckeditor5_phoenix,
  contexts: %{
    "your-context" => %{
      config: %{
        plugins: [
          :CustomContextPlugin
        ]
      },
      watchdog: %{
        crash_number_limit: 20
      }
    }
  },
  presets: %{
    # ...
  }

And use it in your LiveView:

<.cke_context context="your-context">
  <.ckeditor class="mb-6" value="Child A" />
  <.ckeditor value="Child B" />
</.cke_context>

Voila!

Note

The context attribute accepts also CKEditor5.Context structure, so it can be used in LiveView assigns or other dynamic contexts.

Custom context translations ๐ŸŒ

Define your custom translations in the configuration:

config :ckeditor5_phoenix,
  contexts: %{
    "custom" => %{
      # ...
      custom_translations: %{
        en: %{
          Bold: "Custom Bold",
          Italic: "Custom Italic"
        },
        pl: %{
          Bold: "Pogrubiony",
          Italic: "Kursywa"
        }
      }
    }
  }

These translations will be used in the context's editors, overriding the default translations. They are available through locale.t plugin in every context plugin.

Watch registered editors ๐Ÿ‘€

You can watch the registered editors using the watch function. This is useful if you want to react to changes in the registered editors, for example, to update the UI or perform some actions when an editor is added or removed.

import { EditorsRegistry } from 'ckeditor5_phoenix';

const unregisterWatcher = EditorsRegistry.the.watch((editors) => {
  console.log('Registered editors changed:', editors);
});

// Later, you can unregister the watcher
unregisterWatcher();

Wait for particular editor to be registered โณ

You can also wait for a specific editor to be registered using the waitForEditor function. This is useful if you want to perform some actions after a specific editor is registered.

This method can be called before the editor is initialized, and it will resolve when the editor is registered.

import { EditorsRegistry } from 'ckeditor5_phoenix';

EditorsRegistry.the.waitFor('editor1').then((editor) => {
  console.log('Editor "editor1" is registered:', editor);
});

// ... init editor somewhere later

The id of the editor must be used to identify the editor. If the editor is already registered, the promise will resolve immediately.

Package development ๐Ÿ› ๏ธ

In order to contribute to CKEditor 5 Phoenix or run it locally for manual testing, here are some handy commands to get you started.

To run the minimal Phoenix application with CKEditor 5 integration, install dependencies and start the server:

mix playground

In order to run the playground in cloud mode, set the CKEDITOR5_PLAYGROUND_MODE environment variable to cloud:

CKEDITOR5_PLAYGROUND_MODE=cloud mix playground

Run tests using the mix test command. All tests are located in the test/ directory.

mix test

To generate a code coverage report, use:

mix coveralls.html

Psst... ๐Ÿ‘€

Discover related projects for other frameworks and languages. Find inspiration or alternative integrations for CKEditor 5.

Looking for similar projects or inspiration? Check out these repositories:

  • ckeditor5-rails Effortless CKEditor 5 integration for Ruby on Rails. Works seamlessly with standard forms, Turbo, and Hotwire. Easy setup, custom builds, and localization support.

  • ckeditor5-livewire Plug-and-play CKEditor 5 solution for Laravel + Livewire applications. Fully compatible with Blade forms. Includes JavaScript hooks, flexible configuration, and easy customization.

  • ckeditor5-symfony Native CKEditor 5 integration for Symfony. Works with Symfony 6.x+, standard forms and Twig. Supports custom builds, multiple editor configurations, asset management, and localization. Designed to be simple, predictable, and framework-native.

Trademarks ๐Ÿ“œ

Information about CKEditorยฎ trademarks and licensing. Clarifies the relationship between this package and CKSource.

CKEditorยฎ is a trademark of CKSource Holding sp. z o.o. All rights reserved. For more information about the license of CKEditorยฎ please visit CKEditor's licensing page.

This package is not owned by CKSource and does not use the CKEditorยฎ trademark for commercial purposes. It should not be associated with or considered an official CKSource product.

License ๐Ÿ“œ

Details about the MIT license for this project and CKEditor 5's GPL license. Make sure to review both licenses for compliance.

This project is licensed under the terms of the MIT LICENSE.

This project injects CKEditor 5 which is licensed under the terms of GNU General Public License Version 2 or later. For more information about CKEditor 5 licensing, please see their official documentation.

About

๐Ÿš€ CKEditor 5 for Phoenix - smooth WYSIWYG integration for Elixir apps! โšก Works seamlessly with LiveView or traditional forms. ๐Ÿ’ก Easy setup, supports custom builds, dynamic loading, and localization. ๐Ÿ”ง Plug-and-play modules, JS hooks, and full customization support. ๐ŸŽฏ Ready for both open-source and commercial use!

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •