@zuilib/text-editor 0.2.0 → 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog — @zuilib/text-editor
2
+
3
+ ## 0.3.0
4
+
5
+ - **Custom toolbars**
6
+ - `toolbar` prop accepts a render function receiving the default groups
7
+ (`format`, `insert`, `history`) so you can add your own buttons
8
+ - Compound components: `MarkdownEditor.Root` / `.Content` / `.Toolbar` /
9
+ `.Outline` plus `.FormatButtons` / `.InsertButtons` / `.HistoryButtons` /
10
+ `.ToolbarButton` / `.ToolbarDivider` — place the toolbar anywhere under
11
+ `Root`
12
+ - `useMarkdownEditor()` headless hook: `editor`, `activeFormats`,
13
+ `toggleFormat`, `insertTable`, `insertDrawing`, `undo`/`redo`
14
+ - Undo/redo buttons (`HistoryButtons`; in the default toolbar via the
15
+ function form only — default toolbar layout unchanged)
16
+ - Internal: `LexicalComposer` is now mounted in `edit-raw` mode too, so
17
+ hooks keep working across mode switches
18
+
19
+ ## 0.2.1
20
+
21
+ - `DRAWING_FORMAT.md`: authoritative spec of the ```drawing JSON payload,
22
+ written for programmatic/LLM consumption; ships in the npm package
23
+ - New export `DRAWING_DATA_JSON_SCHEMA` (JSON Schema draft-07) for
24
+ validating or structured-output-generating drawing payloads
25
+ - New exports `parseDrawingData` / `serializeDrawingData`
26
+ - This changelog; README overhaul
27
+
28
+ ## 0.2.0
29
+
30
+ - **Outline** (`outline` prop, default off): docked, collapsible
31
+ table-of-contents sidebar — live heading list, click-to-scroll, current
32
+ section highlighted
33
+ - **Section folding** (`foldable` prop, default on): collapse everything
34
+ under a heading via a gutter chevron; view-layer only (markdown
35
+ untouched); auto-expands if the cursor enters a folded section
36
+
37
+ ## 0.1.0
38
+
39
+ - **Tables**: GFM pipe tables round-trip through markdown; artifact-style
40
+ rendering; in-place editing via Lexical `TablePlugin`; new peer deps
41
+ `@lexical/table` and `@lexical/utils`
42
+ - **Toolbar** (`toolbar` prop, default on): inline formatting, insert
43
+ table, insert drawing
44
+ - **Drawing canvas**: Excalidraw-style diagrams embedded as ```drawing
45
+ fenced JSON blocks — see `DRAWING_FORMAT.md`
46
+ - Shapes: rectangle, ellipse, triangle, pentagon, arrow, line, text
47
+ - Boxes are cards with three attached text slots (label / content /
48
+ footer) that move, resize, and wrap with the shape
49
+ - Connectors bind to boxes and track them; one-way or two-way arrows;
50
+ midpoint labels; straight, elbow, or waypoint-routed paths
51
+ - Stroke/fill palettes; dark mode via canvas color inversion
52
+ - Round-trip test suite (`pnpm test`)
53
+
54
+ ## 0.0.2 and earlier
55
+
56
+ - Markdown editor wrapping Lexical: `edit-md` / `edit-raw` / `view` modes,
57
+ markdown shortcuts, checklists, fenced code highlighting, YAML
58
+ frontmatter block, controlled `value`/`onChange`
@@ -0,0 +1,149 @@
1
+ # The `drawing` block format
2
+
3
+ `@zuilib/text-editor` documents are plain markdown. Diagrams are embedded as
4
+ a fenced code block with the language `drawing`, containing a single JSON
5
+ object:
6
+
7
+ ````md
8
+ ```drawing
9
+ {"version":1,"height":320,"shapes":[ ... ]}
10
+ ```
11
+ ````
12
+
13
+ This document is the authoritative spec of that JSON payload. It is written
14
+ so it can be handed to a program or an LLM that needs to **generate or edit
15
+ drawings**. A machine-readable JSON Schema of the same rules is exported
16
+ from the package as `DRAWING_DATA_JSON_SCHEMA`.
17
+
18
+ Malformed payloads never crash the editor: anything that fails validation
19
+ degrades to an empty canvas (invalid shapes are dropped individually).
20
+
21
+ ## Coordinate system
22
+
23
+ - Origin `(0,0)` is the canvas's top-left; x grows right, y grows down.
24
+ - Units are CSS pixels.
25
+ - The canvas is as wide as the editor (fluid). Keep shapes within roughly
26
+ x ∈ [0, 700] to be safe on typical layouts.
27
+ - `height` is the canvas height in pixels (minimum 80; 300–400 is typical).
28
+
29
+ ## Top-level object
30
+
31
+ | Field | Type | Notes |
32
+ |-----------|-----------|-----------------------------------------|
33
+ | `version` | `1` | Literal `1` |
34
+ | `height` | `number` | Canvas height in px |
35
+ | `shapes` | `Shape[]` | Render order: later shapes draw on top |
36
+
37
+ ## Shape object
38
+
39
+ All shapes share these required fields:
40
+
41
+ | Field | Type | Meaning |
42
+ |---------------|----------|--------------------------------------------------------------------|
43
+ | `id` | `string` | Unique within the drawing; bindings reference it |
44
+ | `type` | `string` | `rect` \| `ellipse` \| `triangle` \| `pentagon` \| `arrow` \| `line` \| `text` |
45
+ | `x`, `y` | `number` | Boxes/text: top-left of the bounding box. Connectors: start point. |
46
+ | `w`, `h` | `number` | Boxes/text: size (non-negative). Connectors: delta to the end point (`end = (x+w, y+h)`; may be negative). |
47
+ | `stroke` | `string` | CSS color of the outline and of all text on the shape |
48
+ | `fill` | `string` | CSS color of the interior; `"transparent"` for none |
49
+ | `strokeWidth` | `number` | Use `2` |
50
+
51
+ ### Boxes (`rect`, `ellipse`, `triangle`, `pentagon`) — "cards"
52
+
53
+ Boxes are card-like: they can carry up to three text slots that render
54
+ inside the shape and move/resize/wrap with it:
55
+
56
+ | Field | Renders as |
57
+ |----------|-----------------------------------------------|
58
+ | `label` | Small bold heading at the top of the box |
59
+ | `text` | Main content, centered |
60
+ | `footer` | Small dim line at the bottom |
61
+
62
+ Text wraps automatically to the box width; `\n` forces a line break.
63
+ Triangles are apex-up isoceles; pentagons are apex-up, inscribed in the
64
+ bounding box. Prefer these slots over floating `text` shapes — slot text is
65
+ attached to the card.
66
+
67
+ ### `text` — free-floating annotation
68
+
69
+ `text` holds the content. `w`/`h` are advisory (the editor recomputes them
70
+ from the content); position is the top-left of the first line. Use only for
71
+ annotations that belong to no box (the editor deletes a text shape when its
72
+ content is emptied).
73
+
74
+ ### Connectors (`arrow`, `line`)
75
+
76
+ Geometry: from `(x, y)` to `(x+w, y+h)`. `arrow` has a head at the end;
77
+ `line` has none.
78
+
79
+ | Field | Type | Meaning |
80
+ |-----------------|------------|-------------------------------------------------------------------------|
81
+ | `text` | `string` | Label rendered at the path midpoint on a small backing plate |
82
+ | `bidirectional` | `boolean` | Arrows only: heads on both ends |
83
+ | `startBinding` | `string` | id of the box the start attaches to |
84
+ | `endBinding` | `string` | id of the box the end attaches to |
85
+ | `routing` | `"elbow"` | Orthogonal auto-routing with up to two right-angle bends |
86
+ | `elbow` | `number` | 0–1: middle-segment position along the span (default 0.5) |
87
+ | `waypoints` | `Point[]` | Explicit intermediate path points (`{x,y}`), ordered start→end |
88
+
89
+ **Bindings.** When `startBinding`/`endBinding` name a box id, the editor
90
+ re-anchors that endpoint onto the box's border (aimed at the other end, with
91
+ a small gap) and keeps it attached as the box moves. Generated endpoints
92
+ therefore only need to be approximately right — place them near the intended
93
+ boxes and the resolver snaps them. Don't bind both ends of one connector to
94
+ the same box. Bindings to ids that don't exist are silently dropped.
95
+
96
+ **Routing.** Omit `routing` for a straight (possibly diagonal) connector.
97
+ `"elbow"` renders an orthogonal path: it runs along the dominant axis, turns
98
+ through a perpendicular middle segment (positioned by `elbow`), and turns
99
+ again. If `waypoints` is non-empty it defines the path (`start → waypoints…
100
+ → end`) and `routing`/`elbow` are ignored. Waypoints are absolute canvas
101
+ coordinates; align consecutive points on x or y to get right angles.
102
+
103
+ ## Recommended colors
104
+
105
+ Any CSS color works; the editor's palette (dark-mode safe — the canvas
106
+ inverts colors Excalidraw-style in dark themes):
107
+
108
+ - Strokes: `#1e1e1e` (default), `#e03131` red, `#2f9e44` green, `#1971c2` blue, `#f08c00` orange
109
+ - Fills: `transparent`, `#ffc9c9`, `#b2f2bb`, `#a5d8ff`, `#ffec99`
110
+
111
+ Pair a colored stroke with its pastel fill (`#1971c2` + `#a5d8ff`, etc.).
112
+
113
+ ## Examples
114
+
115
+ Two bound cards with a labeled arrow:
116
+
117
+ ````md
118
+ ```drawing
119
+ {"version":1,"height":260,"shapes":[
120
+ {"id":"web","type":"rect","x":40,"y":70,"w":170,"h":100,"stroke":"#1971c2","fill":"#a5d8ff","strokeWidth":2,"label":"CLIENT","text":"Web App","footer":"React"},
121
+ {"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API","footer":"Kotlin"},
122
+ {"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"web","endBinding":"api","text":"REST"}
123
+ ]}
124
+ ```
125
+ ````
126
+
127
+ An elbow arrow and a hand-routed multi-bend line:
128
+
129
+ ````md
130
+ ```drawing
131
+ {"version":1,"height":300,"shapes":[
132
+ {"id":"a","type":"rect","x":40,"y":40,"w":150,"h":80,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"text":"A"},
133
+ {"id":"b","type":"ellipse","x":420,"y":180,"w":150,"h":80,"stroke":"#e03131","fill":"#ffc9c9","strokeWidth":2,"text":"B"},
134
+ {"id":"e1","type":"arrow","x":196,"y":80,"w":224,"h":140,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"a","endBinding":"b","routing":"elbow"},
135
+ {"id":"e2","type":"line","x":115,"y":126,"w":380,"h":54,"stroke":"#1971c2","fill":"transparent","strokeWidth":2,"startBinding":"a","endBinding":"b","waypoints":[{"x":115,"y":260},{"x":495,"y":260}]}
136
+ ]}
137
+ ```
138
+ ````
139
+
140
+ ## Generation checklist
141
+
142
+ 1. One JSON object, single ```drawing fence, no comments or trailing commas.
143
+ 2. Every `id` unique; every binding references an existing box id.
144
+ 3. Boxes ≥ 120×70 when they carry text slots; leave ~60px between cards for
145
+ connectors and labels.
146
+ 4. Box text goes in `label`/`text`/`footer` — not in floating `text` shapes.
147
+ 5. Connectors between cards should bind both ends; approximate endpoints are
148
+ fine (the editor snaps them to borders).
149
+ 6. `height` large enough to contain every shape plus ~20px margin.
package/README.md CHANGED
@@ -1,30 +1,30 @@
1
1
  # @zuilib/text-editor
2
2
 
3
- Markdown editor for ZUI built on [Lexical](https://lexical.dev/): rich editing with shortcuts, raw markdown mode, read-only view, checklists, fenced code highlighting, GFM tables, and an embedded drawing canvas for diagrams.
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
- | `@lexical/table` | `^0.35.0` |
26
- | `@lexical/utils` | `^0.35.0` |
27
- | `@zuilib/core` | workspace / published — import core styles |
3
+ A markdown editor for ZUI built on [Lexical](https://lexical.dev/). Plain
4
+ markdown in, plain markdown out — with an artifact-style editing experience
5
+ on top: rich tables, an embedded Excalidraw-style diagram canvas, a
6
+ document outline, and collapsible sections.
7
+
8
+ **Features**
9
+
10
+ - **Three modes**: rich WYSIWYG (`edit-md`), raw markdown (`edit-raw`),
11
+ read-only render (`view`) — all driven by one controlled `value` string
12
+ - **Markdown shortcuts** while typing: headings, lists, checklists,
13
+ blockquotes, links, fenced code (with syntax highlighting), tables
14
+ - **Tables**: GFM pipe tables, edited in place (tab between cells, ranges,
15
+ inline formatting), styled like Claude artifacts
16
+ - **Diagrams**: a drawing canvas embedded in the document — cards with
17
+ attached text slots, bound arrows that follow their cards, elbow and
18
+ multi-waypoint routing, labels, color palettes, dark mode
19
+ - **Outline**: optional table-of-contents sidebar with click-to-scroll and
20
+ current-section highlight
21
+ - **Section folding**: collapse everything under a heading, view-layer only
22
+ - **Toolbar**: inline formatting plus insert-table / insert-drawing
23
+ - YAML **frontmatter** block support (`---` at the top)
24
+
25
+ Everything round-trips through the markdown string: tables as GFM,
26
+ diagrams as ` ```drawing ` fenced JSON ([format spec](./DRAWING_FORMAT.md)),
27
+ frontmatter as `---` blocks. Feature history: [CHANGELOG](./CHANGELOG.md).
28
28
 
29
29
  ## Installation
30
30
 
@@ -34,42 +34,8 @@ pnpm add @zuilib/text-editor @zuilib/core \
34
34
  @lexical/code @lexical/list @lexical/link @lexical/table @lexical/utils
35
35
  ```
36
36
 
37
- Monorepo:
38
-
39
- ```json
40
- {
41
- "dependencies": {
42
- "@zuilib/text-editor": "workspace:*",
43
- "@zuilib/core": "workspace:*",
44
- "lexical": "^0.35.0",
45
- "@lexical/react": "^0.35.0",
46
- "@lexical/markdown": "^0.35.0",
47
- "@lexical/rich-text": "^0.35.0",
48
- "@lexical/code": "^0.35.0",
49
- "@lexical/list": "^0.35.0",
50
- "@lexical/link": "^0.35.0",
51
- "@lexical/table": "^0.35.0",
52
- "@lexical/utils": "^0.35.0"
53
- }
54
- }
55
- ```
56
-
57
- ## Import rules
58
-
59
- | Rule | Detail |
60
- |------|--------|
61
- | **Named export** | `import { MarkdownEditor } from '@zuilib/text-editor'` |
62
- | **Types** | `import type { MarkdownEditorProps } from '@zuilib/text-editor'` |
63
- | **Styles** | `import '@zuilib/text-editor/styles.css'` and `@zuilib/core/styles.css` |
64
- | **Controlled** | Always pass `value` + `onChange` for persisted content |
65
- | **Mode switches** | Switching `edit-raw` → `edit-md` re-mounts Lexical with latest markdown (cursor-safe) |
66
-
67
- ## Exports
68
-
69
- | Subpath | Exports |
70
- |---------|---------|
71
- | `@zuilib/text-editor` | `MarkdownEditor` (default re-exported as named), `MarkdownEditorProps` |
72
- | `@zuilib/text-editor/styles.css` | Editor chrome + Lexical theme classes |
37
+ All `lexical`/`@lexical/*` packages are peer dependencies at `^0.35.0`;
38
+ `react`/`react-dom` at `^18 || ^19`.
73
39
 
74
40
  ## Quick start
75
41
 
@@ -81,32 +47,12 @@ import { MarkdownEditor } from '@zuilib/text-editor'
81
47
 
82
48
  function Notes() {
83
49
  const [value, setValue] = useState('# Hello\n\n- [ ] Try checklists')
84
-
85
- return (
86
- <MarkdownEditor
87
- value={value}
88
- onChange={setValue}
89
- placeholder="Start writing…"
90
- />
91
- )
50
+ return <MarkdownEditor value={value} onChange={setValue} outline />
92
51
  }
93
52
  ```
94
53
 
95
- ## Modes
96
-
97
- | `mode` | UI | `onChange` |
98
- |--------|-----|------------|
99
- | `'edit-md'` (default) | Lexical rich editor + markdown shortcuts | Emits markdown string |
100
- | `'edit-raw'` | Plain `<textarea>` with markdown source | Emits markdown string |
101
- | `'view'` | Read-only rendered content | No editing (`onChange` unused) |
102
-
103
- ```tsx
104
- <MarkdownEditor mode="edit-md" value={md} onChange={setMd} />
105
- <MarkdownEditor mode="edit-raw" value={md} onChange={setMd} />
106
- <MarkdownEditor mode="view" value={md} />
107
- ```
108
-
109
- Toggle modes in parent state; the editor preserves content via an internal ref when remounting Lexical after raw editing.
54
+ The component is controlled: pass `value` and persist what `onChange`
55
+ emits. The emitted string is always plain markdown.
110
56
 
111
57
  ## Props (`MarkdownEditorProps`)
112
58
 
@@ -115,25 +61,29 @@ Toggle modes in parent state; the editor preserves content via an internal ref w
115
61
  | `value` | `string` | `''` | Markdown source (controlled) |
116
62
  | `onChange` | `(value: string) => void` | — | Called on every edit |
117
63
  | `mode` | `'edit-md' \| 'edit-raw' \| 'view'` | `'edit-md'` | Editing surface |
118
- | `placeholder` | `string` | `'Start writing...'` | Empty state hint |
64
+ | `placeholder` | `string` | `'Start writing...'` | Empty-state hint |
119
65
  | `readOnly` | `boolean` | `false` | Disables editing in Lexical modes |
120
66
  | `autoFocus` | `boolean` | `false` | Focus on mount |
121
67
  | `className` | `string` | — | Root wrapper class |
122
- | `toolbar` | `boolean` | `true` | Formatting/insert toolbar in `edit-md` mode |
123
- | `outline` | `boolean` | `false` | Table-of-contents sidebar listing headings |
124
- | `foldable` | `boolean` | `true` | Collapse sections under their headings |
125
-
126
- ## Markdown features
68
+ | `toolbar` | `boolean \| (items) => ReactNode` | `true` | Formatting/insert toolbar (`edit-md`); function form customises it |
69
+ | `outline` | `boolean` | `false` | Table-of-contents sidebar |
70
+ | `foldable` | `boolean` | `true` | Collapse sections under headings |
127
71
 
128
- **Shortcuts (edit-md):** headings (`#`), lists, blockquote, links, fenced code (via `CodeBlockShortcutPlugin`), checklists (`- [ ]` via `ChecklistShortcutPlugin`), tables (`| a | b |`).
72
+ ### Modes
129
73
 
130
- **Built-in plugins:** history (undo/redo), lists, checklists, links, tables (`TablePlugin`), markdown sync (`MarkdownSyncPlugin`), code highlighting (`CodeHighlightPlugin`), toolbar (`ToolbarPlugin`).
74
+ | `mode` | UI | Notes |
75
+ |--------|-----|-------|
76
+ | `'edit-md'` | Lexical rich editor | Shortcuts, toolbar, tables, canvas |
77
+ | `'edit-raw'` | Plain `<textarea>` | Direct markdown source editing |
78
+ | `'view'` | Read-only render | Outline/folding still work |
131
79
 
132
- **Not included:** file uploads, collaborative editing, or custom Lexical node registration extend by forking or wrapping `MarkdownEditor`.
80
+ Switching `edit-raw` `edit-md` re-mounts Lexical with the latest text
81
+ (cursor-safe).
133
82
 
134
83
  ## Tables
135
84
 
136
- GFM pipe tables round-trip through markdown and render artifact-style (rounded outer border, shaded header row, per-cell rules):
85
+ Type a `| a | b |` row or use the toolbar's insert-table button. GFM
86
+ round-trip:
137
87
 
138
88
  ```md
139
89
  | Metric | Q1 | Q2 |
@@ -141,72 +91,200 @@ GFM pipe tables round-trip through markdown and render artifact-style (rounded o
141
91
  | Revenue | $1.2M | $1.8M |
142
92
  ```
143
93
 
144
- Insert a 3×3 table from the toolbar, or type a `| a | b |` row. Tab/arrow navigation, cell selection, and row/column operations come from Lexical's `TablePlugin`. Inline formatting (`**bold**`, `*italic*`, `` `code` ``) works inside cells.
94
+ Rendering is artifact-style (rounded outer border, shaded header row).
95
+ Editing is in place: Tab/arrows between cells, cell range selection, inline
96
+ formatting inside cells. Cell content is single-line in markdown; newlines
97
+ are escaped as `\n`.
145
98
 
146
- ## Drawing canvas
99
+ ## Toolbar & custom toolbars
147
100
 
148
- The toolbar's "Insert drawing" button embeds an Excalidraw-style canvas for simple diagrams: rectangles, ellipses, triangles, pentagons, arrows, lines, and text labels, with a stroke and fill color palette.
101
+ In `edit-md` mode a toolbar offers inline formatting (bold, italic,
102
+ strikethrough, inline code) plus **Insert table** and **Insert drawing**.
103
+ Hide it with `toolbar={false}`.
149
104
 
150
- - Draw a shape by picking a tool and dragging; the tool returns to **Select** after each shape
151
- - Select to move, resize (corner handles), or re-point arrows/lines (endpoint handles)
152
- - Color swatches restyle the selected shape (or set defaults for the next one)
153
- - `Delete`/`Backspace` removes the selected shape; drag the bottom pill to resize the canvas
154
- - Boxes (rects/ellipses) have three built-in text slots that move with the shape: a bold **label** at the top, main **content** in the center, and a dimmer **footer** at the bottom. Double-click the top strip of a box to edit the label, the middle for content, the bottom strip for the footer (or click a selected box again, or press Enter for content). Clearing a slot removes it; the box stays.
155
- - **Arrows bind to boxes**: draw an arrow starting or ending on a box and it attaches to the box border — moving or resizing the box drags the arrow along. Endpoints re-anchor toward the other end automatically. Drag an endpoint off a box to detach it; drop it on another box to re-attach. Deleting a box releases its arrows.
156
- - A selected arrow shows a **direction toggle** in the toolbar: one-way (head at the end) or two-way (heads on both ends).
157
- - A selected connector also shows a **routing toggle**: straight (freely diagonal) or **elbow** — an orthogonal path with right-angle bends. The elbow's middle segment has a drag handle to reposition the bend; its position is stored as a fraction of the span so it stays put while bound boxes move.
158
- - **Waypoints** for arbitrary multi-segment paths: a selected connector shows dashed "+" handles at each segment midpoint — drag one to insert a bend point there. Waypoints snap to their neighbors' axes near-alignment so right angles are easy; drag freely for diagonals. Double-click a waypoint to remove it. Adding a point to an elbow connector converts its corners into editable waypoints. Selecting a routing mode from the toolbar clears waypoints.
159
- - **Arrows and lines can carry a label**: double-click the connector (or select it and press Enter) to edit text that rides the midpoint on a small backing plate, and moves as the connector moves.
160
- - The standalone **Text** tool is for free-floating annotations (e.g. labeling an arrow); double-click to edit
105
+ ### Extending the toolbar
161
106
 
162
- Drawings persist inside the markdown as a fenced block, so the document stays a plain string:
107
+ Pass a function as `toolbar`. It receives the default button groups and
108
+ returns the toolbar to render. Use `MarkdownEditor.ToolbarButton` for your
109
+ own buttons so they match the built-ins and keep the editor selection when
110
+ clicked.
163
111
 
164
- ````md
165
- ```drawing
166
- {"version":1,"height":320,"shapes":[...]}
112
+ ```tsx
113
+ <MarkdownEditor
114
+ value={value}
115
+ onChange={setValue}
116
+ toolbar={(items) => (
117
+ <MarkdownEditor.Toolbar>
118
+ {items.format}
119
+ <MarkdownEditor.ToolbarDivider />
120
+ {items.insert}
121
+ <MarkdownEditor.ToolbarDivider />
122
+ {items.history}
123
+ <MarkdownEditor.ToolbarButton label="Save" onClick={save}>
124
+ 💾
125
+ </MarkdownEditor.ToolbarButton>
126
+ </MarkdownEditor.Toolbar>
127
+ )}
128
+ />
167
129
  ```
168
- ````
169
130
 
170
- In `view` mode (or `readOnly`) the canvas renders the shapes without any editing chrome.
131
+ `MarkdownEditor.Toolbar` hides itself in `view` / `edit-raw` / `readOnly`.
132
+
133
+ ### Placing your own toolbar (compound components)
134
+
135
+ When the toolbar must live somewhere else in your layout (an app bar, a
136
+ panel header), compose the editor from its parts. Everything under
137
+ `MarkdownEditor.Root` shares one editor instance, so the toolbar can sit
138
+ anywhere in that subtree.
139
+
140
+ ```tsx
141
+ <MarkdownEditor.Root value={value} onChange={setValue}>
142
+ <header className="app-bar">
143
+ <MarkdownEditor.Toolbar>
144
+ <MarkdownEditor.FormatButtons />
145
+ <MarkdownEditor.ToolbarDivider />
146
+ <MarkdownEditor.InsertButtons />
147
+ </MarkdownEditor.Toolbar>
148
+ <MyAppButtons />
149
+ </header>
150
+ <MarkdownEditor.Content placeholder="Write…">
151
+ <MarkdownEditor.Outline />
152
+ </MarkdownEditor.Content>
153
+ </MarkdownEditor.Root>
154
+ ```
155
+
156
+ | Part | Role |
157
+ |------|------|
158
+ | `Root` | Lexical composer + plugins. Takes `value`, `onChange`, `mode`, `readOnly`, `autoFocus`, `className` |
159
+ | `Content` | The editable surface. Takes `placeholder`, `foldable`; children are docked sidebars |
160
+ | `Toolbar` | Container; renders the default groups when empty |
161
+ | `FormatButtons`, `InsertButtons`, `HistoryButtons` | Built-in groups |
162
+ | `ToolbarButton`, `ToolbarDivider` | Primitives for your own items |
163
+ | `Outline` | Table-of-contents sidebar |
164
+
165
+ ### Headless: `useMarkdownEditor()`
166
+
167
+ For fully custom UI (e.g. buttons in your own design system), call the hook
168
+ from any component rendered under `MarkdownEditor.Root`:
169
+
170
+ ```tsx
171
+ function BoldButton() {
172
+ const { activeFormats, toggleFormat } = useMarkdownEditor()
173
+ return (
174
+ <MyButton pressed={activeFormats.has('bold')} onClick={() => toggleFormat('bold')}>
175
+ B
176
+ </MyButton>
177
+ )
178
+ }
179
+ ```
180
+
181
+ It returns `editor` (the Lexical instance), `activeFormats`,
182
+ `toggleFormat`, `insertTable`, `insertDrawing`, `canUndo`, `canRedo`,
183
+ `undo`, `redo`.
184
+
185
+ ## Diagrams (drawing canvas)
186
+
187
+ The toolbar's insert-drawing button embeds a canvas; the drawing persists
188
+ in the markdown as a ` ```drawing ` fenced JSON block, fully specified in
189
+ **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
190
+
191
+ - **Shapes**: rectangle, ellipse, triangle, pentagon, arrow, line, text —
192
+ with stroke/fill palettes. Draw by picking a tool and dragging.
193
+ - **Cards**: every box carries three text slots that move, resize, and wrap
194
+ with it — a bold **label** on top, **content** in the center, a dim
195
+ **footer** at the bottom. Click a selected box (or double-click its
196
+ top/middle/bottom strip, or press Enter) to edit a slot.
197
+ - **Bound connectors**: an arrow drawn from one card to another attaches to
198
+ both — moving a card moves its arrows, endpoints anchored to the border.
199
+ Drag an endpoint off/onto a card to detach/re-attach.
200
+ - **Routing**: straight (diagonal) by default; toggle **elbow** for right
201
+ angles, or drag the dashed "+" handles on a selected connector to add any
202
+ number of **waypoints** (they snap to neighbors' axes for clean 90°
203
+ bends). One-way / two-way arrowhead toggle; midpoint **labels**.
204
+ - **Canvas**: resizable height, dot grid, white surface that inverts
205
+ Excalidraw-style in dark mode (`.dark` ancestor class).
171
206
 
172
207
  ## Outline & section folding
173
208
 
174
- `outline` docks a collapsible table-of-contents sidebar on the right: it lists the document's headings live (indented by level), scrolls to a heading on click, and highlights the section currently in view. Pure UI — nothing is added to the document. Works in `edit-md` and `view` modes (not `edit-raw`).
209
+ `outline` docks a collapsible table-of-contents sidebar: live heading list
210
+ indented by level, click to scroll, current section highlighted. Not part
211
+ of the document — pure UI. Available in `edit-md` and `view`.
175
212
 
176
- `foldable` (on by default) adds a chevron in the left gutter of every heading (visible on hover). Clicking it collapses the section — everything up to the next heading of the same or higher level — marked by a trailing `…` on the heading. Folding is view-layer only: the markdown string is unaffected, and fold state resets on remount. If the cursor enters a folded section (e.g. via arrow keys), it auto-expands so content can never be edited invisibly.
213
+ `foldable` (default on) shows a chevron in the gutter of each heading on
214
+ hover; clicking collapses the section (until the next heading of the same
215
+ or higher level). Folding never changes the markdown; fold state resets on
216
+ remount; a folded section auto-expands if the cursor enters it.
177
217
 
178
- ## Form integration
218
+ ## For AI agents / programmatic authoring
219
+
220
+ Documents are plain markdown, so LLMs can generate them — including
221
+ diagrams:
179
222
 
180
- With `@zuilib/form`:
223
+ - **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative
224
+ ` ```drawing ` payload spec, written to be pasted into a prompt (ships in
225
+ the npm package next to this README).
226
+ - `DRAWING_DATA_JSON_SCHEMA` (exported) is the same contract as JSON Schema
227
+ — use it to validate generated payloads or as a structured-output/tool
228
+ schema.
229
+ - `parseDrawingData(json)` is the editor's own lenient parser (invalid
230
+ shapes drop out; never throws); `serializeDrawingData` is its inverse.
231
+ - A ready-made Claude Code skill lives in the monorepo at
232
+ `.claude/skills/text-editor-documents/` — copy it into consuming repos so
233
+ agents there know the dialect.
234
+
235
+ ## Exports
236
+
237
+ | Export | Purpose |
238
+ |--------|---------|
239
+ | `MarkdownEditor`, `MarkdownEditorProps` | The component; compound parts as statics (`.Root`, `.Content`, `.Toolbar`, …) |
240
+ | `useMarkdownEditor`, `MarkdownEditorApi` | Headless editor hook |
241
+ | `Toolbar`, `ToolbarButton`, `ToolbarDivider`, `FormatButtons`, `InsertButtons`, `HistoryButtons` | Toolbar primitives |
242
+ | `@zuilib/text-editor/styles.css` | Editor chrome + theme styles (required) |
243
+ | `TABLE` | GFM table markdown transformer |
244
+ | `DRAWING`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformer |
245
+ | `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
246
+ | `DrawingData`, `DrawingShape`, `DrawingShapeType` | Drawing payload types |
247
+ | `parseDrawingData`, `serializeDrawingData` | Drawing payload (de)serialization |
248
+ | `DRAWING_DATA_JSON_SCHEMA` | JSON Schema of the drawing payload |
249
+
250
+ ## Form integration
181
251
 
182
252
  ```tsx
183
253
  <FormField control={form.control} name="body">
184
254
  {({ field }) => (
185
- <MarkdownEditor
186
- value={field.value}
187
- onChange={field.onChange}
188
- mode="edit-md"
189
- />
255
+ <MarkdownEditor value={field.value} onChange={field.onChange} />
190
256
  )}
191
257
  </FormField>
192
258
  ```
193
259
 
194
- Ensure both CSS entry points are loaded in the app layout.
260
+ Load both CSS entry points (`@zuilib/core/styles.css`,
261
+ `@zuilib/text-editor/styles.css`) in the app layout. Dark mode follows the
262
+ ZUI convention: a `dark` class on `<html>`.
195
263
 
196
264
  ## Architecture notes (for extenders)
197
265
 
198
- - Source: `src/MarkdownEditor.tsx`, plugins under `src/plugins/`
199
- - Lexical namespace: `ZuiTextEditor`
200
- - Markdown import/export uses `@lexical/markdown` transformers (`CHECK_LIST` prioritized for import)
201
- - `edit-raw` bypasses Lexical; switching back to `edit-md` captures `latestValueRef` and bumps `mountKey` to avoid cursor jumps
266
+ - Source: `src/EditorRoot.tsx` (composer + plugins), `src/EditorContent.tsx`,
267
+ `src/MarkdownEditor.tsx` (default composition), `src/useMarkdownEditor.ts`,
268
+ `src/components/Toolbar.tsx`; plugins in `src/plugins/`; drawing
269
+ canvas in `src/components/`; custom nodes in `src/nodes/`;
270
+ transformers in `src/transformers/`
271
+ - Markdown import/export via `@lexical/markdown` transformers; order
272
+ matters: `FRONTMATTER` and `DRAWING` claim their blocks before `CODE`,
273
+ `CHECK_LIST` before `UNORDERED_LIST`
274
+ - Drawing geometry (bindings resolver, elbow/waypoint paths, wrapping) is
275
+ pure and lives in `src/components/drawingGeometry.ts`
276
+ - Tests: `pnpm test` runs a headless-Lexical markdown round-trip suite
277
+ (`tests/roundtrip.mjs`)
202
278
 
203
279
  ## Related packages
204
280
 
205
281
  - `@zuilib/core` — design tokens and base styles
206
282
  - `@zuilib/form` — react-hook-form wiring
207
283
 
208
- ## Build (maintainers)
284
+ ## Build & release (maintainers)
209
285
 
210
286
  ```bash
211
- pnpm --filter @zuilib/text-editor build
287
+ pnpm --filter @zuilib/text-editor build # tsup → dist/
288
+ pnpm --filter @zuilib/text-editor test # round-trip suite
289
+ pnpm --filter @zuilib/text-editor publish --access public
212
290
  ```