@zuilib/text-editor 0.4.0 → 0.5.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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog — @zuilib/text-editor
2
2
 
3
+ ## 0.5.0
4
+
5
+ **Diagram builder rewrite.** The drawing canvas, its payload format and the
6
+ authoring story for LLMs were rebuilt.
7
+
8
+ - **Format v2 (breaking)**: ` ```drawing ` payloads are now
9
+ `{"version":2,"canvasHeight":<px>,"canvasWidth"?:<px>,"width"?:"content","shapes":[...]}`.
10
+ No migration from version 1 and no legacy spellings: anything that is
11
+ not a version 2 payload renders as an empty canvas.
12
+ `canvasWidth` is optional: when set the drawing scales to fit narrower
13
+ layouts (SVG viewBox), when absent the canvas is fluid. `triangle` and
14
+ `pentagon` are removed. `DRAWING_DATA_JSON_SCHEMA` and
15
+ `DRAWING_FORMAT.md` rewritten for v2
16
+ - **Shapes**: boxes `rect`, `ellipse`, `diamond`, `note` (sticky note with
17
+ a folded corner, default fill `#ffec99`), `cylinder` (database), `cloud`,
18
+ `queue` (horizontal cylinder), `actor` (stick figure with a name under
19
+ it, `text` slot only); connectors `arrow`, `line`; free `text`. Palette
20
+ gains purple (`#7048e8` / `#d0bfff`)
21
+ - **Bindings** are objects: `{ id, fixedPoint?: [fx, fy], mode?: "orbit" |
22
+ "inside" }`. `fixedPoint` is a ratio in the box's bounding box; omitted,
23
+ the endpoint auto-aims from the box center at the other end. `orbit`
24
+ (default) projects the endpoint onto the outline with a 6px gap,
25
+ `inside` pins it exactly. The `side: "top" | "right" | "bottom" |
26
+ "left"` shorthand exists only in the ` ```diagram ` skeleton
27
+ - **Router**: `routing: "elbow"` is auto-routed. It leaves the box
28
+ perpendicular to the attach side, avoids every box on the canvas and
29
+ minimises bends. `elbow` (0 to 1) remains as an override of the middle
30
+ segment when the path is a simple three-segment Z; `waypoints` still
31
+ take precedence
32
+ - **Editor UX**: multi-select (shift-click, marquee on empty canvas; move,
33
+ delete and recolor together), redesigned toolbar with a "more shapes"
34
+ popover, contextual property bar, smoother drag and selection visuals,
35
+ **Copy as Mermaid** button
36
+ - **` ```diagram ` skeleton**: a fenced block with a coordinate-free JSON
37
+ skeleton (`boxes`, `connectors`, `texts`, `direction`, palette `color`
38
+ names) that the editor auto-lays out and expands into a ` ```drawing `
39
+ block on import (one-way). New exports `expandSkeleton`,
40
+ `parseDrawingSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA`, `DIAGRAM`
41
+ transformer, types `DrawingSkeleton`, `SkeletonBox`, `SkeletonConnector`
42
+ - **Mermaid export**: `drawingToMermaid(data, { direction? })` returns a
43
+ `flowchart` with one node per box (shape-specific node syntax), edges for
44
+ connectors bound at both ends (labels as `|text|`), `style` lines for
45
+ coloured boxes and free text as `%% note:` comments
46
+ - Internal: the drawing code moved from `src/components/` into
47
+ `src/drawing/` (`types.ts`, `schema.ts`, `geometry.ts`, `mermaid.ts`,
48
+ `shapes/`, `canvas/`); shape geometry is data-driven so the router,
49
+ bindings, skeleton expansion and exporters run without a DOM
50
+
3
51
  ## 0.4.0
4
52
 
5
53
  - **Table density**, Slab-style: the floating table toolbar (caret in a
package/DRAWING_FORMAT.md CHANGED
@@ -1,43 +1,59 @@
1
- # The `drawing` block format
1
+ # The `drawing` and `diagram` block formats
2
2
 
3
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:
4
+ fenced code blocks containing a single JSON object. Two languages exist:
5
+
6
+ - ```` ```diagram ```` holds a **skeleton**: boxes, connectors and colors, no
7
+ coordinates. The editor lays it out and expands it into a full drawing on
8
+ import. **Generators should emit this one.** See
9
+ [Prefer the ```diagram skeleton](#prefer-the-diagram-skeleton).
10
+ - ```` ```drawing ```` holds the **concrete format** (version 2): every shape
11
+ with its position, size and style. This is what the editor stores once a
12
+ drawing has been edited.
6
13
 
7
14
  ````md
8
15
  ```drawing
9
- {"version":1,"height":320,"shapes":[ ... ]}
16
+ {"version":2,"canvasHeight":320,"shapes":[ ... ]}
10
17
  ```
11
18
  ````
12
19
 
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`.
20
+ This document is the authoritative spec of both payloads. It is written so
21
+ it can be handed to a program or an LLM that needs to **generate or edit
22
+ drawings**. Machine-readable JSON Schemas of the same rules are exported
23
+ from the package as `DRAWING_DATA_JSON_SCHEMA` (concrete) and
24
+ `DRAWING_SKELETON_JSON_SCHEMA` (skeleton).
17
25
 
18
26
  Malformed payloads never crash the editor: anything that fails validation
19
27
  degrades to an empty canvas (invalid shapes are dropped individually).
20
28
 
29
+ Exactly one concrete format is supported: version 2, as written by the
30
+ editor. Payloads with any other `version` (or none) render as an empty
31
+ canvas. Convenience spellings such as `side` exist only in the ```diagram
32
+ skeleton, which is a separate block type.
33
+
21
34
  ## Coordinate system
22
35
 
23
36
  - Origin `(0,0)` is the canvas's top-left; x grows right, y grows down.
24
37
  - Units are CSS pixels.
25
- - The canvas is as wide as the editor by default (`width` omitted or
26
- `"full"`). Keep shapes within roughly x ∈ [0, 700] to be safe on typical
27
- layouts.
28
- - `"width":"content"` instead sizes the canvas to fit the rightmost shape
29
- (plus a margin, never narrower than 240px), left-aligned with the text
30
- use it for small diagrams that shouldn't stretch across the page.
31
- - `height` is the canvas height in pixels (minimum 80; 300–400 is typical).
38
+ - `canvasHeight` is the canvas height in pixels (minimum 80; 300 to 400 is
39
+ typical).
40
+ - `canvasWidth` is optional. When set, the drawing is scaled down to fit
41
+ narrower layouts (SVG viewBox), so coordinates can assume that width.
42
+ When omitted the canvas is fluid and as wide as the editor; keep shapes
43
+ within roughly x in [0, 700] to be safe on typical layouts.
44
+ - `"width":"content"` sizes the block to fit the rightmost shape (plus a
45
+ margin), left-aligned with the text. Use it for small diagrams that
46
+ should not stretch across the page. Default `"full"`, omit it.
32
47
 
33
48
  ## Top-level object
34
49
 
35
- | Field | Type | Notes |
36
- |-----------|-----------|-----------------------------------------|
37
- | `version` | `1` | Literal `1` |
38
- | `height` | `number` | Canvas height in px |
39
- | `width` | `"full"` \| `"content"` | Optional; default `"full"`. `"content"` fits the shapes' extent |
40
- | `shapes` | `Shape[]` | Render order: later shapes draw on top |
50
+ | Field | Type | Notes |
51
+ |----------------|-----------|--------------------------------------------------------------|
52
+ | `version` | `2` | Literal `2` |
53
+ | `canvasHeight` | `number` | Canvas height in px (min 80) |
54
+ | `canvasWidth` | `number` | Optional logical width in px (min 120); scales to fit when set |
55
+ | `width` | `"full"` \| `"content"` | Optional; default `"full"`. `"content"` fits the shapes' extent |
56
+ | `shapes` | `Shape[]` | Render order: later shapes draw on top |
41
57
 
42
58
  ## Shape object
43
59
 
@@ -46,17 +62,33 @@ All shapes share these required fields:
46
62
  | Field | Type | Meaning |
47
63
  |---------------|----------|--------------------------------------------------------------------|
48
64
  | `id` | `string` | Unique within the drawing; bindings reference it |
49
- | `type` | `string` | `rect` \| `ellipse` \| `triangle` \| `pentagon` \| `arrow` \| `line` \| `text` |
65
+ | `type` | `string` | One of the types below |
50
66
  | `x`, `y` | `number` | Boxes/text: top-left of the bounding box. Connectors: start point. |
51
67
  | `w`, `h` | `number` | Boxes/text: size (non-negative). Connectors: delta to the end point (`end = (x+w, y+h)`; may be negative). |
52
68
  | `stroke` | `string` | CSS color of the outline and of all text on the shape |
53
69
  | `fill` | `string` | CSS color of the interior; `"transparent"` for none |
54
70
  | `strokeWidth` | `number` | Use `2` |
55
71
 
56
- ### Boxes (`rect`, `ellipse`, `triangle`, `pentagon`) — "cards"
72
+ ### Shape types
73
+
74
+ | Type | Kind | Default size | Notes |
75
+ |------------|-----------|--------------|--------------------------------------------------------------|
76
+ | `rect` | box | 160 x 90 | Rectangle; the general-purpose card |
77
+ | `ellipse` | box | 160 x 90 | Ellipse inscribed in the bounding box |
78
+ | `diamond` | box | 170 x 100 | Decision; text area is the inner half |
79
+ | `note` | box | 160 x 110 | Sticky note with a folded bottom-right corner; default fill `#ffec99` |
80
+ | `cylinder` | box | 140 x 110 | Database; text sits below the top ellipse |
81
+ | `cloud` | box | 180 x 110 | Cloud; outline is an ellipse for binding purposes |
82
+ | `queue` | box | 180 x 80 | Horizontal cylinder (queue, stream, pipe) |
83
+ | `actor` | box | 90 x 120 | Stick figure; `text` only, rendered as a name under the figure |
84
+ | `arrow` | connector | | Head at the end; `bidirectional` for both ends |
85
+ | `line` | connector | | No arrowhead |
86
+ | `text` | free text | | Floating annotation |
87
+
88
+ ### Boxes ("cards")
57
89
 
58
90
  Boxes are card-like: they can carry up to three text slots that render
59
- inside the shape and move/resize/wrap with it:
91
+ inside the shape and move, resize and wrap with it:
60
92
 
61
93
  | Field | Renders as |
62
94
  |----------|-----------------------------------------------|
@@ -64,12 +96,14 @@ inside the shape and move/resize/wrap with it:
64
96
  | `text` | Main content, centered |
65
97
  | `footer` | Small dim line at the bottom |
66
98
 
67
- Text wraps automatically to the box width; `\n` forces a line break.
68
- Triangles are apex-up isoceles; pentagons are apex-up, inscribed in the
69
- bounding box. Prefer these slots over floating `text` shapes slot text is
70
- attached to the card.
99
+ Text wraps automatically to the box's text area; `\n` forces a line break.
100
+ The text area is smaller than the bounding box for non-rectangular shapes
101
+ (diamond, ellipse, cloud, cylinder), so give those a little more room.
102
+ `actor` has only the `text` slot: it is drawn under the figure and may be
103
+ wider than the box; `label` and `footer` are dropped. Prefer slots over
104
+ floating `text` shapes; slot text is attached to the card.
71
105
 
72
- ### `text` free-floating annotation
106
+ ### `text`: free-floating annotation
73
107
 
74
108
  `text` holds the content. `w`/`h` are advisory (the editor recomputes them
75
109
  from the content); position is the top-left of the first line. Use only for
@@ -85,70 +119,210 @@ Geometry: from `(x, y)` to `(x+w, y+h)`. `arrow` has a head at the end;
85
119
  |-----------------|------------|-------------------------------------------------------------------------|
86
120
  | `text` | `string` | Label rendered at the path midpoint on a small backing plate |
87
121
  | `bidirectional` | `boolean` | Arrows only: heads on both ends |
88
- | `startBinding` | `string` | id of the box the start attaches to |
89
- | `endBinding` | `string` | id of the box the end attaches to |
90
- | `routing` | `"elbow"` | Orthogonal auto-routing with up to two right-angle bends |
91
- | `elbow` | `number` | 01: middle-segment position along the span (default 0.5) |
92
- | `waypoints` | `Point[]` | Explicit intermediate path points (`{x,y}`), ordered startend |
93
-
94
- **Bindings.** When `startBinding`/`endBinding` name a box id, the editor
95
- re-anchors that endpoint onto the box's border (aimed at the other end, with
96
- a small gap) and keeps it attached as the box moves. Generated endpoints
97
- therefore only need to be approximately right — place them near the intended
98
- boxes and the resolver snaps them. Don't bind both ends of one connector to
99
- the same box. Bindings to ids that don't exist are silently dropped.
100
-
101
- **Routing.** Omit `routing` for a straight (possibly diagonal) connector.
102
- `"elbow"` renders an orthogonal path: it runs along the dominant axis, turns
103
- through a perpendicular middle segment (positioned by `elbow`), and turns
104
- again. If `waypoints` is non-empty it defines the path (`start → waypoints…
105
- end`) and `routing`/`elbow` are ignored. Waypoints are absolute canvas
122
+ | `startBinding` | `Binding` | Box the start attaches to (see below) |
123
+ | `endBinding` | `Binding` | Box the end attaches to |
124
+ | `routing` | `"elbow"` | Right-angled auto-routed path |
125
+ | `elbow` | `number` | 0 to 1: override of the middle segment, only for a simple three-segment Z |
126
+ | `waypoints` | `Point[]` | Explicit intermediate path points (`{x,y}`), ordered start to end |
127
+
128
+ ## Bindings
129
+
130
+ A binding attaches a connector endpoint to a box and keeps it attached as
131
+ the box moves:
132
+
133
+ ```json
134
+ { "id": "<box id>", "fixedPoint": [fx, fy], "mode": "orbit" }
135
+ ```
136
+
137
+ | Field | Type | Meaning |
138
+ |--------------|--------------------------|-------------------------------------------------------------------------|
139
+ | `id` | `string` | Required. Id of a box shape (bindings to unknown or non-box ids are dropped) |
140
+ | `fixedPoint` | `[number, number]` | Optional. Attach point as a ratio of the box's bounding box: `[0,0]` top-left, `[1,1]` bottom-right, `[0.5,0]` top edge midpoint. Values are clamped to 0 to 1 |
141
+ | `mode` | `"orbit"` \| `"inside"` | Optional, default `"orbit"` |
142
+
143
+ - **Omit `fixedPoint`** when you do not care where the connector attaches:
144
+ the endpoint auto-aims from the box center at the other end of the
145
+ connector, so the line always meets the outline on the facing side.
146
+ - **Set `fixedPoint` to an edge midpoint** when the attach edge matters (a
147
+ decision's "yes" leaves at the bottom `[0.5,1]`, "no" at the right
148
+ `[1,0.5]`). In the ```diagram skeleton the same thing is spelled
149
+ `"side":"bottom"`.
150
+ - `mode: "orbit"` (default) projects the endpoint onto the box outline with
151
+ a 6px gap. `mode: "inside"` pins the endpoint exactly on the fixed point
152
+ (useful for lines that end inside a shape).
153
+
154
+ Bound endpoints are re-anchored by the editor, so the connector's own
155
+ `x`/`y`/`w`/`h` only need to be approximately right: place them near the
156
+ intended boxes and the resolver snaps them. Don't bind both ends of one
157
+ connector to the same box.
158
+
159
+ ## Routing
160
+
161
+ Omit `routing` for a straight (possibly diagonal) connector.
162
+
163
+ `"routing":"elbow"` renders an auto-routed orthogonal path: it leaves the
164
+ box perpendicular to the attach side, avoids every box on the canvas, and
165
+ uses as few bends as possible. `elbow` (0 to 1) is an optional override of
166
+ the middle segment's position along the span; it is applied only when the
167
+ routed path is a simple three-segment Z, and ignored otherwise.
168
+
169
+ If `waypoints` is non-empty it defines the path (`start, waypoints..., end`)
170
+ and `routing`/`elbow` are ignored. Waypoints are absolute canvas
106
171
  coordinates; align consecutive points on x or y to get right angles.
107
172
 
108
173
  ## Recommended colors
109
174
 
110
- Any CSS color works; the editor's palette (dark-mode safe the canvas
175
+ Any CSS color works; the editor's palette (dark-mode safe, the canvas
111
176
  inverts colors Excalidraw-style in dark themes):
112
177
 
113
- - Strokes: `#1e1e1e` (default), `#e03131` red, `#2f9e44` green, `#1971c2` blue, `#f08c00` orange
114
- - Fills: `transparent`, `#ffc9c9`, `#b2f2bb`, `#a5d8ff`, `#ffec99`
178
+ | Name | Stroke | Fill |
179
+ |--------|-----------|---------------|
180
+ | gray | `#1e1e1e` | `transparent` |
181
+ | red | `#e03131` | `#ffc9c9` |
182
+ | green | `#2f9e44` | `#b2f2bb` |
183
+ | blue | `#1971c2` | `#a5d8ff` |
184
+ | orange | `#f08c00` | `#ffec99` |
185
+ | purple | `#7048e8` | `#d0bfff` |
186
+
187
+ Pair a colored stroke with its pastel fill. `note` defaults to the yellow
188
+ fill `#ffec99`.
189
+
190
+ ## Prefer the ```diagram skeleton
191
+
192
+ A fenced block with language `diagram` holds a skeleton: what the boxes are
193
+ and how they connect. The editor expands it into a full ```drawing block
194
+ on import (auto layout, default sizes, palette colors). The expansion is
195
+ one-way: once opened, the document stores the concrete ```drawing block.
196
+
197
+ ```
198
+ {
199
+ "direction"?: "right" | "down", // auto-layout flow, default right
200
+ "canvasWidth"?: n, "canvasHeight"?: n, "width"?: "content",
201
+ "boxes": [ { "id", "type"?: <box type, default rect>, "label"?, "text"?, "footer"?,
202
+ "x"?, "y"?, // omitted: auto layout by connector rank
203
+ "w"?, "h"?, // omitted: sized to fit the text
204
+ "color"?: "gray"|"red"|"green"|"blue"|"orange"|"purple", // stroke + pastel fill
205
+ "stroke"?, "fill"? } ],
206
+ "connectors"?: [ { "id"?, "type"?: "arrow"|"line", "from": "<id>" | {"id","side"?}, "to": same,
207
+ "text"?, "bidirectional"?, "routing"?: "elbow"|"straight", "color"?, "stroke"? } ],
208
+ "texts"?: [ { "x", "y", "text", "color"? } ]
209
+ }
210
+ ```
211
+
212
+ Rules:
213
+
214
+ - `boxes` is required; every box needs a unique `id`. `type` is any box
215
+ type from the table above (default `rect`).
216
+ - `from`/`to` are box ids, or `{"id","side"}` to pick the attach edge.
217
+ - `color` picks a palette pair (stroke plus pastel fill) from the table
218
+ above. `stroke`/`fill` override it with explicit CSS colors. On
219
+ connectors only the stroke applies.
115
220
 
116
- Pair a colored stroke with its pastel fill (`#1971c2` + `#a5d8ff`, etc.).
221
+ - Auto layout: boxes are ranked along `direction` by the longest path from
222
+ the sources (cycles are broken). Ranks are 90px apart, boxes within a
223
+ rank 40px apart, starting at (32,32). Boxes with explicit `x`/`y` keep
224
+ them. Boxes without `w`/`h` are sized to fit their text. `canvasHeight`
225
+ is computed when omitted.
226
+ - `texts` are free annotations and need explicit coordinates.
117
227
 
118
- ## Examples
228
+ A three-box flow:
229
+
230
+ ````md
231
+ ```diagram
232
+ {"boxes":[
233
+ {"id":"web","label":"CLIENT","text":"Web App","footer":"React","color":"blue"},
234
+ {"id":"api","label":"SERVICE","text":"API","footer":"Kotlin","color":"green"},
235
+ {"id":"db","type":"cylinder","text":"Postgres"}
236
+ ],"connectors":[
237
+ {"from":"web","to":"api","text":"REST"},
238
+ {"from":"api","to":"db"}
239
+ ]}
240
+ ```
241
+ ````
242
+
243
+ A decision with attach sides and elbow routing, flowing down:
244
+
245
+ ````md
246
+ ```diagram
247
+ {"direction":"down","boxes":[
248
+ {"id":"user","type":"actor","text":"User"},
249
+ {"id":"check","type":"diamond","text":"Valid?"},
250
+ {"id":"save","text":"Save","color":"green"},
251
+ {"id":"err","type":"note","text":"Show error","color":"red"}
252
+ ],"connectors":[
253
+ {"from":"user","to":"check"},
254
+ {"from":{"id":"check","side":"bottom"},"to":"save","text":"yes","routing":"elbow"},
255
+ {"from":{"id":"check","side":"right"},"to":{"id":"err","side":"left"},"text":"no","routing":"elbow"}
256
+ ]}
257
+ ```
258
+ ````
259
+
260
+ Programmatic use: `parseDrawingSkeleton(json)` parses a skeleton,
261
+ `expandSkeleton(skeleton)` returns `DrawingData`, and
262
+ `DRAWING_SKELETON_JSON_SCHEMA` is the JSON Schema. Types: `DrawingSkeleton`,
263
+ `SkeletonBox`, `SkeletonConnector`.
264
+
265
+ ## Concrete examples
119
266
 
120
267
  Two bound cards with a labeled arrow:
121
268
 
122
269
  ````md
123
270
  ```drawing
124
- {"version":1,"height":260,"shapes":[
271
+ {"version":2,"canvasHeight":260,"shapes":[
125
272
  {"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"},
126
273
  {"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API","footer":"Kotlin"},
127
- {"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"web","endBinding":"api","text":"REST"}
274
+ {"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":{"id":"web"},"endBinding":{"id":"api"},"text":"REST"}
128
275
  ]}
129
276
  ```
130
277
  ````
131
278
 
132
- An elbow arrow and a hand-routed multi-bend line:
279
+ An elbow arrow with attach sides, a database, and a hand-routed multi-bend
280
+ line:
133
281
 
134
282
  ````md
135
283
  ```drawing
136
- {"version":1,"height":300,"shapes":[
284
+ {"version":2,"canvasHeight":300,"canvasWidth":640,"shapes":[
137
285
  {"id":"a","type":"rect","x":40,"y":40,"w":150,"h":80,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"text":"A"},
138
- {"id":"b","type":"ellipse","x":420,"y":180,"w":150,"h":80,"stroke":"#e03131","fill":"#ffc9c9","strokeWidth":2,"text":"B"},
139
- {"id":"e1","type":"arrow","x":196,"y":80,"w":224,"h":140,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"a","endBinding":"b","routing":"elbow"},
140
- {"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}]}
286
+ {"id":"b","type":"cylinder","x":420,"y":180,"w":140,"h":110,"stroke":"#7048e8","fill":"#d0bfff","strokeWidth":2,"text":"B"},
287
+ {"id":"e1","type":"arrow","x":190,"y":80,"w":300,"h":100,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":{"id":"a","fixedPoint":[1,0.5]},"endBinding":{"id":"b","fixedPoint":[0.5,0]},"routing":"elbow"},
288
+ {"id":"e2","type":"line","x":115,"y":126,"w":375,"h":54,"stroke":"#1971c2","fill":"transparent","strokeWidth":2,"startBinding":{"id":"a"},"endBinding":{"id":"b"},"waypoints":[{"x":115,"y":260},{"x":490,"y":260}]}
141
289
  ]}
142
290
  ```
143
291
  ````
144
292
 
293
+ ## Mermaid export
294
+
295
+ The canvas has a **Copy as Mermaid** button. Programmatically,
296
+ `drawingToMermaid(data, { direction?: 'LR' | 'TD' })` returns a
297
+ `flowchart`:
298
+
299
+ - one node per box, in shape order: `rect` and `note` `["…"]`, `ellipse`
300
+ `(["…"])`, `diamond` `{"…"}`, `cylinder` `[("…")]`, `cloud` and `actor`
301
+ `(("…"))`, `queue` `[["…"]]`; node text joins `label`, `text` and
302
+ `footer`
303
+ - one edge per connector bound at both ends: `-->`, `<-->` (bidirectional),
304
+ `---` (line); connector `text` becomes `|text|`. Unbound connectors are
305
+ dropped
306
+ - a `style` line for every box with a non-default stroke or fill
307
+ - free `text` shapes as `%% note:` comments
308
+
309
+ When `direction` is omitted it is `LR` if most edges run horizontally,
310
+ otherwise `TD`.
311
+
145
312
  ## Generation checklist
146
313
 
147
- 1. One JSON object, single ```drawing fence, no comments or trailing commas.
148
- 2. Every `id` unique; every binding references an existing box id.
149
- 3. Boxes 120×70 when they carry text slots; leave ~60px between cards for
314
+ 1. Prefer a ```diagram skeleton. Fall back to ```drawing only when you edit
315
+ an existing concrete payload or need exact positions.
316
+ 2. One JSON object per fence, no comments or trailing commas.
317
+ 3. Every `id` unique; every binding references an existing box id.
318
+ 4. Concrete boxes: at least 120 x 70 when they carry text slots (use the
319
+ default sizes from the table); leave about 60px between cards for
150
320
  connectors and labels.
151
- 4. Box text goes in `label`/`text`/`footer` not in floating `text` shapes.
152
- 5. Connectors between cards should bind both ends; approximate endpoints are
153
- fine (the editor snaps them to borders).
154
- 6. `height` large enough to contain every shape plus ~20px margin.
321
+ 5. Box text goes in `label`/`text`/`footer`, not in floating `text` shapes.
322
+ `actor` only has `text`.
323
+ 6. Connectors between cards bind both ends as objects (`{"id":"web"}`).
324
+ Add an edge-midpoint `fixedPoint` when the attach edge matters; omit it
325
+ otherwise.
326
+ 7. Use `"routing":"elbow"` for flowcharts; leave `elbow` and `waypoints`
327
+ out unless you need to override the router.
328
+ 8. `canvasHeight` large enough to contain every shape plus ~20px margin.
package/README.md CHANGED
@@ -15,9 +15,12 @@ document outline, and collapsible sections.
15
15
  inline formatting), styled like Claude artifacts
16
16
  - **Table settings**: full/content width and row density per table;
17
17
  diagrams switch between full and content width too — Slab-style
18
- - **Diagrams**: a drawing canvas embedded in the document cards with
19
- attached text slots, bound arrows that follow their cards, elbow and
20
- multi-waypoint routing, labels, color palettes, dark mode
18
+ - **Diagrams**: a drawing canvas embedded in the document. Eight box
19
+ shapes (rectangle, ellipse, diamond, note, database, cloud, queue, actor)
20
+ with attached text slots, bound arrows that follow their boxes and attach
21
+ to a chosen side, auto-routed elbows that avoid other boxes, multi-select,
22
+ color palettes, dark mode, Mermaid export. LLMs author diagrams as a
23
+ coordinate-free ` ```diagram ` skeleton that the editor lays out
21
24
  - **Outline**: optional table-of-contents sidebar with click-to-scroll and
22
25
  current-section highlight
23
26
  - **Section folding**: collapse everything under a heading, view-layer only
@@ -217,29 +220,79 @@ table containing the selection, `null` outside tables), `canUndo`,
217
220
  ## Diagrams (drawing canvas)
218
221
 
219
222
  The toolbar's insert-drawing button embeds a canvas; the drawing persists
220
- in the markdown as a ` ```drawing ` fenced JSON block, fully specified in
221
- **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
223
+ in the markdown as a ` ```drawing ` fenced JSON block (format version 2),
224
+ fully specified in **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
222
225
 
223
- - **Shapes**: rectangle, ellipse, triangle, pentagon, arrow, line, text —
224
- with stroke/fill palettes. Draw by picking a tool and dragging.
226
+ - **Shapes**: rectangle, ellipse, diamond, note (sticky note), database
227
+ (cylinder), cloud, queue, actor (stick figure), arrow, line, text. The
228
+ toolbar shows the common tools and a "more shapes" popover for the rest.
229
+ Draw by picking a tool and dragging.
225
230
  - **Cards**: every box carries three text slots that move, resize, and wrap
226
- with it a bold **label** on top, **content** in the center, a dim
227
- **footer** at the bottom. Click a selected box (or double-click its
228
- top/middle/bottom strip, or press Enter) to edit a slot.
231
+ with it: a bold **label** on top, **content** in the center, a dim
232
+ **footer** at the bottom (actor has only the name under the figure).
233
+ Click a selected box (or double-click its top/middle/bottom strip, or
234
+ press Enter) to edit a slot.
229
235
  - **Bound connectors**: an arrow drawn from one card to another attaches to
230
- both moving a card moves its arrows, endpoints anchored to the border.
231
- Drag an endpoint off/onto a card to detach/re-attach.
232
- - **Routing**: straight (diagonal) by default; toggle **elbow** for right
233
- angles, or drag the dashed "+" handles on a selected connector to add any
234
- number of **waypoints** (they snap to neighbors' axes for clean 90°
235
- bends). One-way / two-way arrowhead toggle; midpoint **labels**.
236
+ both; moving a card moves its arrows, endpoints anchored to the outline.
237
+ An endpoint either auto-aims at the other end or sticks to a fixed point
238
+ on the box (a side, or a ratio inside the box). Drag an endpoint off/onto
239
+ a card to detach/re-attach.
240
+ - **Routing**: straight (diagonal) by default; toggle **elbow** for an
241
+ auto-routed right-angled path that leaves the box perpendicular to its
242
+ attach side and avoids the other boxes, or drag the dashed "+" handles on
243
+ a selected connector to add any number of **waypoints** (they snap to
244
+ neighbors' axes for clean 90° bends). One-way / two-way arrowhead toggle;
245
+ midpoint **labels**.
246
+ - **Multi-select**: shift-click, or drag a marquee on empty canvas, then
247
+ move, delete, or recolor the selection together. A contextual property
248
+ bar shows the options for whatever is selected.
249
+ - **Copy as Mermaid**: a canvas button copies the drawing as a Mermaid
250
+ `flowchart` (also available as `drawingToMermaid(data)`).
236
251
  - **Canvas**: resizable height, dot grid, white surface that inverts
237
- Excalidraw-style in dark mode (`.dark` ancestor class).
252
+ Excalidraw-style in dark mode (`.dark` ancestor class). An optional
253
+ `canvasWidth` scales the drawing to fit narrower layouts.
238
254
  - **Width**: the toggle at the right end of the canvas toolbar switches
239
255
  between **full width** (spans the editor) and **content width** (the
240
256
  canvas fits the rightmost shape and grows as shapes move). Stored as
241
257
  `"width":"content"` in the payload; omitted when full.
242
258
 
259
+ A stored drawing looks like this:
260
+
261
+ ````md
262
+ ```drawing
263
+ {"version":2,"canvasHeight":260,"shapes":[
264
+ {"id":"web","type":"rect","x":40,"y":70,"w":170,"h":100,"stroke":"#1971c2","fill":"#a5d8ff","strokeWidth":2,"label":"CLIENT","text":"Web App"},
265
+ {"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API"},
266
+ {"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":{"id":"web"},"endBinding":{"id":"api"},"text":"REST"}
267
+ ]}
268
+ ```
269
+ ````
270
+
271
+ ### The ` ```diagram ` skeleton
272
+
273
+ Generators (LLMs, scripts) do not need coordinates. A ` ```diagram ` block
274
+ holds a skeleton: boxes, connectors, colors. On import the editor lays it
275
+ out along `direction` (`right` by default, or `down`), sizes boxes to their
276
+ text, and stores the result as a normal ` ```drawing ` block (one-way
277
+ expansion).
278
+
279
+ ````md
280
+ ```diagram
281
+ {"boxes":[
282
+ {"id":"web","label":"CLIENT","text":"Web App","color":"blue"},
283
+ {"id":"api","label":"SERVICE","text":"API","color":"green"},
284
+ {"id":"db","type":"cylinder","text":"Postgres"}
285
+ ],"connectors":[
286
+ {"from":"web","to":"api","text":"REST"},
287
+ {"from":"api","to":"db","routing":"elbow"}
288
+ ]}
289
+ ```
290
+ ````
291
+
292
+ The full skeleton spec (attach sides, explicit positions, free texts) is
293
+ in [DRAWING_FORMAT.md](./DRAWING_FORMAT.md). Programmatic access:
294
+ `parseDrawingSkeleton`, `expandSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA`.
295
+
243
296
  ## Outline & section folding
244
297
 
245
298
  `outline` docks a collapsible table-of-contents sidebar: live heading list
@@ -256,14 +309,19 @@ remount; a folded section auto-expands if the cursor enters it.
256
309
  Documents are plain markdown, so LLMs can generate them — including
257
310
  diagrams:
258
311
 
259
- - **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative
260
- ` ```drawing ` payload spec, written to be pasted into a prompt (ships in
261
- the npm package next to this README).
262
- - `DRAWING_DATA_JSON_SCHEMA` (exported) is the same contract as JSON Schema
263
- use it to validate generated payloads or as a structured-output/tool
264
- schema.
265
- - `parseDrawingData(json)` is the editor's own lenient parser (invalid
266
- shapes drop out; never throws); `serializeDrawingData` is its inverse.
312
+ - Emit ` ```diagram ` skeleton blocks: no coordinates, the editor lays them
313
+ out. Use ` ```drawing ` (format v2) only to edit an existing concrete
314
+ payload.
315
+ - **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative spec of
316
+ both payloads, written to be pasted into a prompt (ships in the npm
317
+ package next to this README).
318
+ - `DRAWING_SKELETON_JSON_SCHEMA` and `DRAWING_DATA_JSON_SCHEMA` (exported)
319
+ are the same contracts as JSON Schema: use them to validate generated
320
+ payloads or as structured-output/tool schemas.
321
+ - `parseDrawingSkeleton(json)` / `expandSkeleton(skeleton)` turn a skeleton
322
+ into `DrawingData`. `parseDrawingData(json)` is the editor's own lenient
323
+ parser (invalid shapes drop out; never throws); `serializeDrawingData` is
324
+ its inverse. `drawingToMermaid(data)` exports a Mermaid `flowchart`.
267
325
  - A ready-made Claude Code skill lives in the monorepo at
268
326
  `.claude/skills/text-editor-documents/` — copy it into consuming repos so
269
327
  agents there know the dialect.
@@ -280,11 +338,14 @@ diagrams:
280
338
  | `BlockWidth`, `TableDensity`, `TableSettings`, `DEFAULT_TABLE_SETTINGS` | Table setting types |
281
339
  | `$getTableSettings`, `$setTableSettings`, `$getTableWidth`, `$setTableWidth`, `$getTableDensity`, `$setTableDensity`, `$getSelectedTable` | Table setting helpers (inside `editor.update`/`read`) |
282
340
  | `parseTableSettingsMarker`, `formatTableSettingsMarker` | Marker comment ⇄ `TableSettings` |
283
- | `DRAWING`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformer |
341
+ | `DRAWING`, `DIAGRAM`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformers (` ```drawing ` and ` ```diagram `) |
284
342
  | `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
285
343
  | `DrawingData`, `DrawingShape`, `DrawingShapeType` | Drawing payload types |
286
344
  | `parseDrawingData`, `serializeDrawingData` | Drawing payload (de)serialization |
287
345
  | `DRAWING_DATA_JSON_SCHEMA` | JSON Schema of the drawing payload |
346
+ | `DrawingSkeleton`, `SkeletonBox`, `SkeletonConnector` | Skeleton types |
347
+ | `parseDrawingSkeleton`, `expandSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA` | Skeleton parsing, expansion to `DrawingData`, JSON Schema |
348
+ | `drawingToMermaid` | Mermaid `flowchart` export |
288
349
 
289
350
  ## Form integration
290
351
 
@@ -305,13 +366,15 @@ ZUI convention: a `dark` class on `<html>`.
305
366
  - Source: `src/EditorRoot.tsx` (composer + plugins), `src/EditorContent.tsx`,
306
367
  `src/MarkdownEditor.tsx` (default composition), `src/useMarkdownEditor.ts`,
307
368
  `src/components/Toolbar.tsx`; plugins in `src/plugins/`; drawing
308
- canvas in `src/components/`; custom nodes in `src/nodes/`;
369
+ canvas in `src/drawing/`; custom nodes in `src/nodes/`;
309
370
  transformers in `src/transformers/`
310
371
  - Markdown import/export via `@lexical/markdown` transformers; order
311
- matters: `FRONTMATTER` and `DRAWING` claim their blocks before `CODE`,
312
- `CHECK_LIST` before `UNORDERED_LIST`
313
- - Drawing geometry (bindings resolver, elbow/waypoint paths, wrapping) is
314
- pure and lives in `src/components/drawingGeometry.ts`
372
+ matters: `FRONTMATTER`, `DRAWING` and `DIAGRAM` claim their blocks before
373
+ `CODE`, `CHECK_LIST` before `UNORDERED_LIST`
374
+ - `src/drawing/` keeps the format (`types.ts`, `schema.ts`), shape
375
+ definitions (`shapes/`), pure geometry (`geometry.ts`), the Mermaid
376
+ exporter (`mermaid.ts`) and the React canvas (`canvas/`) apart, so the
377
+ router, bindings and skeleton expansion run without a DOM
315
378
  - Tests: `pnpm test` runs a headless-Lexical markdown round-trip suite
316
379
  (`tests/roundtrip.mjs`)
317
380