@lyfie/luthor-headless 2.3.1 → 2.3.3
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 +8 -659
- package/package.json +4 -5
- package/dist/index.cjs +0 -5
- package/dist/index.d.cts +0 -2531
package/README.md
CHANGED
|
@@ -1,671 +1,20 @@
|
|
|
1
1
|
# @lyfie/luthor-headless
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless, extension-first rich text editor runtime for React on top of Lexical.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
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`.
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
```bash
|
|
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
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Optional syntax highlighting provider:
|
|
5
|
+
## Install
|
|
18
6
|
|
|
19
7
|
```bash
|
|
20
|
-
pnpm add
|
|
8
|
+
pnpm add @lyfie/luthor-headless lexical @lexical/code @lexical/link @lexical/list @lexical/markdown @lexical/react @lexical/rich-text @lexical/selection @lexical/table @lexical/utils react react-dom
|
|
21
9
|
```
|
|
22
10
|
|
|
23
|
-
Optional
|
|
11
|
+
Optional:
|
|
24
12
|
|
|
25
13
|
```bash
|
|
26
|
-
pnpm add @emoji-mart/data
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Optional package behavior:
|
|
30
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
```tsx
|
|
38
|
-
import {
|
|
39
|
-
createEditorSystem,
|
|
40
|
-
RichText,
|
|
41
|
-
richTextExtension,
|
|
42
|
-
boldExtension,
|
|
43
|
-
italicExtension,
|
|
44
|
-
} from "@lyfie/luthor-headless";
|
|
45
|
-
|
|
46
|
-
const extensions = [richTextExtension, boldExtension, italicExtension] as const;
|
|
47
|
-
const { Provider, useEditor } = createEditorSystem<typeof extensions>();
|
|
48
|
-
|
|
49
|
-
function Toolbar() {
|
|
50
|
-
const { commands, activeStates } = useEditor();
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div>
|
|
54
|
-
<button onClick={() => commands.toggleBold()} aria-pressed={activeStates.bold}>
|
|
55
|
-
Bold
|
|
56
|
-
</button>
|
|
57
|
-
<button onClick={() => commands.toggleItalic()} aria-pressed={activeStates.italic}>
|
|
58
|
-
Italic
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function Editor() {
|
|
65
|
-
return (
|
|
66
|
-
<Provider extensions={extensions}>
|
|
67
|
-
<Toolbar />
|
|
68
|
-
<RichText placeholder="Write here..." />
|
|
69
|
-
</Provider>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
14
|
+
pnpm add highlight.js @emoji-mart/data
|
|
72
15
|
```
|
|
73
16
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
### `createEditorSystem<Exts>()`
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
|
|
80
|
-
- `Provider(props)`
|
|
81
|
-
- `useEditor()`
|
|
82
|
-
|
|
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`:
|
|
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>`
|
|
17
|
+
## Docs
|
|
298
18
|
|
|
299
|
-
|
|
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;
|
|
313
|
-
```
|
|
314
|
-
|
|
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.
|
|
322
|
-
|
|
323
|
-
`CodeHighlightProvider` shape:
|
|
324
|
-
|
|
325
|
-
- `highlightAuto?(code, languageSubset?)`
|
|
326
|
-
- `tokenizer?: CodeTokenizer | null`
|
|
327
|
-
- `getTokenizer?: () => CodeTokenizer | null | Promise<CodeTokenizer | null>`
|
|
328
|
-
|
|
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`)
|
|
431
|
-
|
|
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
|
|
505
|
-
```
|
|
506
|
-
|
|
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`)
|
|
514
|
-
|
|
515
|
-
`ContextMenuConfig`:
|
|
516
|
-
|
|
517
|
-
- `defaultRenderer?: ContextMenuRenderer`
|
|
518
|
-
- `preventDefault?: boolean`
|
|
519
|
-
- `theme?: { container?: string; item?: string; itemDisabled?: string }`
|
|
520
|
-
- `styles?: { container?: CSSProperties; item?: CSSProperties; itemDisabled?: CSSProperties }`
|
|
521
|
-
|
|
522
|
-
Commands:
|
|
523
|
-
|
|
524
|
-
- `registerProvider(provider)`
|
|
525
|
-
- `unregisterProvider(id)`
|
|
526
|
-
- `showContextMenu({ items, position, renderer? })`
|
|
527
|
-
- `hideContextMenu()`
|
|
528
|
-
|
|
529
|
-
`ContextMenuProvider`:
|
|
530
|
-
|
|
531
|
-
- `id`
|
|
532
|
-
- `priority?`
|
|
533
|
-
- `canHandle(context)`
|
|
534
|
-
- `getItems(context)`
|
|
535
|
-
- `renderer?`
|
|
536
|
-
|
|
537
|
-
#### Floating Toolbar (`FloatingToolbarExtension`, `floatingToolbarExtension`)
|
|
538
|
-
|
|
539
|
-
`FloatingConfig<TCommands, TStates>`:
|
|
540
|
-
|
|
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 }`
|
|
550
|
-
|
|
551
|
-
#### Draggable Blocks (`DraggableBlockExtension`, `draggableBlockExtension`)
|
|
552
|
-
|
|
553
|
-
`DraggableConfig`:
|
|
554
|
-
|
|
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)`
|
|
566
|
-
|
|
567
|
-
Default behavior includes draggable handle, add button, and left-side controls.
|
|
568
|
-
|
|
569
|
-
### Custom Nodes
|
|
570
|
-
|
|
571
|
-
`createCustomNodeExtension(config)` lets you define a full custom node extension.
|
|
572
|
-
|
|
573
|
-
`CustomNodeConfig` includes:
|
|
574
|
-
|
|
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)`
|
|
583
|
-
|
|
584
|
-
Returns:
|
|
585
|
-
|
|
586
|
-
- `extension`
|
|
587
|
-
- `$createCustomNode(payload?)`
|
|
588
|
-
- `jsxToDOM(jsxElement)`
|
|
589
|
-
|
|
590
|
-
## Complete Extension Example
|
|
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
|
-
```
|
|
19
|
+
- https://luthor-editor.vercel.app/docs/getting-started/luthor-headless/
|
|
20
|
+
- https://luthor-editor.vercel.app/docs/luthor-headless/features/
|
package/package.json
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyfie/luthor-headless",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "Luthor Editor - A headless, extensible rich text editor built on Lexical",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
|
-
"main": "./dist/index.
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts"
|
|
12
12
|
],
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"import": "./dist/index.js",
|
|
18
|
-
"require": "./dist/index.cjs",
|
|
19
18
|
"default": "./dist/index.js"
|
|
20
19
|
}
|
|
21
20
|
},
|