@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 CHANGED
@@ -1,57 +1,161 @@
1
1
  # @zuilib/text-editor
2
2
 
3
- Markdown editor for ZUI Design System — a React component built on [Lexical](https://lexical.dev/) with markdown shortcuts, code highlighting, checklists, and raw/rich/view modes.
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 lexical @lexical/react @lexical/markdown @lexical/rich-text @lexical/code @lexical/list @lexical/link
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
- ## Usage
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 { MarkdownEditor } from '@zuilib/text-editor'
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 me')
20
-
21
- return <MarkdownEditor value={value} onChange={setValue} />
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
- // Rich markdown editing (default) — shortcuts like `#`, `- [ ]`, ``` `
29
- <MarkdownEditor mode="edit-md" value={value} onChange={setValue} />
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
- // Raw markdown in a plain textarea
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
- // Read-only rendered view
35
- <MarkdownEditor mode="view" value={value} />
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
- ## Props
143
+ Ensure both CSS entry points are loaded in the app layout.
39
144
 
40
- - `value`: `string` markdown source
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
- ## Features
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
- - Markdown shortcuts: headings, lists, checklists, code blocks, quotes, links
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
- ## License
154
+ - `@zuilib/core` — design tokens and base styles
155
+ - `@zuilib/form` — react-hook-form wiring
56
156
 
57
- MIT
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
- export { MarkdownEditor, type Props as MarkdownEditorProps };
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.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
+ }