@kodezen/editor
v1.3.4
Published
Embeddable drag-and-drop block email editor. Framework-agnostic, zero server/CMS dependency. Drop-in replacement for a Quill/TinyMCE editor: produces email HTML client-side.
Downloads
476
Readme
@kodezen/editor
Embeddable drag-and-drop block editor. Framework-agnostic, zero server/CMS dependency. A drop-in replacement for a Quill/TinyMCE editor: it produces HTML client-side (no DB table, backend endpoint, or any other package required beyond React) and ships zero global CSS (every pane is inline-styled).
One component (EmailEditor) covers four modes — email builder, plain
rich-text, form builder, and certificate/print layout — plus two convenience
wrappers (RichTextEditor, FormEditor) for call sites that only ever want
one mode.
Install
npm install @kodezen/editorThe package is pre-built (transpiled ESM) so it works regardless of the consumer's bundler/transform setup.
Peer dependencies (host-supplied)
react, react-dom. These are left external so the consumer's single React
instance is reused. @dnd-kit/*, react-icons, and just-debounce-it are
bundled in.
Media picker
The default mediaUpload handler opens a native <input type="file"> picker
and resolves the chosen file as a data: URL — it works in any browser with
no configuration, but it's a testing baseline, not a production upload
pipeline (data: URLs bloat stored HTML and are unreliable in several email
clients). A host with real storage (S3, its own media library, etc.) should
pass its own mediaUpload prop instead — see below.
Usage
import { EmailEditor, renderContent, renderDocument, BLANK_TREE } from '@kodezen/editor';
const ref = useRef();
<EmailEditor
ref={ref}
initialTree={savedTree} // { version, root } — read once at mount; remount via React `key`
mergeTags={[{ tag: '{{first_name}}', label: 'First name' }]}
mediaUpload={async ({ allowedTypes }) => ({ url, id, alt })} // optional; defaults to a native file picker
onChange={({ tree, html }) => persist(tree, html)} // debounced
renderMode="document" // 'document' (full email) | 'fragment' (host wraps in its own shell)
header // show a slim Preview bar
/>
// On submit, pull the latest value past the debounce:
const html = ref.current.getHtml();
const tree = ref.current.getTree();Modes
EmailEditor takes a mode prop; RichTextEditor and FormEditor are thin
wrappers that hard-code it (<EmailEditor mode="richtext" /> /
<EmailEditor mode="form" /> respectively):
| mode | What |
| --- | --- |
| builder (default) | Full drag-and-drop block email builder. |
| richtext | A simple Quill-like rich-text field (persistent toolbar, single editable surface). Takes value (HTML string) instead of initialTree; onChange still fires { tree, html } (tree is a single Custom HTML block wrapping the field's HTML). |
| form | Drag-and-drop form builder (semantic, responsive <form> output). onChange fires { tree, html, schema }. |
| certificate | The block builder laid out on a fixed landscape print page with a background, for certificates/awards. |
Props
| Prop | Modes | What |
| --- | --- | --- |
| mode | all | 'builder' (default) | 'richtext' | 'form' | 'certificate'. |
| initialTree | builder, form, certificate | { version, root } — saved block tree, or omit for a blank starter. Read once at mount (like defaultValue); remount via a React key to load a different record. |
| value | richtext | HTML string — the saved content. Re-applied if it changes while the field is unfocused (async form loads). |
| mergeTags | all | [{ tag: '{{first_name}}', label: 'First name' }, ...] — host personalization tags, inserted verbatim and resolved at the host's send time. |
| mediaUpload | all | ({ allowedTypes }) => Promise<{ url, id, alt } | null>. Optional; defaults to defaultMediaUpload (native file picker, data: URL). |
| onChange | all | Debounced. { tree, html } (form mode also includes schema). |
| header | builder, richtext, form | false (default) | true — shows a slim Preview bar. Always on for certificate. |
| height | all | Editor height in px. Omit to use the stylesheet defaults (640 builder/form/certificate, 320 richtext), which hosts can override by class. |
| renderMode | builder | 'document' (full <!doctype html>… email) | 'fragment' (content only — host wraps its own <html>/<body> shell). |
| blocks | builder, form, certificate | Optional manifest override, replacing the mode's default block set entirely. |
| blockPlugins | builder, form | [{ type, spec, render, controls, schema }, ...] — teach the editor host-specific blocks without forking the core. render(node, ctx, helpers) returns an HTML string; helpers exposes the built-in renderers and style utilities so a plugin can delegate to them. See dev_emb/library/blockPlugins.js. |
| hideBlocks | builder, form, certificate | string[] of core block types to drop from the palette/quick-add. Blocks already present in a saved tree still render. |
| templates | builder, certificate | Array of starter templates shown in the template picker. |
| onTemplatesClick | builder, certificate | Callback when the host's own "browse templates" affordance is used. |
| suffix | all | Theming hook, e.g. suffix="zaplane". Every shell chrome piece gets a stable --{suffix} modifier class, and hosts can override the --emb-* custom properties on .emb-embed-root--{suffix} to theme the chrome (e.g. dark mode) without !important. |
| config | all | { 'English string': 'Host-translated string', ... } — plain lookup object. The package does no translation logic of its own; the host resolves everything and hands over final strings. Anything missing falls back to the English text as-is. Omit for plain English. See i18n. |
Ref handle (all modes): { getTree, getHtml, getSchema, setTree, setHtml }.
Not every method is meaningful in every mode (e.g. setHtml is for
richtext) — calling one that doesn't apply is a no-op. Read
getHtml()/getTree() on the host's submit to capture the latest value even
if the debounce hasn't fired.
Exports
| Export | What |
| --- | --- |
| EmailEditor | The controlled editor component (forwardRef, all modes — see above). |
| RichTextEditor | Convenience wrapper = <EmailEditor mode="richtext" />. |
| FormEditor | Convenience wrapper = <EmailEditor mode="form" />. |
| renderDocument(tree) | Tree → full email HTML document (doctype + MSO shell). |
| renderContent(tree) | Tree → email content fragment (no doctype/body) for hosts that wrap in their own email shell. |
| renderFormDocument(tree) / renderFormContent(tree) | Same, for a form-mode tree. |
| renderCertificateDocument(tree) / renderCertificateContent(tree) | Same, for a certificate-mode tree. |
| extractSchema(tree) | Form tree → field schema (also emitted as schema in form-mode onChange). |
| pageDims | Fixed page dimensions used by the certificate layout. |
| treeFromHtml(html) / htmlToBlocks(html) | Best-effort HTML → block tree, for importing existing content. |
| BLANK_TREE | Starter tree used when initialTree is absent (builder/richtext). |
| BLANK_CERT_TREE | Starter tree for certificate mode. |
| defaultMediaUpload | The native file-picker default media handler (see Media picker above). |
i18n
Every UI string is untranslated (plain English) by default — there's no
bundled translation catalog and no runtime translate hook. To localize, pass
a plain lookup object as the config prop:
<EmailEditor config={{ 'Add block': 'Ajouter un bloc' }} />The package does no lookup/interpolation logic beyond config[text] ?? text
— the host is responsible for resolving strings up front.
Storage contract
The host owns persistence. onChange gives { tree, html } (plus schema
in form mode):
html→ the field your send pipeline already uses.tree(JSON) → a sibling field, so the editor round-trips on reopen.
Building from source
npm install
npm run build # → dist/index.js