@lyfie/luthor-headless 2.2.0 → 2.3.1
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 +590 -174
- 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,255 +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
|
-
|
|
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
|
-
|
|
17
|
+
Optional syntax highlighting provider:
|
|
57
18
|
|
|
58
19
|
```bash
|
|
59
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
### Basic Usage
|
|
23
|
+
Optional emoji catalog provider:
|
|
69
24
|
|
|
70
25
|
```bash
|
|
71
|
-
|
|
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
|
-
|
|
29
|
+
Optional package behavior:
|
|
79
30
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
64
|
+
export function Editor() {
|
|
133
65
|
return (
|
|
134
66
|
<Provider extensions={extensions}>
|
|
135
|
-
<
|
|
67
|
+
<Toolbar />
|
|
68
|
+
<RichText placeholder="Write here..." />
|
|
136
69
|
</Provider>
|
|
137
70
|
);
|
|
138
71
|
}
|
|
139
72
|
```
|
|
140
73
|
|
|
141
|
-
|
|
74
|
+
## Core API
|
|
142
75
|
|
|
143
|
-
|
|
76
|
+
### `createEditorSystem<Exts>()`
|
|
144
77
|
|
|
145
|
-
|
|
78
|
+
Returns:
|
|
146
79
|
|
|
147
|
-
|
|
80
|
+
- `Provider(props)`
|
|
81
|
+
- `useEditor()`
|
|
148
82
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
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
|
-
|
|
323
|
+
`CodeHighlightProvider` shape:
|
|
166
324
|
|
|
167
|
-
|
|
325
|
+
- `highlightAuto?(code, languageSubset?)`
|
|
326
|
+
- `tokenizer?: CodeTokenizer | null`
|
|
327
|
+
- `getTokenizer?: () => CodeTokenizer | null | Promise<CodeTokenizer | null>`
|
|
168
328
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
pnpm add @lyfie/luthor-headless @lyfie/luthor
|
|
430
|
+
#### Slash Command (`SlashCommandExtension`, `slashCommandExtension`)
|
|
175
431
|
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
515
|
+
`ContextMenuConfig`:
|
|
186
516
|
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
- Clean UX that matches modern editors
|
|
524
|
+
- `registerProvider(provider)`
|
|
525
|
+
- `unregisterProvider(id)`
|
|
526
|
+
- `showContextMenu({ items, position, renderer? })`
|
|
527
|
+
- `hideContextMenu()`
|
|
200
528
|
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
531
|
+
- `id`
|
|
532
|
+
- `priority?`
|
|
533
|
+
- `canHandle(context)`
|
|
534
|
+
- `getItems(context)`
|
|
535
|
+
- `renderer?`
|
|
212
536
|
|
|
213
|
-
|
|
537
|
+
#### Floating Toolbar (`FloatingToolbarExtension`, `floatingToolbarExtension`)
|
|
214
538
|
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
+
`DraggableConfig`:
|
|
230
554
|
|
|
231
|
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
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
|
-
|
|
567
|
+
Default behavior includes draggable handle, add button, and left-side controls.
|
|
239
568
|
|
|
240
|
-
|
|
569
|
+
### Custom Nodes
|
|
241
570
|
|
|
242
|
-
|
|
571
|
+
`createCustomNodeExtension(config)` lets you define a full custom node extension.
|
|
243
572
|
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
+
- `extension`
|
|
587
|
+
- `$createCustomNode(payload?)`
|
|
588
|
+
- `jsxToDOM(jsxElement)`
|
|
253
589
|
|
|
254
|
-
|
|
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
|
+
```
|