@lyfie/luthor-headless 2.1.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,235 +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 install @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
50
15
  ```
51
16
 
52
- Install the required Lexical peer dependencies:
17
+ Optional syntax highlighting provider:
53
18
 
54
19
  ```bash
55
- npm install 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
56
21
  ```
57
22
 
58
- > **💡 Want a simpler setup?** Check out [@lyfie/luthor](../luthor/README.md) which bundles all Lexical dependencies for you.
59
-
60
- ### Basic Usage
23
+ Optional emoji catalog provider:
61
24
 
62
25
  ```bash
63
- npm install @lyfie/luthor-headless
26
+ pnpm add @emoji-mart/data
64
27
  ```
65
28
 
66
- Install the Lexical peer dependencies:
29
+ Optional package behavior:
67
30
 
68
- ```bash
69
- npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils
70
- ```
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.
34
+
35
+ ## Quick Start
71
36
 
72
37
  ```tsx
73
38
  import {
74
39
  createEditorSystem,
40
+ RichText,
41
+ richTextExtension,
75
42
  boldExtension,
76
43
  italicExtension,
77
- listExtension,
78
- RichText,
79
44
  } from "@lyfie/luthor-headless";
80
45
 
81
- const extensions = [boldExtension, italicExtension, listExtension] as const;
46
+ const extensions = [richTextExtension, boldExtension, italicExtension] as const;
82
47
  const { Provider, useEditor } = createEditorSystem<typeof extensions>();
83
48
 
84
49
  function Toolbar() {
85
50
  const { commands, activeStates } = useEditor();
51
+
86
52
  return (
87
- <div className="toolbar">
88
- <button
89
- onClick={() => commands.toggleBold()}
90
- className={activeStates.bold ? "active" : ""}
91
- >
53
+ <div>
54
+ <button onClick={() => commands.toggleBold()} aria-pressed={activeStates.bold}>
92
55
  Bold
93
56
  </button>
94
- <button
95
- onClick={() => commands.toggleItalic()}
96
- className={activeStates.italic ? "active" : ""}
97
- >
57
+ <button onClick={() => commands.toggleItalic()} aria-pressed={activeStates.italic}>
98
58
  Italic
99
59
  </button>
100
- <button onClick={() => commands.toggleUnorderedList()}>
101
- Bullet List
102
- </button>
103
- </div>
104
- );
105
- }
106
-
107
- function Editor() {
108
- return (
109
- <div className="editor-container">
110
- <Toolbar />
111
- <RichText placeholder="Start writing..." />
112
60
  </div>
113
61
  );
114
62
  }
115
63
 
116
- export default function App() {
64
+ export function Editor() {
117
65
  return (
118
66
  <Provider extensions={extensions}>
119
- <Editor />
67
+ <Toolbar />
68
+ <RichText placeholder="Write here..." />
120
69
  </Provider>
121
70
  );
122
71
  }
123
72
  ```
124
73
 
125
- **That's it.** You now have a fully functional, type-safe rich text editor.
74
+ ## Core API
126
75
 
127
- ## Installation Options
76
+ ### `createEditorSystem<Exts>()`
128
77
 
129
- ### Option 1: Headless Package (This Package)
78
+ Returns:
130
79
 
131
- For maximum control and flexibility:
80
+ - `Provider(props)`
81
+ - `useEditor()`
132
82
 
133
- ```bash
134
- # Install headless package
135
- npm install @lyfie/luthor-headless
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`
264
+
265
+ `TextHighlightConfig`:
136
266
 
137
- # Manually install Lexical peer dependencies
138
- npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils @lexical/code @lexical/link @lexical/table
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;
139
313
  ```
140
314
 
141
- **Use this when:**
142
- - You want complete control over Lexical versions
143
- - Building a custom editor UI from scratch
144
- - Need minimum bundle size
145
- - 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.
146
322
 
147
- ### Option 2: Full Package (Recommended for Quick Start)
323
+ `CodeHighlightProvider` shape:
148
324
 
149
- For a batteries-included experience:
325
+ - `highlightAuto?(code, languageSubset?)`
326
+ - `tokenizer?: CodeTokenizer | null`
327
+ - `getTokenizer?: () => CodeTokenizer | null | Promise<CodeTokenizer | null>`
150
328
 
151
- ```bash
152
- # Install both packages
153
- npm install @lyfie/luthor-headless
154
- npm install @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
429
+
430
+ #### Slash Command (`SlashCommandExtension`, `slashCommandExtension`)
155
431
 
156
- # 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
157
505
  ```
158
506
 
159
- **Use this when:**
160
- - You want ready-to-use editor presets
161
- - Don't want to manage Lexical dependencies
162
- - Need a working editor quickly
163
- - 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`)
164
514
 
165
- [📖 See @lyfie/luthor documentation](../luthor/README.md)
515
+ `ContextMenuConfig`:
166
516
 
167
- ## Features
517
+ - `defaultRenderer?: ContextMenuRenderer`
518
+ - `preventDefault?: boolean`
519
+ - `theme?: { container?: string; item?: string; itemDisabled?: string }`
520
+ - `styles?: { container?: CSSProperties; item?: CSSProperties; itemDisabled?: CSSProperties }`
168
521
 
169
- ### 🎨 Built-in Extensions (25+)
170
- - **Text Formatting**: Bold, italic, underline, strikethrough, inline code
171
- - **Structure**: Headings, lists (with nesting), quotes, horizontal rules
172
- - **Rich Content**: Tables, images (upload/paste/alignment), links, code blocks
173
- - **Advanced**: History (undo/redo), command palette, floating toolbar, context menus
522
+ Commands:
174
523
 
175
- ### 🎯 Smart List Handling
176
- - Toggle lists with intelligent nesting behavior
177
- - Context-aware toolbar (indent/outdent appear when needed)
178
- - Nested lists without keyboard shortcuts
179
- - Clean UX that matches modern editors
524
+ - `registerProvider(provider)`
525
+ - `unregisterProvider(id)`
526
+ - `showContextMenu({ items, position, renderer? })`
527
+ - `hideContextMenu()`
180
528
 
181
- ### 📤 Export & Import
182
- - **HTML** with semantic markup
183
- - **Markdown** with GitHub Flavored syntax
184
- - **JSON** for structured data
185
- - Custom transformers for specialized formats
529
+ `ContextMenuProvider`:
186
530
 
187
- ### 🎨 Theming & Styling
188
- - CSS classes or Tailwind utilities
189
- - Custom themes for consistent styling
190
- - Dark mode support
191
- - Accessible by default
531
+ - `id`
532
+ - `priority?`
533
+ - `canHandle(context)`
534
+ - `getItems(context)`
535
+ - `renderer?`
192
536
 
193
- ## Real World Usage
537
+ #### Floating Toolbar (`FloatingToolbarExtension`, `floatingToolbarExtension`)
194
538
 
195
- Luthor powers editors in:
196
- - Content management systems
197
- - Documentation platforms
198
- - Blog editors
199
- - Note-taking applications
200
- - Comment systems
201
- - Collaborative writing tools
539
+ `FloatingConfig<TCommands, TStates>`:
202
540
 
203
- ## 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 }`
204
550
 
205
- - **[💬 Discord](https://discord.gg/RAMYSDRag7)** - Get help, share ideas
206
- - **[🐛 GitHub Issues](https://github.com/lyfie-app/luthor/issues)** - Bug reports, feature requests
207
- - **[💭 Discussions](https://github.com/lyfie-app/luthor/discussions)** - Questions, showcase your projects
551
+ #### Draggable Blocks (`DraggableBlockExtension`, `draggableBlockExtension`)
208
552
 
209
- ## Contributing
553
+ `DraggableConfig`:
210
554
 
211
- We welcome contributions! Whether you:
212
- - Find and report bugs
213
- - Suggest new features
214
- - Contribute code or documentation
215
- - Share projects built with Luthor
216
- - 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)`
217
566
 
218
- Check our [Contributing Guide](./CONTRIBUTING.md) to get started.
567
+ Default behavior includes draggable handle, add button, and left-side controls.
219
568
 
220
- ## Support This Project
569
+ ### Custom Nodes
221
570
 
222
- 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.
223
572
 
224
- - **⭐ Star this repository** to show your support
225
- - **💝 [Sponsor the project](https://github.com/sponsors/rahulnsanand)** to help with maintenance and new features
226
- - **📢 Share Luthor** with other developers
573
+ `CustomNodeConfig` includes:
227
574
 
228
- 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)`
229
583
 
230
- ---
584
+ Returns:
231
585
 
232
- **Built with ❤️ by [Rahul NS Anand](https://github.com/rahulnsanand)**
586
+ - `extension`
587
+ - `$createCustomNode(payload?)`
588
+ - `jsxToDOM(jsxElement)`
233
589
 
234
- MIT License - Use it however you want.</content>
590
+ ## Complete Extension Example
235
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
+ ```