@zuilib/text-editor 0.0.0 → 0.0.2
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 +132 -28
- package/dist/index.d.ts +44 -1
- package/dist/index.js +89 -3
- package/dist/styles.css +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,57 +1,161 @@
|
|
|
1
1
|
# @zuilib/text-editor
|
|
2
2
|
|
|
3
|
-
Markdown editor for ZUI
|
|
3
|
+
Markdown editor for ZUI built on [Lexical](https://lexical.dev/): rich editing with shortcuts, raw markdown mode, read-only view, checklists, and fenced code highlighting.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Notes, comments, or docs fields stored as **markdown strings**
|
|
8
|
+
- Controlled `value` / `onChange` integration with forms or autosave
|
|
9
|
+
- WYSIWYG-style editing without leaving markdown as the source of truth
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Peer dependencies (install in the consuming app):
|
|
14
|
+
|
|
15
|
+
| Package | Version |
|
|
16
|
+
|---------|---------|
|
|
17
|
+
| `react`, `react-dom` | `^18.0.0 \|\| ^19.0.0` |
|
|
18
|
+
| `lexical` | `^0.35.0` |
|
|
19
|
+
| `@lexical/react` | `^0.35.0` |
|
|
20
|
+
| `@lexical/markdown` | `^0.35.0` |
|
|
21
|
+
| `@lexical/rich-text` | `^0.35.0` |
|
|
22
|
+
| `@lexical/code` | `^0.35.0` |
|
|
23
|
+
| `@lexical/list` | `^0.35.0` |
|
|
24
|
+
| `@lexical/link` | `^0.35.0` |
|
|
25
|
+
| `@zuilib/core` | workspace / published — import core styles |
|
|
4
26
|
|
|
5
27
|
## Installation
|
|
6
28
|
|
|
7
29
|
```bash
|
|
8
|
-
pnpm add @zuilib/text-editor @zuilib/core
|
|
30
|
+
pnpm add @zuilib/text-editor @zuilib/core \
|
|
31
|
+
lexical @lexical/react @lexical/markdown @lexical/rich-text \
|
|
32
|
+
@lexical/code @lexical/list @lexical/link
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Monorepo:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@zuilib/text-editor": "workspace:*",
|
|
41
|
+
"@zuilib/core": "workspace:*",
|
|
42
|
+
"lexical": "^0.35.0",
|
|
43
|
+
"@lexical/react": "^0.35.0",
|
|
44
|
+
"@lexical/markdown": "^0.35.0",
|
|
45
|
+
"@lexical/rich-text": "^0.35.0",
|
|
46
|
+
"@lexical/code": "^0.35.0",
|
|
47
|
+
"@lexical/list": "^0.35.0",
|
|
48
|
+
"@lexical/link": "^0.35.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
9
51
|
```
|
|
10
52
|
|
|
11
|
-
##
|
|
53
|
+
## Import rules
|
|
54
|
+
|
|
55
|
+
| Rule | Detail |
|
|
56
|
+
|------|--------|
|
|
57
|
+
| **Named export** | `import { MarkdownEditor } from '@zuilib/text-editor'` |
|
|
58
|
+
| **Types** | `import type { MarkdownEditorProps } from '@zuilib/text-editor'` |
|
|
59
|
+
| **Styles** | `import '@zuilib/text-editor/styles.css'` and `@zuilib/core/styles.css` |
|
|
60
|
+
| **Controlled** | Always pass `value` + `onChange` for persisted content |
|
|
61
|
+
| **Mode switches** | Switching `edit-raw` → `edit-md` re-mounts Lexical with latest markdown (cursor-safe) |
|
|
62
|
+
|
|
63
|
+
## Exports
|
|
64
|
+
|
|
65
|
+
| Subpath | Exports |
|
|
66
|
+
|---------|---------|
|
|
67
|
+
| `@zuilib/text-editor` | `MarkdownEditor` (default re-exported as named), `MarkdownEditorProps` |
|
|
68
|
+
| `@zuilib/text-editor/styles.css` | Editor chrome + Lexical theme classes |
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
12
71
|
|
|
13
72
|
```tsx
|
|
14
73
|
import { useState } from 'react'
|
|
15
|
-
import
|
|
74
|
+
import '@zuilib/core/styles.css'
|
|
16
75
|
import '@zuilib/text-editor/styles.css'
|
|
76
|
+
import { MarkdownEditor } from '@zuilib/text-editor'
|
|
17
77
|
|
|
18
78
|
function Notes() {
|
|
19
|
-
const [value, setValue] = useState('# Hello\n\n- [ ] Try
|
|
20
|
-
|
|
21
|
-
return
|
|
79
|
+
const [value, setValue] = useState('# Hello\n\n- [ ] Try checklists')
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<MarkdownEditor
|
|
83
|
+
value={value}
|
|
84
|
+
onChange={setValue}
|
|
85
|
+
placeholder="Start writing…"
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
22
88
|
}
|
|
23
89
|
```
|
|
24
90
|
|
|
25
91
|
## Modes
|
|
26
92
|
|
|
93
|
+
| `mode` | UI | `onChange` |
|
|
94
|
+
|--------|-----|------------|
|
|
95
|
+
| `'edit-md'` (default) | Lexical rich editor + markdown shortcuts | Emits markdown string |
|
|
96
|
+
| `'edit-raw'` | Plain `<textarea>` with markdown source | Emits markdown string |
|
|
97
|
+
| `'view'` | Read-only rendered content | No editing (`onChange` unused) |
|
|
98
|
+
|
|
27
99
|
```tsx
|
|
28
|
-
|
|
29
|
-
<MarkdownEditor mode="edit-
|
|
100
|
+
<MarkdownEditor mode="edit-md" value={md} onChange={setMd} />
|
|
101
|
+
<MarkdownEditor mode="edit-raw" value={md} onChange={setMd} />
|
|
102
|
+
<MarkdownEditor mode="view" value={md} />
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Toggle modes in parent state; the editor preserves content via an internal ref when remounting Lexical after raw editing.
|
|
106
|
+
|
|
107
|
+
## Props (`MarkdownEditorProps`)
|
|
108
|
+
|
|
109
|
+
| Prop | Type | Default | Description |
|
|
110
|
+
|------|------|---------|-------------|
|
|
111
|
+
| `value` | `string` | `''` | Markdown source (controlled) |
|
|
112
|
+
| `onChange` | `(value: string) => void` | — | Called on every edit |
|
|
113
|
+
| `mode` | `'edit-md' \| 'edit-raw' \| 'view'` | `'edit-md'` | Editing surface |
|
|
114
|
+
| `placeholder` | `string` | `'Start writing...'` | Empty state hint |
|
|
115
|
+
| `readOnly` | `boolean` | `false` | Disables editing in Lexical modes |
|
|
116
|
+
| `autoFocus` | `boolean` | `false` | Focus on mount |
|
|
117
|
+
| `className` | `string` | — | Root wrapper class |
|
|
118
|
+
|
|
119
|
+
## Markdown features
|
|
120
|
+
|
|
121
|
+
**Shortcuts (edit-md):** headings (`#`), lists, blockquote, links, fenced code (via `CodeBlockShortcutPlugin`), checklists (`- [ ]` via `ChecklistShortcutPlugin`).
|
|
30
122
|
|
|
31
|
-
|
|
32
|
-
<MarkdownEditor mode="edit-raw" value={value} onChange={setValue} />
|
|
123
|
+
**Built-in plugins:** history (undo/redo), lists, checklists, links, markdown sync (`MarkdownSyncPlugin`), code highlighting (`CodeHighlightPlugin`).
|
|
33
124
|
|
|
34
|
-
|
|
35
|
-
|
|
125
|
+
**Not included:** file uploads, collaborative editing, or custom Lexical node registration — extend by forking or wrapping `MarkdownEditor`.
|
|
126
|
+
|
|
127
|
+
## Form integration
|
|
128
|
+
|
|
129
|
+
With `@zuilib/form`:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<FormField control={form.control} name="body">
|
|
133
|
+
{({ field }) => (
|
|
134
|
+
<MarkdownEditor
|
|
135
|
+
value={field.value}
|
|
136
|
+
onChange={field.onChange}
|
|
137
|
+
mode="edit-md"
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</FormField>
|
|
36
141
|
```
|
|
37
142
|
|
|
38
|
-
|
|
143
|
+
Ensure both CSS entry points are loaded in the app layout.
|
|
39
144
|
|
|
40
|
-
|
|
41
|
-
- `onChange`: `(value: string) => void`
|
|
42
|
-
- `mode`: `'edit-md' | 'edit-raw' | 'view'` (default `'edit-md'`)
|
|
43
|
-
- `placeholder`: `string` (default `'Start writing...'`)
|
|
44
|
-
- `readOnly`: `boolean`
|
|
45
|
-
- `autoFocus`: `boolean`
|
|
46
|
-
- `className`: `string`
|
|
145
|
+
## Architecture notes (for extenders)
|
|
47
146
|
|
|
48
|
-
|
|
147
|
+
- Source: `src/MarkdownEditor.tsx`, plugins under `src/plugins/`
|
|
148
|
+
- Lexical namespace: `ZuiTextEditor`
|
|
149
|
+
- Markdown import/export uses `@lexical/markdown` transformers (`CHECK_LIST` prioritized for import)
|
|
150
|
+
- `edit-raw` bypasses Lexical; switching back to `edit-md` captures `latestValueRef` and bumps `mountKey` to avoid cursor jumps
|
|
49
151
|
|
|
50
|
-
|
|
51
|
-
- Syntax-highlighted fenced code blocks
|
|
52
|
-
- Undo/redo history
|
|
53
|
-
- Cursor-stable mode switching between raw and rich editing
|
|
152
|
+
## Related packages
|
|
54
153
|
|
|
55
|
-
|
|
154
|
+
- `@zuilib/core` — design tokens and base styles
|
|
155
|
+
- `@zuilib/form` — react-hook-form wiring
|
|
56
156
|
|
|
57
|
-
|
|
157
|
+
## Build (maintainers)
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pnpm --filter @zuilib/text-editor build
|
|
161
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ElementNode, NodeKey, EditorConfig, SerializedElementNode, LexicalNode } from 'lexical';
|
|
3
|
+
import { MultilineElementTransformer } from '@lexical/markdown';
|
|
2
4
|
|
|
3
5
|
type Props = Readonly<{
|
|
4
6
|
value?: string;
|
|
@@ -11,4 +13,45 @@ type Props = Readonly<{
|
|
|
11
13
|
}>;
|
|
12
14
|
declare function MarkdownEditor({ value, onChange, placeholder, readOnly, className, mode, autoFocus, }: Props): react_jsx_runtime.JSX.Element;
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
type SerializedFrontmatterNode = SerializedElementNode;
|
|
17
|
+
/**
|
|
18
|
+
* A block node holding a document's YAML frontmatter — the metadata section
|
|
19
|
+
* delimited by `---` lines at the very top of a markdown file (as used by
|
|
20
|
+
* Claude skills, static-site generators, etc.).
|
|
21
|
+
*
|
|
22
|
+
* Content is stored as plain TextNode children separated by LineBreakNodes, so
|
|
23
|
+
* `getTextContent()` yields the raw YAML with `\n` line separators — exactly
|
|
24
|
+
* what the FRONTMATTER transformer re-serializes on export.
|
|
25
|
+
*/
|
|
26
|
+
declare class FrontmatterNode extends ElementNode {
|
|
27
|
+
static getType(): string;
|
|
28
|
+
static clone(node: FrontmatterNode): FrontmatterNode;
|
|
29
|
+
constructor(key?: NodeKey);
|
|
30
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
31
|
+
updateDOM(): boolean;
|
|
32
|
+
static importJSON(_serializedNode: SerializedFrontmatterNode): FrontmatterNode;
|
|
33
|
+
exportJSON(): SerializedFrontmatterNode;
|
|
34
|
+
isInline(): false;
|
|
35
|
+
canBeEmpty(): boolean;
|
|
36
|
+
canIndent(): false;
|
|
37
|
+
}
|
|
38
|
+
declare function $createFrontmatterNode(): FrontmatterNode;
|
|
39
|
+
declare function $isFrontmatterNode(node: LexicalNode | null | undefined): node is FrontmatterNode;
|
|
40
|
+
/**
|
|
41
|
+
* Markdown transformer for a leading YAML frontmatter block:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* ---
|
|
45
|
+
* name: my-skill
|
|
46
|
+
* description: ...
|
|
47
|
+
* ---
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Import only claims the block when it's the very first thing in the document
|
|
51
|
+
* (frontmatter is defined as leading metadata); a `---` fence appearing later
|
|
52
|
+
* falls through to normal handling. Must be ordered before other multiline
|
|
53
|
+
* transformers so its `export` runs first for a FrontmatterNode.
|
|
54
|
+
*/
|
|
55
|
+
declare const FRONTMATTER: MultilineElementTransformer;
|
|
56
|
+
|
|
57
|
+
export { $createFrontmatterNode, $isFrontmatterNode, FRONTMATTER, FrontmatterNode, MarkdownEditor, type Props as MarkdownEditorProps, type SerializedFrontmatterNode };
|
package/dist/index.js
CHANGED
|
@@ -192,6 +192,86 @@ function MarkdownSyncPlugin({
|
|
|
192
192
|
return null;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// src/nodes/FrontmatterNode.ts
|
|
196
|
+
import {
|
|
197
|
+
$applyNodeReplacement,
|
|
198
|
+
$createLineBreakNode,
|
|
199
|
+
$createTextNode,
|
|
200
|
+
ElementNode
|
|
201
|
+
} from "lexical";
|
|
202
|
+
var FrontmatterNode = class _FrontmatterNode extends ElementNode {
|
|
203
|
+
static getType() {
|
|
204
|
+
return "frontmatter";
|
|
205
|
+
}
|
|
206
|
+
static clone(node) {
|
|
207
|
+
return new _FrontmatterNode(node.__key);
|
|
208
|
+
}
|
|
209
|
+
constructor(key) {
|
|
210
|
+
super(key);
|
|
211
|
+
}
|
|
212
|
+
createDOM(config) {
|
|
213
|
+
const dom = document.createElement("div");
|
|
214
|
+
const className = config.theme.frontmatter;
|
|
215
|
+
dom.className = typeof className === "string" ? className : "zui-frontmatter";
|
|
216
|
+
return dom;
|
|
217
|
+
}
|
|
218
|
+
updateDOM() {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
static importJSON(_serializedNode) {
|
|
222
|
+
return $createFrontmatterNode();
|
|
223
|
+
}
|
|
224
|
+
exportJSON() {
|
|
225
|
+
return {
|
|
226
|
+
...super.exportJSON(),
|
|
227
|
+
type: "frontmatter",
|
|
228
|
+
version: 1
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
isInline() {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
canBeEmpty() {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
canIndent() {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
function $createFrontmatterNode() {
|
|
242
|
+
return $applyNodeReplacement(new FrontmatterNode());
|
|
243
|
+
}
|
|
244
|
+
function $isFrontmatterNode(node) {
|
|
245
|
+
return node instanceof FrontmatterNode;
|
|
246
|
+
}
|
|
247
|
+
var FRONTMATTER_DELIMITER = /^---\s*$/;
|
|
248
|
+
var FRONTMATTER = {
|
|
249
|
+
dependencies: [FrontmatterNode],
|
|
250
|
+
export: (node) => {
|
|
251
|
+
if (!$isFrontmatterNode(node)) return null;
|
|
252
|
+
const body = node.getTextContent();
|
|
253
|
+
return body ? `---
|
|
254
|
+
${body}
|
|
255
|
+
---` : "---\n---";
|
|
256
|
+
},
|
|
257
|
+
regExpStart: FRONTMATTER_DELIMITER,
|
|
258
|
+
regExpEnd: FRONTMATTER_DELIMITER,
|
|
259
|
+
replace: (rootNode, children, _startMatch, _endMatch, linesInBetween, isImport) => {
|
|
260
|
+
if (!isImport || children != null || linesInBetween == null) return false;
|
|
261
|
+
if (rootNode.getChildrenSize() !== 0) return false;
|
|
262
|
+
const lines = [...linesInBetween];
|
|
263
|
+
while (lines.length > 0 && lines[0].length === 0) lines.shift();
|
|
264
|
+
while (lines.length > 0 && lines[lines.length - 1].length === 0) lines.pop();
|
|
265
|
+
const node = $createFrontmatterNode();
|
|
266
|
+
lines.forEach((line, index) => {
|
|
267
|
+
if (index > 0) node.append($createLineBreakNode());
|
|
268
|
+
node.append($createTextNode(line));
|
|
269
|
+
});
|
|
270
|
+
rootNode.append(node);
|
|
271
|
+
},
|
|
272
|
+
type: "multiline-element"
|
|
273
|
+
};
|
|
274
|
+
|
|
195
275
|
// src/theme.ts
|
|
196
276
|
var editorTheme = {
|
|
197
277
|
ltr: "text-left",
|
|
@@ -261,12 +341,13 @@ var editorTheme = {
|
|
|
261
341
|
url: "text-blue-500",
|
|
262
342
|
variable: "text-blue-500"
|
|
263
343
|
},
|
|
264
|
-
quote: "border-l-4 border-border pl-4 italic"
|
|
344
|
+
quote: "border-l-4 border-border pl-4 italic",
|
|
345
|
+
frontmatter: "zui-frontmatter"
|
|
265
346
|
};
|
|
266
347
|
|
|
267
348
|
// src/MarkdownEditor.tsx
|
|
268
349
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
269
|
-
var SYNC_TRANSFORMERS = [CHECK_LIST, ...TRANSFORMERS];
|
|
350
|
+
var SYNC_TRANSFORMERS = [FRONTMATTER, CHECK_LIST, ...TRANSFORMERS];
|
|
270
351
|
var SHORTCUT_TRANSFORMERS = TRANSFORMERS.filter((t) => t !== CODE);
|
|
271
352
|
function onError(error) {
|
|
272
353
|
console.error(error);
|
|
@@ -279,7 +360,8 @@ var editorNodes = [
|
|
|
279
360
|
CodeNode,
|
|
280
361
|
CodeHighlightNode,
|
|
281
362
|
AutoLinkNode,
|
|
282
|
-
LinkNode
|
|
363
|
+
LinkNode,
|
|
364
|
+
FrontmatterNode
|
|
283
365
|
];
|
|
284
366
|
function ReadOnlyPlugin({ readOnly }) {
|
|
285
367
|
const [editor] = useLexicalComposerContext5();
|
|
@@ -387,5 +469,9 @@ function MarkdownEditor({
|
|
|
387
469
|
] }) }, mountKey);
|
|
388
470
|
}
|
|
389
471
|
export {
|
|
472
|
+
$createFrontmatterNode,
|
|
473
|
+
$isFrontmatterNode,
|
|
474
|
+
FRONTMATTER,
|
|
475
|
+
FrontmatterNode,
|
|
390
476
|
MarkdownEditor
|
|
391
477
|
};
|
package/dist/styles.css
CHANGED
|
@@ -64,6 +64,22 @@
|
|
|
64
64
|
transform: rotate(45deg);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Frontmatter (YAML metadata block delimited by --- at the top of a doc) */
|
|
68
|
+
.zui-frontmatter {
|
|
69
|
+
display: block;
|
|
70
|
+
white-space: pre-wrap;
|
|
71
|
+
margin: 0 0 1rem;
|
|
72
|
+
padding: 0.75rem 1rem;
|
|
73
|
+
border: 1px solid currentColor;
|
|
74
|
+
border-radius: 0.375rem;
|
|
75
|
+
border-left-width: 3px;
|
|
76
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
|
|
77
|
+
'Liberation Mono', monospace;
|
|
78
|
+
font-size: 0.8125rem;
|
|
79
|
+
line-height: 1.6;
|
|
80
|
+
opacity: 0.7;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
.zui-text-editor-textarea {
|
|
68
84
|
width: 100%;
|
|
69
85
|
height: 100%;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuilib/text-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "ZUI — A markdown editor component wrapping Lexical",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
"react",
|
|
55
55
|
"typescript"
|
|
56
56
|
]
|
|
57
|
-
}
|
|
57
|
+
}
|