@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 +592 -156
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +2531 -0
- package/dist/index.d.ts +735 -515
- package/dist/index.js +5 -3
- package/package.json +37 -18
package/README.md
CHANGED
|
@@ -1,235 +1,671 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lyfie/luthor-headless
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe, headless rich text editor system for React, built on Lexical.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Package responsibility
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-

|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
Optional syntax highlighting provider:
|
|
53
18
|
|
|
54
19
|
```bash
|
|
55
|
-
|
|
20
|
+
pnpm add highlight.js
|
|
56
21
|
```
|
|
57
22
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### Basic Usage
|
|
23
|
+
Optional emoji catalog provider:
|
|
61
24
|
|
|
62
25
|
```bash
|
|
63
|
-
|
|
26
|
+
pnpm add @emoji-mart/data
|
|
64
27
|
```
|
|
65
28
|
|
|
66
|
-
|
|
29
|
+
Optional package behavior:
|
|
67
30
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
|
64
|
+
export function Editor() {
|
|
117
65
|
return (
|
|
118
66
|
<Provider extensions={extensions}>
|
|
119
|
-
<
|
|
67
|
+
<Toolbar />
|
|
68
|
+
<RichText placeholder="Write here..." />
|
|
120
69
|
</Provider>
|
|
121
70
|
);
|
|
122
71
|
}
|
|
123
72
|
```
|
|
124
73
|
|
|
125
|
-
|
|
74
|
+
## Core API
|
|
126
75
|
|
|
127
|
-
|
|
76
|
+
### `createEditorSystem<Exts>()`
|
|
128
77
|
|
|
129
|
-
|
|
78
|
+
Returns:
|
|
130
79
|
|
|
131
|
-
|
|
80
|
+
- `Provider(props)`
|
|
81
|
+
- `useEditor()`
|
|
132
82
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
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
|
-
|
|
323
|
+
`CodeHighlightProvider` shape:
|
|
148
324
|
|
|
149
|
-
|
|
325
|
+
- `highlightAuto?(code, languageSubset?)`
|
|
326
|
+
- `tokenizer?: CodeTokenizer | null`
|
|
327
|
+
- `getTokenizer?: () => CodeTokenizer | null | Promise<CodeTokenizer | null>`
|
|
150
328
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
515
|
+
`ContextMenuConfig`:
|
|
166
516
|
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
- Clean UX that matches modern editors
|
|
524
|
+
- `registerProvider(provider)`
|
|
525
|
+
- `unregisterProvider(id)`
|
|
526
|
+
- `showContextMenu({ items, position, renderer? })`
|
|
527
|
+
- `hideContextMenu()`
|
|
180
528
|
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
-
|
|
531
|
+
- `id`
|
|
532
|
+
- `priority?`
|
|
533
|
+
- `canHandle(context)`
|
|
534
|
+
- `getItems(context)`
|
|
535
|
+
- `renderer?`
|
|
192
536
|
|
|
193
|
-
|
|
537
|
+
#### Floating Toolbar (`FloatingToolbarExtension`, `floatingToolbarExtension`)
|
|
194
538
|
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
+
`DraggableConfig`:
|
|
210
554
|
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
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
|
-
|
|
567
|
+
Default behavior includes draggable handle, add button, and left-side controls.
|
|
219
568
|
|
|
220
|
-
|
|
569
|
+
### Custom Nodes
|
|
221
570
|
|
|
222
|
-
|
|
571
|
+
`createCustomNodeExtension(config)` lets you define a full custom node extension.
|
|
223
572
|
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
+
- `extension`
|
|
587
|
+
- `$createCustomNode(payload?)`
|
|
588
|
+
- `jsxToDOM(jsxElement)`
|
|
233
589
|
|
|
234
|
-
|
|
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
|
+
```
|