@layers-app/editor-core
v0.3.0
Published
Plugin SDK for @layers-app/editor — shared primitives used to build first-party and third-party editor plugins
Downloads
448
Readme
@layers-app/editor-core
Public Plugin SDK for @layers-app/editor. Use it to build first-party or third-party editor plugins.
For most consumers this package is a transitive dependency of
@layers-app/editor— you don't install it directly. Install it explicitly only when you're authoring a plugin.
Install
npm install @layers-app/editor-corePeer dependencies (install as needed): react, react-dom, lexical, @lexical/react, @mantine/core, @mantine/hooks, @mantine/dropzone, @mantine/carousel, @hugeicons/react, @hugeicons/core-free-icons, @tabler/icons-react, @tanstack/react-query, i18next, react-i18next, lodash-es.
What's exported
Plugin infrastructure types
PluginConfig— full plugin contract consumed by<Editor plugins={[...]} />SlashMenuConfig— slash-menu entry for a pluginPluginModeConfig— which editor modes a plugin is enabled in (comment / default / editor)UploadData,AlignmentType,MediaPlatform
Hooks
useChunkedUpload({ endpoints, ... })— chunked file upload with resume/retryuseCancelUpload()— AbortController helper shared by File, Images, Video uploaders
UI primitives
ActionIcon,MobileDrawer,Select,SelectListOptionButtonAlignmentMenu— simple left/center/right pickerNodeAlignmentMenu— wired-up menu for any Lexical node exposingsetAlignmentResizableContainer— media block with resize handles, touch support, alignment-aware cursors- Upload UI:
ActionButtons,ProgressBar,UploadLoaderIndicator,UploadProgressPanel
Media utilities
parseVideoURL(url, isLink?)— recognizes YouTube, VK, Rutube, uploaded UUIDsinsertMediaNode(editor, node)— basic Lexical-safe node insertiondeleteNode(editor, nodeOrKey)formatTime(seconds)—M:SS/H:MM:SSformatting
Environment & utilities
IS_APPLE,IS_IOS,IS_ANDROID,IS_SAFARI,IS_FIREFOX,IS_CHROME,IS_TOUCH_DEVICE,IS_HOVER_CAPABLE,IS_TOUCH_TABLETisMobile,CAN_USE_DOMgetAllowedControls(alignment)— resize handles allowed for a given alignment
Building a plugin — step by step
A plugin is a PluginConfig object passed to <Editor plugins={[yourPlugin(config)]} />. Below is the minimum needed to ship one.
1. Define a Lexical node
Your plugin owns a custom Lexical node. This is what gets serialized into the editor state.
// node.ts
import { DecoratorNode, type LexicalCommand, type NodeKey, createCommand } from 'lexical';
export type CalendarPayload = { date: string };
export class CalendarNode extends DecoratorNode<JSX.Element> {
__date: string;
static getType(): string { return 'calendar'; }
static clone(node: CalendarNode): CalendarNode { return new CalendarNode(node.__date, node.__key); }
constructor(date: string, key?: NodeKey) {
super(key);
this.__date = date;
}
createDOM(): HTMLElement { return document.createElement('div'); }
updateDOM(): false { return false; }
decorate(): JSX.Element {
return <CalendarBlock date={this.__date} />;
}
exportJSON() { return { type: 'calendar', date: this.__date, version: 1 }; }
static importJSON(json: ReturnType<CalendarNode['exportJSON']>): CalendarNode {
return new CalendarNode(json.date);
}
}
export const INSERT_CALENDAR_COMMAND: LexicalCommand<CalendarPayload> =
createCommand('INSERT_CALENDAR_COMMAND');2. Write the plugin component
The component listens to commands and inserts your node.
// CalendarPlugin.tsx
import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { COMMAND_PRIORITY_LOW } from 'lexical';
import { insertMediaNode } from '@layers-app/editor-core';
import { CalendarNode, INSERT_CALENDAR_COMMAND } from './node';
export default function CalendarPlugin(): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([CalendarNode])) {
throw new Error('CalendarPlugin: CalendarNode not registered');
}
return editor.registerCommand(
INSERT_CALENDAR_COMMAND,
(payload) => {
const node = new CalendarNode(payload.date);
insertMediaNode(editor, node);
return true;
},
COMMAND_PRIORITY_LOW,
);
}, [editor]);
return null;
}3. Wire up the factory
The factory returns the PluginConfig consumed by <Editor>.
// factory.tsx
import { Calendar01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import type { PluginConfig } from '@layers-app/editor-core';
import CalendarPlugin from './CalendarPlugin';
import { CalendarNode, INSERT_CALENDAR_COMMAND } from './node';
export type CalendarPluginConfig = {
/* config fields available to your component via context */
defaultDate?: string;
};
export function calendarPlugin(config: CalendarPluginConfig): PluginConfig {
return {
key: 'calendar',
node: CalendarNode,
Plugin: CalendarPlugin,
Provider: ({ children }) => <>{children}</>, // or your own context provider
modes: { comment: false, default: true, editor: true },
slashMenu: {
parentKey: 'media',
name: 'editor.calendar.name',
description: 'editor.calendar.description',
icon: <HugeiconsIcon icon={Calendar01Icon} size={16} />,
keywords: ['calendar', 'date', 'event'],
command: INSERT_CALENDAR_COMMAND,
commandPayload: new Date().toISOString().slice(0, 10),
},
};
}4. Use it in a host application
import { Editor } from '@layers-app/editor';
import { calendarPlugin } from '@your-org/editor-calendar';
const config = useMemo(() => calendarPlugin({ defaultDate: '2026-01-01' }), []);
<Editor plugins={[config]} ... />Reference implementation
@layers-app/editor-video is the canonical reference — a fully-featured plugin (chunked upload, custom player UI, settings modal, subtitles, chapters) built using only @layers-app/editor-core. Its source is the best example of how to use everything in this SDK.
Conventions
- Node
getType()must be unique across all plugins in the editor. - Command names (e.g.
INSERT_CALENDAR_COMMAND) should be plugin-prefixed. - i18n keys live in the host application; the plugin only references key names.
- Icons: import from
@hugeicons/core-free-icons+@hugeicons/react. - Styles: scoped to the plugin's components; if using SCSS, bundle via the package itself.
Architectural constraint
editor-core must never import from @layers-app/editor. Plugins that follow this constraint can be developed and shipped independently of the editor source.
Cross-plugin imports are also discouraged — share via editor-core instead.
License
MIT
