@zuilib/text-editor 0.2.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog — @zuilib/text-editor
2
+
3
+ ## 0.2.1
4
+
5
+ - `DRAWING_FORMAT.md`: authoritative spec of the ```drawing JSON payload,
6
+ written for programmatic/LLM consumption; ships in the npm package
7
+ - New export `DRAWING_DATA_JSON_SCHEMA` (JSON Schema draft-07) for
8
+ validating or structured-output-generating drawing payloads
9
+ - New exports `parseDrawingData` / `serializeDrawingData`
10
+ - This changelog; README overhaul
11
+
12
+ ## 0.2.0
13
+
14
+ - **Outline** (`outline` prop, default off): docked, collapsible
15
+ table-of-contents sidebar — live heading list, click-to-scroll, current
16
+ section highlighted
17
+ - **Section folding** (`foldable` prop, default on): collapse everything
18
+ under a heading via a gutter chevron; view-layer only (markdown
19
+ untouched); auto-expands if the cursor enters a folded section
20
+
21
+ ## 0.1.0
22
+
23
+ - **Tables**: GFM pipe tables round-trip through markdown; artifact-style
24
+ rendering; in-place editing via Lexical `TablePlugin`; new peer deps
25
+ `@lexical/table` and `@lexical/utils`
26
+ - **Toolbar** (`toolbar` prop, default on): inline formatting, insert
27
+ table, insert drawing
28
+ - **Drawing canvas**: Excalidraw-style diagrams embedded as ```drawing
29
+ fenced JSON blocks — see `DRAWING_FORMAT.md`
30
+ - Shapes: rectangle, ellipse, triangle, pentagon, arrow, line, text
31
+ - Boxes are cards with three attached text slots (label / content /
32
+ footer) that move, resize, and wrap with the shape
33
+ - Connectors bind to boxes and track them; one-way or two-way arrows;
34
+ midpoint labels; straight, elbow, or waypoint-routed paths
35
+ - Stroke/fill palettes; dark mode via canvas color inversion
36
+ - Round-trip test suite (`pnpm test`)
37
+
38
+ ## 0.0.2 and earlier
39
+
40
+ - Markdown editor wrapping Lexical: `edit-md` / `edit-raw` / `view` modes,
41
+ markdown shortcuts, checklists, fenced code highlighting, YAML
42
+ 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` | `true` | Formatting/insert toolbar (`edit-md`) |
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,110 @@ 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`.
98
+
99
+ ## Diagrams (drawing canvas)
100
+
101
+ The toolbar's insert-drawing button embeds a canvas; the drawing persists
102
+ in the markdown as a ` ```drawing ` fenced JSON block, fully specified in
103
+ **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
104
+
105
+ - **Shapes**: rectangle, ellipse, triangle, pentagon, arrow, line, text —
106
+ with stroke/fill palettes. Draw by picking a tool and dragging.
107
+ - **Cards**: every box carries three text slots that move, resize, and wrap
108
+ with it — a bold **label** on top, **content** in the center, a dim
109
+ **footer** at the bottom. Click a selected box (or double-click its
110
+ top/middle/bottom strip, or press Enter) to edit a slot.
111
+ - **Bound connectors**: an arrow drawn from one card to another attaches to
112
+ both — moving a card moves its arrows, endpoints anchored to the border.
113
+ Drag an endpoint off/onto a card to detach/re-attach.
114
+ - **Routing**: straight (diagonal) by default; toggle **elbow** for right
115
+ angles, or drag the dashed "+" handles on a selected connector to add any
116
+ number of **waypoints** (they snap to neighbors' axes for clean 90°
117
+ bends). One-way / two-way arrowhead toggle; midpoint **labels**.
118
+ - **Canvas**: resizable height, dot grid, white surface that inverts
119
+ Excalidraw-style in dark mode (`.dark` ancestor class).
145
120
 
146
- ## Drawing canvas
147
-
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.
121
+ ## Outline & section folding
149
122
 
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
123
+ `outline` docks a collapsible table-of-contents sidebar: live heading list
124
+ indented by level, click to scroll, current section highlighted. Not part
125
+ of the document pure UI. Available in `edit-md` and `view`.
161
126
 
162
- Drawings persist inside the markdown as a fenced block, so the document stays a plain string:
127
+ `foldable` (default on) shows a chevron in the gutter of each heading on
128
+ hover; clicking collapses the section (until the next heading of the same
129
+ or higher level). Folding never changes the markdown; fold state resets on
130
+ remount; a folded section auto-expands if the cursor enters it.
163
131
 
164
- ````md
165
- ```drawing
166
- {"version":1,"height":320,"shapes":[...]}
167
- ```
168
- ````
132
+ ## For AI agents / programmatic authoring
169
133
 
170
- In `view` mode (or `readOnly`) the canvas renders the shapes without any editing chrome.
134
+ Documents are plain markdown, so LLMs can generate them including
135
+ diagrams:
171
136
 
172
- ## Outline & section folding
137
+ - **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative
138
+ ` ```drawing ` payload spec, written to be pasted into a prompt (ships in
139
+ the npm package next to this README).
140
+ - `DRAWING_DATA_JSON_SCHEMA` (exported) is the same contract as JSON Schema
141
+ — use it to validate generated payloads or as a structured-output/tool
142
+ schema.
143
+ - `parseDrawingData(json)` is the editor's own lenient parser (invalid
144
+ shapes drop out; never throws); `serializeDrawingData` is its inverse.
145
+ - A ready-made Claude Code skill lives in the monorepo at
146
+ `.claude/skills/text-editor-documents/` — copy it into consuming repos so
147
+ agents there know the dialect.
173
148
 
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`).
149
+ ## Exports
175
150
 
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.
151
+ | Export | Purpose |
152
+ |--------|---------|
153
+ | `MarkdownEditor`, `MarkdownEditorProps` | The component |
154
+ | `@zuilib/text-editor/styles.css` | Editor chrome + theme styles (required) |
155
+ | `TABLE` | GFM table markdown transformer |
156
+ | `DRAWING`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformer |
157
+ | `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
158
+ | `DrawingData`, `DrawingShape`, `DrawingShapeType` | Drawing payload types |
159
+ | `parseDrawingData`, `serializeDrawingData` | Drawing payload (de)serialization |
160
+ | `DRAWING_DATA_JSON_SCHEMA` | JSON Schema of the drawing payload |
177
161
 
178
162
  ## Form integration
179
163
 
180
- With `@zuilib/form`:
181
-
182
164
  ```tsx
183
165
  <FormField control={form.control} name="body">
184
166
  {({ field }) => (
185
- <MarkdownEditor
186
- value={field.value}
187
- onChange={field.onChange}
188
- mode="edit-md"
189
- />
167
+ <MarkdownEditor value={field.value} onChange={field.onChange} />
190
168
  )}
191
169
  </FormField>
192
170
  ```
193
171
 
194
- Ensure both CSS entry points are loaded in the app layout.
172
+ Load both CSS entry points (`@zuilib/core/styles.css`,
173
+ `@zuilib/text-editor/styles.css`) in the app layout. Dark mode follows the
174
+ ZUI convention: a `dark` class on `<html>`.
195
175
 
196
176
  ## Architecture notes (for extenders)
197
177
 
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
178
+ - Source: `src/MarkdownEditor.tsx`; plugins in `src/plugins/`; drawing
179
+ canvas in `src/components/`; custom nodes in `src/nodes/`;
180
+ transformers in `src/transformers/`
181
+ - Markdown import/export via `@lexical/markdown` transformers; order
182
+ matters: `FRONTMATTER` and `DRAWING` claim their blocks before `CODE`,
183
+ `CHECK_LIST` before `UNORDERED_LIST`
184
+ - Drawing geometry (bindings resolver, elbow/waypoint paths, wrapping) is
185
+ pure and lives in `src/components/drawingGeometry.ts`
186
+ - Tests: `pnpm test` runs a headless-Lexical markdown round-trip suite
187
+ (`tests/roundtrip.mjs`)
202
188
 
203
189
  ## Related packages
204
190
 
205
191
  - `@zuilib/core` — design tokens and base styles
206
192
  - `@zuilib/form` — react-hook-form wiring
207
193
 
208
- ## Build (maintainers)
194
+ ## Build & release (maintainers)
209
195
 
210
196
  ```bash
211
- pnpm --filter @zuilib/text-editor build
197
+ pnpm --filter @zuilib/text-editor build # tsup → dist/
198
+ pnpm --filter @zuilib/text-editor test # round-trip suite
199
+ pnpm --filter @zuilib/text-editor publish --access public
212
200
  ```
package/dist/index.d.ts CHANGED
@@ -104,6 +104,8 @@ type DrawingData = Readonly<{
104
104
  height: number;
105
105
  shapes: readonly DrawingShape[];
106
106
  }>;
107
+ declare function serializeDrawingData(data: DrawingData): string;
108
+ declare function parseDrawingData(json: string): DrawingData;
107
109
 
108
110
  type SerializedDrawingNode = Spread<{
109
111
  data: string;
@@ -147,4 +149,136 @@ declare const DRAWING: MultilineElementTransformer;
147
149
  */
148
150
  declare const TABLE: ElementTransformer;
149
151
 
150
- export { $createDrawingNode, $createFrontmatterNode, $isDrawingNode, $isFrontmatterNode, DRAWING, type DrawingData, DrawingNode, type DrawingShape, type DrawingShapeType, FRONTMATTER, FrontmatterNode, MarkdownEditor, type Props as MarkdownEditorProps, type SerializedDrawingNode, type SerializedFrontmatterNode, TABLE };
152
+ /**
153
+ * JSON Schema (draft-07) for the payload of a ```drawing fenced block.
154
+ *
155
+ * Use it to validate LLM- or tool-generated drawings before embedding them,
156
+ * or pass it as a structured-output / tool schema so a model is forced to
157
+ * emit valid payloads. Kept in sync with `parseDrawingData` / `isValidShape`
158
+ * in drawingTypes.ts — update both together.
159
+ */
160
+ declare const DRAWING_DATA_JSON_SCHEMA: {
161
+ readonly $schema: "http://json-schema.org/draft-07/schema#";
162
+ readonly title: "DrawingData";
163
+ readonly description: "Vector drawing embedded in markdown as a ```drawing fenced code block";
164
+ readonly type: "object";
165
+ readonly required: readonly ["version", "height", "shapes"];
166
+ readonly additionalProperties: false;
167
+ readonly properties: {
168
+ readonly version: {
169
+ readonly const: 1;
170
+ };
171
+ readonly height: {
172
+ readonly type: "number";
173
+ readonly minimum: 80;
174
+ readonly description: "Canvas height in pixels (width is fluid)";
175
+ };
176
+ readonly shapes: {
177
+ readonly type: "array";
178
+ readonly items: {
179
+ readonly $ref: "#/definitions/shape";
180
+ };
181
+ readonly description: "Render order: later shapes draw on top";
182
+ };
183
+ };
184
+ readonly definitions: {
185
+ readonly point: {
186
+ readonly type: "object";
187
+ readonly required: readonly ["x", "y"];
188
+ readonly additionalProperties: false;
189
+ readonly properties: {
190
+ readonly x: {
191
+ readonly type: "number";
192
+ };
193
+ readonly y: {
194
+ readonly type: "number";
195
+ };
196
+ };
197
+ };
198
+ readonly shape: {
199
+ readonly type: "object";
200
+ readonly required: readonly ["id", "type", "x", "y", "w", "h", "stroke", "fill", "strokeWidth"];
201
+ readonly additionalProperties: false;
202
+ readonly properties: {
203
+ readonly id: {
204
+ readonly type: "string";
205
+ readonly description: "Unique within the drawing; connector bindings reference it";
206
+ };
207
+ readonly type: {
208
+ readonly enum: readonly ["rect", "ellipse", "triangle", "pentagon", "arrow", "line", "text"];
209
+ };
210
+ readonly x: {
211
+ readonly type: "number";
212
+ readonly description: "Boxes/text: left edge of the bounding box. Connectors: start point x.";
213
+ };
214
+ readonly y: {
215
+ readonly type: "number";
216
+ readonly description: "Boxes/text: top edge of the bounding box. Connectors: start point y.";
217
+ };
218
+ readonly w: {
219
+ readonly type: "number";
220
+ readonly description: "Boxes/text: width (non-negative). Connectors: delta x to the end point (may be negative).";
221
+ };
222
+ readonly h: {
223
+ readonly type: "number";
224
+ readonly description: "Boxes/text: height (non-negative). Connectors: delta y to the end point (may be negative).";
225
+ };
226
+ readonly stroke: {
227
+ readonly type: "string";
228
+ readonly description: "CSS color of the outline (and of any text)";
229
+ };
230
+ readonly fill: {
231
+ readonly type: "string";
232
+ readonly description: "CSS color of the interior; \"transparent\" for none";
233
+ };
234
+ readonly strokeWidth: {
235
+ readonly type: "number";
236
+ readonly description: "Outline width; use 2";
237
+ };
238
+ readonly text: {
239
+ readonly type: "string";
240
+ readonly description: "Standalone text content; center content of a box; midpoint label of a connector";
241
+ };
242
+ readonly label: {
243
+ readonly type: "string";
244
+ readonly description: "Boxes only: small bold heading at the top";
245
+ };
246
+ readonly footer: {
247
+ readonly type: "string";
248
+ readonly description: "Boxes only: small dim line at the bottom";
249
+ };
250
+ readonly startBinding: {
251
+ readonly type: "string";
252
+ readonly description: "Connectors only: id of the box the start attaches to. The editor re-anchors the endpoint onto that box’s border.";
253
+ };
254
+ readonly endBinding: {
255
+ readonly type: "string";
256
+ readonly description: "Connectors only: id of the box the end attaches to";
257
+ };
258
+ readonly bidirectional: {
259
+ readonly type: "boolean";
260
+ readonly description: "Arrows only: arrowheads on both ends";
261
+ };
262
+ readonly routing: {
263
+ readonly const: "elbow";
264
+ readonly description: "Connectors only: orthogonal (right-angled) auto-routing. Ignored when waypoints are present.";
265
+ };
266
+ readonly elbow: {
267
+ readonly type: "number";
268
+ readonly minimum: 0;
269
+ readonly maximum: 1;
270
+ readonly description: "Elbow middle-segment position as a fraction of the span (default 0.5)";
271
+ };
272
+ readonly waypoints: {
273
+ readonly type: "array";
274
+ readonly items: {
275
+ readonly $ref: "#/definitions/point";
276
+ };
277
+ readonly description: "Connectors only: intermediate path points in canvas coordinates, ordered start→end. Takes precedence over routing.";
278
+ };
279
+ };
280
+ };
281
+ };
282
+ };
283
+
284
+ export { $createDrawingNode, $createFrontmatterNode, $isDrawingNode, $isFrontmatterNode, DRAWING, DRAWING_DATA_JSON_SCHEMA, type DrawingData, DrawingNode, type DrawingShape, type DrawingShapeType, FRONTMATTER, FrontmatterNode, MarkdownEditor, type Props as MarkdownEditorProps, type SerializedDrawingNode, type SerializedFrontmatterNode, TABLE, parseDrawingData, serializeDrawingData };
package/dist/index.js CHANGED
@@ -2689,15 +2689,127 @@ function MarkdownEditor({
2689
2689
  autoFocus && /* @__PURE__ */ jsx6(AutoFocusPlugin, {})
2690
2690
  ] }) }, mountKey);
2691
2691
  }
2692
+
2693
+ // src/components/drawingSchema.ts
2694
+ var DRAWING_DATA_JSON_SCHEMA = {
2695
+ $schema: "http://json-schema.org/draft-07/schema#",
2696
+ title: "DrawingData",
2697
+ description: "Vector drawing embedded in markdown as a ```drawing fenced code block",
2698
+ type: "object",
2699
+ required: ["version", "height", "shapes"],
2700
+ additionalProperties: false,
2701
+ properties: {
2702
+ version: { const: 1 },
2703
+ height: {
2704
+ type: "number",
2705
+ minimum: 80,
2706
+ description: "Canvas height in pixels (width is fluid)"
2707
+ },
2708
+ shapes: {
2709
+ type: "array",
2710
+ items: { $ref: "#/definitions/shape" },
2711
+ description: "Render order: later shapes draw on top"
2712
+ }
2713
+ },
2714
+ definitions: {
2715
+ point: {
2716
+ type: "object",
2717
+ required: ["x", "y"],
2718
+ additionalProperties: false,
2719
+ properties: { x: { type: "number" }, y: { type: "number" } }
2720
+ },
2721
+ shape: {
2722
+ type: "object",
2723
+ required: ["id", "type", "x", "y", "w", "h", "stroke", "fill", "strokeWidth"],
2724
+ additionalProperties: false,
2725
+ properties: {
2726
+ id: {
2727
+ type: "string",
2728
+ description: "Unique within the drawing; connector bindings reference it"
2729
+ },
2730
+ type: {
2731
+ enum: ["rect", "ellipse", "triangle", "pentagon", "arrow", "line", "text"]
2732
+ },
2733
+ x: {
2734
+ type: "number",
2735
+ description: "Boxes/text: left edge of the bounding box. Connectors: start point x."
2736
+ },
2737
+ y: {
2738
+ type: "number",
2739
+ description: "Boxes/text: top edge of the bounding box. Connectors: start point y."
2740
+ },
2741
+ w: {
2742
+ type: "number",
2743
+ description: "Boxes/text: width (non-negative). Connectors: delta x to the end point (may be negative)."
2744
+ },
2745
+ h: {
2746
+ type: "number",
2747
+ description: "Boxes/text: height (non-negative). Connectors: delta y to the end point (may be negative)."
2748
+ },
2749
+ stroke: {
2750
+ type: "string",
2751
+ description: "CSS color of the outline (and of any text)"
2752
+ },
2753
+ fill: {
2754
+ type: "string",
2755
+ description: 'CSS color of the interior; "transparent" for none'
2756
+ },
2757
+ strokeWidth: { type: "number", description: "Outline width; use 2" },
2758
+ text: {
2759
+ type: "string",
2760
+ description: "Standalone text content; center content of a box; midpoint label of a connector"
2761
+ },
2762
+ label: {
2763
+ type: "string",
2764
+ description: "Boxes only: small bold heading at the top"
2765
+ },
2766
+ footer: {
2767
+ type: "string",
2768
+ description: "Boxes only: small dim line at the bottom"
2769
+ },
2770
+ startBinding: {
2771
+ type: "string",
2772
+ description: "Connectors only: id of the box the start attaches to. The editor re-anchors the endpoint onto that box\u2019s border."
2773
+ },
2774
+ endBinding: {
2775
+ type: "string",
2776
+ description: "Connectors only: id of the box the end attaches to"
2777
+ },
2778
+ bidirectional: {
2779
+ type: "boolean",
2780
+ description: "Arrows only: arrowheads on both ends"
2781
+ },
2782
+ routing: {
2783
+ const: "elbow",
2784
+ description: "Connectors only: orthogonal (right-angled) auto-routing. Ignored when waypoints are present."
2785
+ },
2786
+ elbow: {
2787
+ type: "number",
2788
+ minimum: 0,
2789
+ maximum: 1,
2790
+ description: "Elbow middle-segment position as a fraction of the span (default 0.5)"
2791
+ },
2792
+ waypoints: {
2793
+ type: "array",
2794
+ items: { $ref: "#/definitions/point" },
2795
+ description: "Connectors only: intermediate path points in canvas coordinates, ordered start\u2192end. Takes precedence over routing."
2796
+ }
2797
+ }
2798
+ }
2799
+ }
2800
+ };
2692
2801
  export {
2693
2802
  $createDrawingNode,
2694
2803
  $createFrontmatterNode,
2695
2804
  $isDrawingNode,
2696
2805
  $isFrontmatterNode,
2697
2806
  DRAWING,
2807
+ DRAWING_DATA_JSON_SCHEMA,
2698
2808
  DrawingNode,
2699
2809
  FRONTMATTER,
2700
2810
  FrontmatterNode,
2701
2811
  MarkdownEditor,
2702
- TABLE
2812
+ TABLE,
2813
+ parseDrawingData,
2814
+ serializeDrawingData
2703
2815
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuilib/text-editor",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "ZUI — A markdown editor component wrapping Lexical",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,9 @@
9
9
  "**/*.css"
10
10
  ],
11
11
  "files": [
12
- "dist"
12
+ "dist",
13
+ "DRAWING_FORMAT.md",
14
+ "CHANGELOG.md"
13
15
  ],
14
16
  "exports": {
15
17
  ".": {