@lyfie/luthor-headless 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,255 +1,671 @@
1
- # Luthor-Headless
1
+ # @lyfie/luthor-headless
2
2
 
3
- **Type-safe rich text editor for React developers**
3
+ Type-safe, headless rich text editor system for React, built on Lexical.
4
4
 
5
- Built on Meta's Lexical. Headless, extensible, and production-ready.
5
+ ## Package responsibility
6
6
 
7
- [![npm version](https://badge.fury.io/js/%40luthor%2Feditor.svg)](https://badge.fury.io/js/%40luthor%2Feditor)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ - Owns Lexical-derived behavior and extension runtime semantics.
8
+ - Designed to stay lightweight with optional integrations that degrade gracefully.
9
+ - Preset UX belongs in `@lyfie/luthor`.
9
10
 
10
- **[🚀 Demo](https://luthor.lyfie.app/demo)** • **[📖 Documentation](https://luthor.lyfie.app/docs)** • **[⚡ Playground](https://stackblitz.com/edit/vitejs-vite-bpg2kpze?file=src%2FEditor.tsx)**
11
-
12
- ![Luthor Editor](https://github.com/user-attachments/assets/ec547406-0ab0-4e69-b9d7-ccd050adf78a)
13
-
14
- ---
15
-
16
- ## Why Luthor?
17
-
18
- Rich text editors shouldn't be a nightmare. Luthor makes building them delightful:
19
-
20
- - **🔒 Type-safe everything** - Commands and states inferred from your extensions. No runtime surprises.
21
- - **🎨 Headless & flexible** - Build any UI you want. Style it your way.
22
- - **🧩 Modular extensions** - Add only what you need, when you need it.
23
- - **⚡ Production features** - HTML/Markdown export, image handling, tables, undo/redo.
24
- - **⚛️ React-first** - Hooks, components, and patterns you already know.
25
-
26
- ```tsx
27
- // Your extensions define your API - TypeScript knows everything ✨
28
- const extensions = [boldExtension, listExtension, imageExtension] as const;
29
- const { Provider, useEditor } = createEditorSystem<typeof extensions>();
30
-
31
- function MyEditor() {
32
- const { commands, activeStates } = useEditor();
33
-
34
- // TypeScript autocompletes and validates these
35
- commands.toggleBold(); // ✅ Available
36
- commands.toggleUnorderedList(); // ✅ Available
37
- commands.insertImage(); // ✅ Available
38
- commands.nonExistent(); // ❌ TypeScript error
39
- }
40
- ```
41
-
42
- ## Quick Start
43
-
44
- ### Installation
45
-
46
- Luthor-Headless is designed to be lightweight with Lexical packages as **peer dependencies**.
11
+ ## Installation
47
12
 
48
13
  ```bash
49
- # npm
50
- npm install @lyfie/luthor-headless
51
-
52
- # pnpm
53
- pnpm add @lyfie/luthor-headless
14
+ pnpm add @lyfie/luthor-headless lexical @lexical/code @lexical/html @lexical/link @lexical/list @lexical/markdown @lexical/react @lexical/rich-text @lexical/selection @lexical/table @lexical/utils react react-dom
54
15
  ```
55
16
 
56
- Install the required Lexical peer dependencies:
17
+ Optional syntax highlighting provider:
57
18
 
58
19
  ```bash
59
- # npm
60
- npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
61
-
62
- # pnpm
63
- pnpm add lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
20
+ pnpm add highlight.js
64
21
  ```
65
22
 
66
- > **💡 Want a simpler setup?** Check out [@lyfie/luthor](../luthor/README.md) which bundles all Lexical dependencies for you.
67
-
68
- ### Basic Usage
23
+ Optional emoji catalog provider:
69
24
 
70
25
  ```bash
71
- # npm
72
- npm install @lyfie/luthor-headless
73
-
74
- # pnpm
75
- pnpm add @lyfie/luthor-headless
26
+ pnpm add @emoji-mart/data
76
27
  ```
77
28
 
78
- Install the Lexical peer dependencies:
29
+ Optional package behavior:
79
30
 
80
- ```bash
81
- # npm
82
- npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
31
+ - Without `highlight.js`, code blocks use built-in fallback token styling.
32
+ - Without `@emoji-mart/data`, emoji features use the built-in lightweight catalog.
33
+ - Both optional packages are fail-safe and do not block editor startup.
83
34
 
84
- # pnpm
85
- pnpm add lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
86
- ```
35
+ ## Quick Start
87
36
 
88
37
  ```tsx
89
38
  import {
90
39
  createEditorSystem,
40
+ RichText,
41
+ richTextExtension,
91
42
  boldExtension,
92
43
  italicExtension,
93
- listExtension,
94
- RichText,
95
44
  } from "@lyfie/luthor-headless";
96
45
 
97
- const extensions = [boldExtension, italicExtension, listExtension] as const;
46
+ const extensions = [richTextExtension, boldExtension, italicExtension] as const;
98
47
  const { Provider, useEditor } = createEditorSystem<typeof extensions>();
99
48
 
100
49
  function Toolbar() {
101
50
  const { commands, activeStates } = useEditor();
51
+
102
52
  return (
103
- <div className="toolbar">
104
- <button
105
- onClick={() => commands.toggleBold()}
106
- className={activeStates.bold ? "active" : ""}
107
- >
53
+ <div>
54
+ <button onClick={() => commands.toggleBold()} aria-pressed={activeStates.bold}>
108
55
  Bold
109
56
  </button>
110
- <button
111
- onClick={() => commands.toggleItalic()}
112
- className={activeStates.italic ? "active" : ""}
113
- >
57
+ <button onClick={() => commands.toggleItalic()} aria-pressed={activeStates.italic}>
114
58
  Italic
115
59
  </button>
116
- <button onClick={() => commands.toggleUnorderedList()}>
117
- Bullet List
118
- </button>
119
- </div>
120
- );
121
- }
122
-
123
- function Editor() {
124
- return (
125
- <div className="editor-container">
126
- <Toolbar />
127
- <RichText placeholder="Start writing..." />
128
60
  </div>
129
61
  );
130
62
  }
131
63
 
132
- export default function App() {
64
+ export function Editor() {
133
65
  return (
134
66
  <Provider extensions={extensions}>
135
- <Editor />
67
+ <Toolbar />
68
+ <RichText placeholder="Write here..." />
136
69
  </Provider>
137
70
  );
138
71
  }
139
72
  ```
140
73
 
141
- **That's it.** You now have a fully functional, type-safe rich text editor.
74
+ ## Core API
142
75
 
143
- ## Installation Options
76
+ ### `createEditorSystem<Exts>()`
144
77
 
145
- ### Option 1: Headless Package (This Package)
78
+ Returns:
146
79
 
147
- For maximum control and flexibility:
80
+ - `Provider(props)`
81
+ - `useEditor()`
148
82
 
149
- ```bash
150
- # npm
151
- npm install @lyfie/luthor-headless
152
- npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
83
+ `Provider` props:
84
+
85
+ - `extensions: Exts` (required)
86
+ - `children: ReactNode` (required)
87
+ - `config?: EditorConfig`
88
+
89
+ `EditorConfig`:
90
+
91
+ - `theme?: EditorThemeClasses`
92
+ - any additional keys are allowed and can be consumed by extensions/components
93
+
94
+ ### `useEditor()` context
95
+
96
+ Returns strongly typed surface based on `extensions`:
97
+
98
+ - `commands`
99
+ - `activeStates`
100
+ - `stateQueries`
101
+ - `hasExtension(name)`
102
+ - `export.toJSON()`
103
+ - `import.fromJSON(json)`
104
+ - `lexical` / `editor`
105
+ - `plugins`
106
+ - listener helpers (`registerUpdate`, `registerPaste`)
107
+
108
+ ## RichText Component API
109
+
110
+ `RichText` and `RichTextExtension` use the same prop shape:
111
+
112
+ - `contentEditable?: ReactElement`
113
+ - `placeholder?: ReactElement | string`
114
+ - `className?: string`
115
+ - `classNames?: { container?: string; contentEditable?: string; placeholder?: string }`
116
+ - `styles?: { container?: CSSProperties; contentEditable?: CSSProperties; placeholder?: CSSProperties }`
117
+ - `errorBoundary?: ComponentType<{ children: JSX.Element; onError: (error: Error) => void }>`
118
+
119
+ ## Theme Utilities
120
+
121
+ - `defaultLuthorTheme`
122
+ - `mergeThemes(baseTheme, overrideTheme)`
123
+ - `isLuthorTheme(value)`
124
+ - `LUTHOR_EDITOR_THEME_TOKENS`
125
+ - `createEditorThemeStyleVars(overrides)`
126
+
127
+ `LuthorEditorThemeOverrides` is a token map with keys from `LUTHOR_EDITOR_THEME_TOKENS` and string values.
128
+
129
+ ## Markdown Bridge API
130
+
131
+ Headless now exposes lightweight markdown bridge helpers:
132
+
133
+ - `markdownToJSONB(markdown: string): JsonbDocument`
134
+ - `jsonbToMarkdown(input: unknown): string`
135
+
136
+ These are intended as conversion primitives for preset mode-switch UIs (for example `MDTextEditor` in `@lyfie/luthor`).
137
+
138
+ ## Base Extension Config
139
+
140
+ All extension configs support `BaseExtensionConfig`:
141
+
142
+ - `showInToolbar?: boolean`
143
+ - `category?: ExtensionCategory[]`
144
+ - `position?: "before" | "after"`
145
+ - `initPriority?: number`
146
+
147
+ ## Built-in Extensions
148
+
149
+ ### Text Formatting
150
+
151
+ - `boldExtension`
152
+ - `italicExtension`
153
+ - `underlineExtension`
154
+ - `strikethroughExtension`
155
+ - `subscriptExtension`
156
+ - `superscriptExtension`
157
+ - `codeFormatExtension` (inline code mark)
158
+
159
+ These are toggle-style text-format extensions and do not require custom config.
160
+
161
+ ### Link Extension (`LinkExtension`, `linkExtension`)
162
+
163
+ `LinkConfig`:
164
+
165
+ - `autoLinkText?: boolean`
166
+ - `autoLinkUrls?: boolean`
167
+ - `linkSelectedTextOnPaste?: boolean`
168
+ - `validateUrl?: (url: string) => boolean`
169
+ - `clickableLinks?: boolean`
170
+ - `openLinksInNewTab?: boolean`
171
+
172
+ Commands:
173
+
174
+ - `insertLink(url?, text?)`
175
+ - `updateLink(url, rel?, target?)`
176
+ - `removeLink()`
177
+ - `getCurrentLink()`
178
+ - `getLinkByKey(linkNodeKey)`
179
+ - `updateLinkByKey(linkNodeKey, url, rel?, target?)`
180
+ - `removeLinkByKey(linkNodeKey)`
181
+
182
+ Note: set `autoLinkUrls` explicitly in your config for unambiguous paste-link behavior.
183
+
184
+ ### Typography Selectors
185
+
186
+ #### `FontFamilyExtension`
187
+
188
+ `FontFamilyOption`:
189
+
190
+ - `value: string`
191
+ - `label: string`
192
+ - `fontFamily: string`
193
+ - `cssImportUrl?: string`
194
+
195
+ `FontFamilyConfig`:
196
+
197
+ - `options: readonly FontFamilyOption[]`
198
+ - `cssLoadStrategy: "none" | "preload-all" | "on-demand"`
199
+
200
+ Nuances:
201
+
202
+ - Invalid/duplicate option values are sanitized out.
203
+ - `default` option is auto-inserted when omitted.
204
+
205
+ #### `FontSizeExtension`
206
+
207
+ `FontSizeOption`:
208
+
209
+ - `value: string`
210
+ - `label: string`
211
+ - `fontSize: string`
212
+
213
+ `FontSizeConfig`:
214
+
215
+ - `options: readonly FontSizeOption[]`
216
+
217
+ Nuances:
218
+
219
+ - Invalid/duplicate option values are sanitized out.
220
+ - `default` option is auto-inserted when omitted.
221
+
222
+ #### `LineHeightExtension`
223
+
224
+ `LineHeightOption`:
225
+
226
+ - `value: string`
227
+ - `label: string`
228
+ - `lineHeight: string`
229
+
230
+ `LineHeightConfig`:
231
+
232
+ - `options: readonly LineHeightOption[]`
233
+ - `defaultLineHeight?: string` (default `"1.5"`)
234
+
235
+ Nuances:
236
+
237
+ - `value: "default"` maps to `defaultLineHeight` (or `"1.5"` when not configured).
238
+ - Non-default entries should use numeric ratios `>= 1.0` (`"1"`, `"1.5"`, `"2"`).
239
+ - Line height is applied at block level (TinyMCE-style): selecting text inside a block updates that whole block.
240
+
241
+ #### `TextColorExtension`
242
+
243
+ `TextColorOption`:
244
+
245
+ - `value: string`
246
+ - `label: string`
247
+ - `color: string`
248
+
249
+ `TextColorConfig`:
250
+
251
+ - `options: readonly TextColorOption[]`
252
+
253
+ Nuances:
254
+
255
+ - `setTextColor` accepts configured option values and valid CSS colors.
256
+
257
+ #### `TextHighlightExtension`
258
+
259
+ `TextHighlightOption`:
260
+
261
+ - `value: string`
262
+ - `label: string`
263
+ - `backgroundColor: string`
153
264
 
154
- # pnpm
155
- pnpm add @lyfie/luthor-headless
156
- pnpm add lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
265
+ `TextHighlightConfig`:
266
+
267
+ - `options: readonly TextHighlightOption[]`
268
+
269
+ Nuances:
270
+
271
+ - `setTextHighlight` accepts configured option values and valid CSS colors.
272
+ - Highlight styling also patches padding/box-decoration style helpers for clean rendering.
273
+
274
+ ### Block/Structure Extensions
275
+
276
+ #### `blockFormatExtension`
277
+
278
+ Commands include paragraph/heading/quote toggles and alignment helpers. No custom config required.
279
+
280
+ #### `listExtension`
281
+
282
+ Commands include unordered/ordered/check list toggles and indentation commands. No custom config required.
283
+
284
+ #### `horizontalRuleExtension`
285
+
286
+ Commands include horizontal rule insertion. No custom config required.
287
+
288
+ ### Code Extensions
289
+
290
+ #### `CodeExtension`
291
+
292
+ `CodeExtensionConfig`:
293
+
294
+ - `syntaxHighlighting?: "auto" | "disabled"`
295
+ - `tokenizer?: CodeTokenizer | null`
296
+ - `provider?: CodeHighlightProvider | null`
297
+ - `loadProvider?: () => Promise<CodeHighlightProvider | null>`
298
+
299
+ Usage (manual language selection + plaintext fallback):
300
+
301
+ ```tsx
302
+ import {
303
+ codeExtension,
304
+ codeIntelligenceExtension,
305
+ } from "@lyfie/luthor-headless";
306
+
307
+ const extensions = [
308
+ codeExtension.configure({
309
+ syntaxHighlighting: "auto", // default
310
+ }),
311
+ codeIntelligenceExtension,
312
+ ] as const;
157
313
  ```
158
314
 
159
- **Use this when:**
160
- - You want complete control over Lexical versions
161
- - Building a custom editor UI from scratch
162
- - Need minimum bundle size
163
- - Want to manage dependencies yourself
315
+ - `CodeIntelligenceExtension` no longer auto-detects code languages.
316
+ - Selecting `plaintext` keeps code tokens in the plaintext fallback theme (`plain`).
317
+ - Selecting any non-plaintext language switches token classes to the `hljs-*` namespace.
318
+ - Language values are alias-normalized (`md` -> `markdown`, `ts` -> `typescript`, `js` -> `javascript` family).
319
+ - Only Prism-supported loaded languages are accepted. Unsupported values are treated as plaintext.
320
+ - If your app loads a highlight.js stylesheet, those `hljs-*` token colors are applied automatically.
321
+ - Without a highlight.js stylesheet, code remains muted/plain fallback.
164
322
 
165
- ### Option 2: Full Package (Recommended for Quick Start)
323
+ `CodeHighlightProvider` shape:
166
324
 
167
- For a batteries-included experience:
325
+ - `highlightAuto?(code, languageSubset?)`
326
+ - `tokenizer?: CodeTokenizer | null`
327
+ - `getTokenizer?: () => CodeTokenizer | null | Promise<CodeTokenizer | null>`
168
328
 
169
- ```bash
170
- # npm
171
- npm install @lyfie/luthor-headless @lyfie/luthor
329
+ #### `CodeIntelligenceExtension`
330
+
331
+ `CodeLanguageOptionsConfig`:
332
+
333
+ - `mode?: "append" | "replace"`
334
+ - `values: readonly string[]`
335
+
336
+ `CodeIntelligenceConfig`:
337
+
338
+ - `provider?: CodeHighlightProvider | null`
339
+ - `loadProvider?: () => Promise<CodeHighlightProvider | null>`
340
+ - `maxAutoDetectLength?: number` (default `12000`)
341
+ - `isCopyAllowed?: boolean` (default `true`)
342
+ - `languageOptions?: readonly string[] | CodeLanguageOptionsConfig`
343
+
344
+ Commands:
345
+
346
+ - `setCodeLanguage(language)`
347
+ - `autoDetectCodeLanguage()`
348
+ - `getCurrentCodeLanguage()`
349
+ - `getCodeLanguageOptions()`
350
+ - `copySelectedCodeBlock()`
351
+
352
+ Nuances:
353
+
354
+ - Array form for `languageOptions` is equivalent to `{ mode: "append", values }`.
355
+ - Aliases are normalized.
356
+ - Duplicate normalized languages throw.
357
+
358
+ ### History and Input Behavior
359
+
360
+ - `historyExtension`: undo/redo commands and canUndo/canRedo state.
361
+ - `tabIndentExtension`: tab/shift-tab indent behavior.
362
+ - `enterKeyBehaviorExtension`: enter behavior normalization (quotes/code/table transitions). No extra config.
363
+
364
+ ### Table Extension (`TableExtension`, `tableExtension`)
365
+
366
+ `TableConfig`:
367
+
368
+ - `rows?: number`
369
+ - `columns?: number`
370
+ - `includeHeaders?: boolean`
371
+ - `enableContextMenu?: boolean`
372
+ - `contextMenuItems?: ContextMenuItem[] | ((commands: TableCommands) => ContextMenuItem[])`
373
+ - `contextMenuRenderer?: ContextMenuRenderer`
374
+ - `contextMenuExtension?: typeof contextMenuExtension`
375
+ - `tableBubbleRenderer?: (props: TableBubbleRenderProps) => ReactNode`
376
+
377
+ `TableBubbleRenderProps`:
378
+
379
+ - `headersEnabled: boolean`
380
+ - `setHeadersEnabled(enabled)`
381
+ - `actions`: row/column insert/delete + delete table actions
382
+
383
+ Defaults:
384
+
385
+ - `rows: 3`
386
+ - `columns: 3`
387
+ - `includeHeaders: false`
388
+ - `enableContextMenu: true`
389
+
390
+ ### Media Extensions
391
+
392
+ #### Image (`ImageExtension`, `imageExtension`)
393
+
394
+ `ImageExtensionConfig`:
395
+
396
+ - `uploadHandler?: (file: File) => Promise<string>`
397
+ - `defaultAlignment?: "left" | "right" | "center" | "none"`
398
+ - `classNames?: Partial<Record<Alignment | "wrapper" | "caption", string>>`
399
+ - `styles?: Partial<Record<Alignment | "wrapper" | "caption", CSSProperties>>`
400
+ - `customRenderer?: ComponentType<ImageComponentProps>`
401
+ - `resizable?: boolean` (default `true`)
402
+ - `scaleByRatio?: boolean` (default `false`)
403
+ - `pasteListener?: { insert: boolean; replace: boolean }` (default both `true`)
404
+ - `debug?: boolean` (default `false`)
405
+ - `forceUpload?: boolean` (default `false`)
406
+
407
+ #### Iframe (`IframeEmbedExtension`, `iframeEmbedExtension`)
408
+
409
+ `IframeEmbedConfig`:
410
+
411
+ - `defaultWidth?: number` (default `640`)
412
+ - `defaultHeight?: number` (default `360`)
413
+ - `defaultAlignment?: "left" | "center" | "right"` (default `"center"`)
414
+
415
+ #### YouTube (`YouTubeEmbedExtension`, `youTubeEmbedExtension`)
416
+
417
+ `YouTubeEmbedConfig`:
418
+
419
+ - `defaultWidth?: number` (default `640`)
420
+ - `defaultHeight?: number` (default `480`)
421
+ - `defaultAlignment?: "left" | "center" | "right"` (default `"center"`)
422
+ - `allowFullscreen?: boolean` (default `true`)
423
+ - `autoplay?: boolean` (default `false`)
424
+ - `controls?: boolean` (default `true`)
425
+ - `nocookie?: boolean` (default `true`)
426
+ - `rel?: number` (default `1`)
427
+
428
+ ### Command UI Extensions
172
429
 
173
- # pnpm
174
- pnpm add @lyfie/luthor-headless @lyfie/luthor
430
+ #### Slash Command (`SlashCommandExtension`, `slashCommandExtension`)
175
431
 
176
- # That's it! All Lexical dependencies included
432
+ `SlashCommandItem`:
433
+
434
+ - `id`, `label`, `action`
435
+ - optional: `description`, `keywords`, `category`, `icon`, `shortcut`
436
+
437
+ `SlashCommandConfig`:
438
+
439
+ - `trigger?: string` (default `"/"`)
440
+ - `offset?: { x: number; y: number }` (default `{ x: 0, y: 8 }`)
441
+ - `items?: readonly SlashCommandItem[]`
442
+
443
+ Commands:
444
+
445
+ - `registerSlashCommand(item)`
446
+ - `unregisterSlashCommand(id)`
447
+ - `setSlashCommands(items)`
448
+ - `closeSlashMenu()`
449
+ - `executeSlashCommand(id)`
450
+
451
+ #### Command Palette (`CommandPaletteExtension`, `commandPaletteExtension`)
452
+
453
+ `CommandPaletteItem`:
454
+
455
+ - `id`, `label`, `action`
456
+ - optional: `description`, `keywords`, `category`, `icon`, `shortcut`
457
+
458
+ Commands:
459
+
460
+ - `showCommandPalette()`
461
+ - `hideCommandPalette()`
462
+ - `registerCommand(item)`
463
+ - `unregisterCommand(id)`
464
+
465
+ #### Emoji (`EmojiExtension`, `emojiExtension`)
466
+
467
+ `EmojiCatalogItem`:
468
+
469
+ - `emoji`, `label`, `shortcodes`
470
+ - optional `keywords`
471
+
472
+ `EmojiConfig`:
473
+
474
+ - `trigger?: string` (default `":"`)
475
+ - `maxSuggestions?: number` (default `8`)
476
+ - `maxQueryLength?: number` (default `32`)
477
+ - `autoReplaceSymbols?: boolean` (default `true`)
478
+ - `symbolReplacements?: Record<string, string>`
479
+ - `catalog?: EmojiCatalogItem[]`
480
+ - `catalogAdapter?: { search(query, options?), resolveShortcode(shortcode), getAll() }`
481
+ - `autoDetectExternalCatalog?: boolean` (default `true`, auto-detects emoji-mart data if available)
482
+ - `offset?: { x: number; y: number }` (default `{ x: 0, y: 8 }`)
483
+
484
+ Commands:
485
+
486
+ - `insertEmoji(emoji)`
487
+ - `executeEmojiSuggestion(emoji)`
488
+ - `closeEmojiSuggestions()`
489
+ - `getEmojiSuggestions(query?)`
490
+ - `getEmojiCatalog()`
491
+ - `resolveEmojiShortcode(shortcode)`
492
+ - `setEmojiCatalog(catalog)`
493
+ - `setEmojiCatalogAdapter(adapter)`
494
+ - `getEmojiCatalogAdapter()`
495
+
496
+ Behavior:
497
+
498
+ - If no custom catalog/adapter is provided, emoji will auto-detect emoji-mart data when available.
499
+ - If nothing is detected, it falls back to the built-in lightweight catalog.
500
+
501
+ Usage (including `apps/demo`):
502
+
503
+ ```bash
504
+ pnpm add -F demo @emoji-mart/data
177
505
  ```
178
506
 
179
- **Use this when:**
180
- - You want ready-to-use editor presets
181
- - Don't want to manage Lexical dependencies
182
- - Need a working editor quickly
183
- - Want plug-and-play components
507
+ - No editor config changes are required.
508
+ - After install, typing `:shortcode` and opening the emoji toolbar picker will use the detected emoji-mart catalog.
509
+ - If `@emoji-mart/data` is not installed (or not available globally), behavior stays on the built-in fallback catalog.
510
+
511
+ ### Context/Overlay Extensions
512
+
513
+ #### Context Menu (`ContextMenuExtension`, `contextMenuExtension`)
184
514
 
185
- [📖 See @lyfie/luthor documentation](../luthor/README.md)
515
+ `ContextMenuConfig`:
186
516
 
187
- ## Features
517
+ - `defaultRenderer?: ContextMenuRenderer`
518
+ - `preventDefault?: boolean`
519
+ - `theme?: { container?: string; item?: string; itemDisabled?: string }`
520
+ - `styles?: { container?: CSSProperties; item?: CSSProperties; itemDisabled?: CSSProperties }`
188
521
 
189
- ### 🎨 Built-in Extensions (25+)
190
- - **Text Formatting**: Bold, italic, underline, strikethrough, inline code
191
- - **Structure**: Headings, lists (with nesting), quotes, horizontal rules
192
- - **Rich Content**: Tables, images (upload/paste/alignment), links, code blocks
193
- - **Advanced**: History (undo/redo), command palette, floating toolbar, context menus
522
+ Commands:
194
523
 
195
- ### 🎯 Smart List Handling
196
- - Toggle lists with intelligent nesting behavior
197
- - Context-aware toolbar (indent/outdent appear when needed)
198
- - Nested lists without keyboard shortcuts
199
- - Clean UX that matches modern editors
524
+ - `registerProvider(provider)`
525
+ - `unregisterProvider(id)`
526
+ - `showContextMenu({ items, position, renderer? })`
527
+ - `hideContextMenu()`
200
528
 
201
- ### 📤 Export & Import
202
- - **HTML** with semantic markup
203
- - **Markdown** with GitHub Flavored syntax
204
- - **JSON** for structured data
205
- - Custom transformers for specialized formats
529
+ `ContextMenuProvider`:
206
530
 
207
- ### 🎨 Theming & Styling
208
- - CSS classes or Tailwind utilities
209
- - Custom themes for consistent styling
210
- - Dark mode support
211
- - Accessible by default
531
+ - `id`
532
+ - `priority?`
533
+ - `canHandle(context)`
534
+ - `getItems(context)`
535
+ - `renderer?`
212
536
 
213
- ## Real World Usage
537
+ #### Floating Toolbar (`FloatingToolbarExtension`, `floatingToolbarExtension`)
214
538
 
215
- Luthor powers editors in:
216
- - Content management systems
217
- - Documentation platforms
218
- - Blog editors
219
- - Note-taking applications
220
- - Comment systems
221
- - Collaborative writing tools
539
+ `FloatingConfig<TCommands, TStates>`:
222
540
 
223
- ## Community & Support
541
+ - `render(props)`
542
+ - `getCommands?(): TCommands`
543
+ - `getActiveStates?(): TStates`
544
+ - `anchorElem?: HTMLElement`
545
+ - `debounceMs?: number` (default `100`)
546
+ - `offset?: { x: number; y: number }` (default `{ x: 0, y: 8 }`)
547
+ - `positionStrategy?: "above" | "below" | "auto"` (default `"below"`)
548
+ - `theme?: { container?: string; button?: string; buttonActive?: string }`
549
+ - `toolbarDimensions?: { width: number; height: number }`
224
550
 
225
- - **[💬 Discord](https://discord.gg/RAMYSDRag7)** - Get help, share ideas
226
- - **[🐛 GitHub Issues](https://github.com/lyfie-app/luthor/issues)** - Bug reports, feature requests
227
- - **[💭 Discussions](https://github.com/lyfie-app/luthor/discussions)** - Questions, showcase your projects
551
+ #### Draggable Blocks (`DraggableBlockExtension`, `draggableBlockExtension`)
228
552
 
229
- ## Contributing
553
+ `DraggableConfig`:
230
554
 
231
- We welcome contributions! Whether you:
232
- - Find and report bugs
233
- - Suggest new features
234
- - Contribute code or documentation
235
- - Share projects built with Luthor
236
- - Star the repo to show support
555
+ - `anchorElem?: HTMLElement`
556
+ - `showAddButton?: boolean`
557
+ - `buttonStackPosition?: "left" | "right"`
558
+ - `enableTextSelectionDrag?: boolean`
559
+ - `offsetLeft?: number`
560
+ - `offsetRight?: number`
561
+ - `theme?: { handle?, handleActive?, blockDragging?, dropIndicator?, addButton?, buttonStack? }`
562
+ - `styles?: { handle?, handleActive?, blockDragging?, dropIndicator?, addButton?, buttonStack? }`
563
+ - `handleRenderer?(props)`
564
+ - `buttonsRenderer?(props)`
565
+ - `dropIndicatorRenderer?(props)`
237
566
 
238
- Check our [Contributing Guide](./CONTRIBUTING.md) to get started.
567
+ Default behavior includes draggable handle, add button, and left-side controls.
239
568
 
240
- ## Support This Project
569
+ ### Custom Nodes
241
570
 
242
- Luthor is free and open source, built by developers for developers. If it's helping you build better editors, consider supporting its development:
571
+ `createCustomNodeExtension(config)` lets you define a full custom node extension.
243
572
 
244
- - **⭐ Star this repository** to show your support
245
- - **💝 [Sponsor the project](https://github.com/sponsors/rahulnsanand)** to help with maintenance and new features
246
- - **📢 Share Luthor** with other developers
573
+ `CustomNodeConfig` includes:
247
574
 
248
- Your support keeps this project alive and helps us build better tools for the React community.
575
+ - `nodeType: string`
576
+ - `isContainer?: boolean`
577
+ - `defaultPayload?: Record<string, any>`
578
+ - `initialChildren?: () => SerializedLexicalNode[]`
579
+ - `render?` or `jsx?`
580
+ - DOM import/export hooks (`createDOM`, `updateDOM`, `importDOM`, `exportDOM`)
581
+ - `commands?(editor)`
582
+ - `stateQueries?(editor)`
249
583
 
250
- ---
584
+ Returns:
251
585
 
252
- **Built with ❤️ by [Rahul NS Anand](https://github.com/rahulnsanand)**
586
+ - `extension`
587
+ - `$createCustomNode(payload?)`
588
+ - `jsxToDOM(jsxElement)`
253
589
 
254
- MIT License - Use it however you want.</content>
590
+ ## Complete Extension Example
255
591
 
592
+ ```tsx
593
+ import {
594
+ createEditorSystem,
595
+ RichText,
596
+ richTextExtension,
597
+ historyExtension,
598
+ boldExtension,
599
+ italicExtension,
600
+ linkExtension,
601
+ listExtension,
602
+ blockFormatExtension,
603
+ tableExtension,
604
+ imageExtension,
605
+ slashCommandExtension,
606
+ commandPaletteExtension,
607
+ codeExtension,
608
+ codeIntelligenceExtension,
609
+ } from "@lyfie/luthor-headless";
610
+
611
+ const extensions = [
612
+ richTextExtension,
613
+ historyExtension,
614
+ boldExtension,
615
+ italicExtension,
616
+ linkExtension.configure({
617
+ autoLinkText: true,
618
+ autoLinkUrls: true,
619
+ linkSelectedTextOnPaste: true,
620
+ }),
621
+ listExtension,
622
+ blockFormatExtension,
623
+ tableExtension.configure({
624
+ rows: 3,
625
+ columns: 4,
626
+ includeHeaders: true,
627
+ }),
628
+ imageExtension.configure({
629
+ resizable: true,
630
+ scaleByRatio: true,
631
+ }),
632
+ codeExtension.configure({
633
+ syntaxHighlighting: "auto",
634
+ }),
635
+ codeIntelligenceExtension.configure({
636
+ maxAutoDetectLength: 12000,
637
+ isCopyAllowed: true,
638
+ languageOptions: {
639
+ mode: "append",
640
+ values: ["sql", "yaml"],
641
+ },
642
+ }),
643
+ slashCommandExtension,
644
+ commandPaletteExtension,
645
+ ] as const;
646
+
647
+ const { Provider } = createEditorSystem<typeof extensions>();
648
+
649
+ export function Editor() {
650
+ return (
651
+ <Provider extensions={extensions}>
652
+ <RichText placeholder="Write here..." />
653
+ </Provider>
654
+ );
655
+ }
656
+ ```
657
+
658
+ ## Documentation
659
+
660
+ - Monorepo docs index: [../../documentation/index.md](../../documentation/index.md)
661
+ - User docs: [../../documentation/user/headless/getting-started.md](../../documentation/user/headless/getting-started.md)
662
+ - Developer docs: [../../documentation/developer/headless/architecture.md](../../documentation/developer/headless/architecture.md)
663
+ - Luthor preset package README: [../luthor/README.md](../luthor/README.md)
664
+
665
+ ## Workspace Development
666
+
667
+ ```bash
668
+ pnpm --filter @lyfie/luthor-headless dev
669
+ pnpm --filter @lyfie/luthor-headless build
670
+ pnpm --filter @lyfie/luthor-headless lint
671
+ ```