@zuilib/text-editor 0.4.0 → 0.6.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,105 @@
1
1
  # Changelog — @zuilib/text-editor
2
2
 
3
+ ## 0.6.0
4
+
5
+ **Text measure and `text` block width.** Additive: markdown, JSON, class
6
+ names and props keep their meaning; with no configuration the layout is
7
+ unchanged.
8
+
9
+ - **Measure** (opt in): `measure="48rem"` on `MarkdownEditor` /
10
+ `MarkdownEditor.Root`, or `--zui-text-editor-measure: 48rem` in a
11
+ stylesheet — the CSS custom property is the contract, the prop is sugar.
12
+ Text blocks (paragraphs, headings, lists, quotes, code, frontmatter) are
13
+ capped at the measure and centred in the content pane; the placeholder
14
+ and fold chevrons follow the column. The pane itself is never capped and
15
+ full-width blocks stop short of the outline sidebar
16
+ - **`BlockWidth` gains `'text'`**: `'full' | 'text' | 'content'`. `text`
17
+ aligns a table or drawing with the text column; without a measure it is
18
+ visually identical to `full` (resolved in CSS through the variable's
19
+ fallback). `full` (default, omitted) and `content` (shrink to fit) are
20
+ unchanged
21
+ - **Persistence**: tables `<!-- width: text -->` (combinable with
22
+ `density`); drawings and ` ```diagram ` skeletons `"width":"text"`.
23
+ `DRAWING_DATA_JSON_SCHEMA`, `DRAWING_SKELETON_JSON_SCHEMA` and
24
+ `DRAWING_FORMAT.md` updated. Unknown width values are ignored (treated as
25
+ `full`) instead of being rejected, so documents from newer versions still
26
+ open — `parseTableSettingsMarker` now returns `{}` (rather than `null`)
27
+ for a marker whose only known key carries an unknown value, and the
28
+ transformer consumes that line
29
+ - **Insertion defaults**: `defaultBlockWidth={{ table?, drawing? }}` on
30
+ Root applies when the toolbar / `useMarkdownEditor()` inserts a new table
31
+ or drawing. The chosen width is written to the markdown explicitly, even
32
+ `full`, so the document reads the same in an app with other defaults; a
33
+ width named in the source (`<!-- width: full -->`, `"width":"full"`) is
34
+ preserved on round trip
35
+ - **UI / headless**: the floating table toolbar and the drawing toolbar
36
+ offer all three widths with distinct icons (`BLOCK_WIDTH_OPTIONS`).
37
+ `useMarkdownEditor()` gains `blockWidth` / `setBlockWidth` for the block
38
+ containing the selection — the focused drawing or the table the caret is
39
+ in; `insertTable` / `insertDrawing` accept a `width` option. `tableWidth`
40
+ / `setTableWidth` keep working and are deprecated
41
+ - **CSS**: the new layout lives in zero-specificity `:where()` rules. The
42
+ content pane exposes `--zui-text-editor-gutter` and
43
+ `--zui-text-editor-bleed`; every top-level block reads
44
+ `--zui-text-editor-block-bleed`, so one class rule overrides any block
45
+ kind. New block classes (added next to the existing utility classes):
46
+ `.zui-paragraph`, `.zui-heading` / `.zui-heading-1`…`6`, `.zui-list` /
47
+ `.zui-list-ordered` / `.zui-list-unordered`, `.zui-quote`, `.zui-code`;
48
+ `.zui-drawing-canvas` gains `.is-text-width`. Nothing is renamed
49
+ - New exports: `BLOCK_WIDTHS`, `isBlockWidth`, `BlockWidthDefaults`,
50
+ `TableSettingsOptions`, `$isTableWidthExplicit`; `$setTableSettings` and
51
+ `formatTableSettingsMarker` take an optional `{ explicitWidth }`
52
+ - **Adopting**: set `measure` (or the CSS variable) and delete any external
53
+ `max-width` / `margin-inline` overrides on `.zui-text-editor-content > *`
54
+
55
+ ## 0.5.0
56
+
57
+ **Diagram builder rewrite.** The drawing canvas, its payload format and the
58
+ authoring story for LLMs were rebuilt.
59
+
60
+ - **Format v2 (breaking)**: ` ```drawing ` payloads are now
61
+ `{"version":2,"canvasHeight":<px>,"canvasWidth"?:<px>,"width"?:"content","shapes":[...]}`.
62
+ No migration from version 1 and no legacy spellings: anything that is
63
+ not a version 2 payload renders as an empty canvas.
64
+ `canvasWidth` is optional: when set the drawing scales to fit narrower
65
+ layouts (SVG viewBox), when absent the canvas is fluid. `triangle` and
66
+ `pentagon` are removed. `DRAWING_DATA_JSON_SCHEMA` and
67
+ `DRAWING_FORMAT.md` rewritten for v2
68
+ - **Shapes**: boxes `rect`, `ellipse`, `diamond`, `note` (sticky note with
69
+ a folded corner, default fill `#ffec99`), `cylinder` (database), `cloud`,
70
+ `queue` (horizontal cylinder), `actor` (stick figure with a name under
71
+ it, `text` slot only); connectors `arrow`, `line`; free `text`. Palette
72
+ gains purple (`#7048e8` / `#d0bfff`)
73
+ - **Bindings** are objects: `{ id, fixedPoint?: [fx, fy], mode?: "orbit" |
74
+ "inside" }`. `fixedPoint` is a ratio in the box's bounding box; omitted,
75
+ the endpoint auto-aims from the box center at the other end. `orbit`
76
+ (default) projects the endpoint onto the outline with a 6px gap,
77
+ `inside` pins it exactly. The `side: "top" | "right" | "bottom" |
78
+ "left"` shorthand exists only in the ` ```diagram ` skeleton
79
+ - **Router**: `routing: "elbow"` is auto-routed. It leaves the box
80
+ perpendicular to the attach side, avoids every box on the canvas and
81
+ minimises bends. `elbow` (0 to 1) remains as an override of the middle
82
+ segment when the path is a simple three-segment Z; `waypoints` still
83
+ take precedence
84
+ - **Editor UX**: multi-select (shift-click, marquee on empty canvas; move,
85
+ delete and recolor together), redesigned toolbar with a "more shapes"
86
+ popover, contextual property bar, smoother drag and selection visuals,
87
+ **Copy as Mermaid** button
88
+ - **` ```diagram ` skeleton**: a fenced block with a coordinate-free JSON
89
+ skeleton (`boxes`, `connectors`, `texts`, `direction`, palette `color`
90
+ names) that the editor auto-lays out and expands into a ` ```drawing `
91
+ block on import (one-way). New exports `expandSkeleton`,
92
+ `parseDrawingSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA`, `DIAGRAM`
93
+ transformer, types `DrawingSkeleton`, `SkeletonBox`, `SkeletonConnector`
94
+ - **Mermaid export**: `drawingToMermaid(data, { direction? })` returns a
95
+ `flowchart` with one node per box (shape-specific node syntax), edges for
96
+ connectors bound at both ends (labels as `|text|`), `style` lines for
97
+ coloured boxes and free text as `%% note:` comments
98
+ - Internal: the drawing code moved from `src/components/` into
99
+ `src/drawing/` (`types.ts`, `schema.ts`, `geometry.ts`, `mermaid.ts`,
100
+ `shapes/`, `canvas/`); shape geometry is data-driven so the router,
101
+ bindings, skeleton expansion and exporters run without a DOM
102
+
3
103
  ## 0.4.0
4
104
 
5
105
  - **Table density**, Slab-style: the floating table toolbar (caret in a
package/DRAWING_FORMAT.md CHANGED
@@ -1,43 +1,63 @@
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` is the block's horizontal sizing in the page, independent of the
45
+ coordinates: `"full"` (default, omit it) spans the editor pane;
46
+ `"text"` aligns the block with the text column (the same as `"full"`
47
+ unless the app sets a text measure); `"content"` sizes the block to fit
48
+ the rightmost shape (plus a margin), left-aligned with the text. Use
49
+ `"content"` for small diagrams that should not stretch across the page.
50
+ Unknown values are ignored (treated as `"full"`).
32
51
 
33
52
  ## Top-level object
34
53
 
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 |
54
+ | Field | Type | Notes |
55
+ |----------------|-----------|--------------------------------------------------------------|
56
+ | `version` | `2` | Literal `2` |
57
+ | `canvasHeight` | `number` | Canvas height in px (min 80) |
58
+ | `canvasWidth` | `number` | Optional logical width in px (min 120); scales to fit when set |
59
+ | `width` | `"full"` \| `"text"` \| `"content"` | Optional; default `"full"`. `"text"` aligns with the text column, `"content"` fits the shapes' extent |
60
+ | `shapes` | `Shape[]` | Render order: later shapes draw on top |
41
61
 
42
62
  ## Shape object
43
63
 
@@ -46,17 +66,33 @@ All shapes share these required fields:
46
66
  | Field | Type | Meaning |
47
67
  |---------------|----------|--------------------------------------------------------------------|
48
68
  | `id` | `string` | Unique within the drawing; bindings reference it |
49
- | `type` | `string` | `rect` \| `ellipse` \| `triangle` \| `pentagon` \| `arrow` \| `line` \| `text` |
69
+ | `type` | `string` | One of the types below |
50
70
  | `x`, `y` | `number` | Boxes/text: top-left of the bounding box. Connectors: start point. |
51
71
  | `w`, `h` | `number` | Boxes/text: size (non-negative). Connectors: delta to the end point (`end = (x+w, y+h)`; may be negative). |
52
72
  | `stroke` | `string` | CSS color of the outline and of all text on the shape |
53
73
  | `fill` | `string` | CSS color of the interior; `"transparent"` for none |
54
74
  | `strokeWidth` | `number` | Use `2` |
55
75
 
56
- ### Boxes (`rect`, `ellipse`, `triangle`, `pentagon`) — "cards"
76
+ ### Shape types
77
+
78
+ | Type | Kind | Default size | Notes |
79
+ |------------|-----------|--------------|--------------------------------------------------------------|
80
+ | `rect` | box | 160 x 90 | Rectangle; the general-purpose card |
81
+ | `ellipse` | box | 160 x 90 | Ellipse inscribed in the bounding box |
82
+ | `diamond` | box | 170 x 100 | Decision; text area is the inner half |
83
+ | `note` | box | 160 x 110 | Sticky note with a folded bottom-right corner; default fill `#ffec99` |
84
+ | `cylinder` | box | 140 x 110 | Database; text sits below the top ellipse |
85
+ | `cloud` | box | 180 x 110 | Cloud; outline is an ellipse for binding purposes |
86
+ | `queue` | box | 180 x 80 | Horizontal cylinder (queue, stream, pipe) |
87
+ | `actor` | box | 90 x 120 | Stick figure; `text` only, rendered as a name under the figure |
88
+ | `arrow` | connector | | Head at the end; `bidirectional` for both ends |
89
+ | `line` | connector | | No arrowhead |
90
+ | `text` | free text | | Floating annotation |
91
+
92
+ ### Boxes ("cards")
57
93
 
58
94
  Boxes are card-like: they can carry up to three text slots that render
59
- inside the shape and move/resize/wrap with it:
95
+ inside the shape and move, resize and wrap with it:
60
96
 
61
97
  | Field | Renders as |
62
98
  |----------|-----------------------------------------------|
@@ -64,12 +100,14 @@ inside the shape and move/resize/wrap with it:
64
100
  | `text` | Main content, centered |
65
101
  | `footer` | Small dim line at the bottom |
66
102
 
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.
103
+ Text wraps automatically to the box's text area; `\n` forces a line break.
104
+ The text area is smaller than the bounding box for non-rectangular shapes
105
+ (diamond, ellipse, cloud, cylinder), so give those a little more room.
106
+ `actor` has only the `text` slot: it is drawn under the figure and may be
107
+ wider than the box; `label` and `footer` are dropped. Prefer slots over
108
+ floating `text` shapes; slot text is attached to the card.
71
109
 
72
- ### `text` free-floating annotation
110
+ ### `text`: free-floating annotation
73
111
 
74
112
  `text` holds the content. `w`/`h` are advisory (the editor recomputes them
75
113
  from the content); position is the top-left of the first line. Use only for
@@ -85,70 +123,212 @@ Geometry: from `(x, y)` to `(x+w, y+h)`. `arrow` has a head at the end;
85
123
  |-----------------|------------|-------------------------------------------------------------------------|
86
124
  | `text` | `string` | Label rendered at the path midpoint on a small backing plate |
87
125
  | `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
126
+ | `startBinding` | `Binding` | Box the start attaches to (see below) |
127
+ | `endBinding` | `Binding` | Box the end attaches to |
128
+ | `routing` | `"elbow"` | Right-angled auto-routed path |
129
+ | `elbow` | `number` | 0 to 1: override of the middle segment, only for a simple three-segment Z |
130
+ | `waypoints` | `Point[]` | Explicit intermediate path points (`{x,y}`), ordered start to end |
131
+
132
+ ## Bindings
133
+
134
+ A binding attaches a connector endpoint to a box and keeps it attached as
135
+ the box moves:
136
+
137
+ ```json
138
+ { "id": "<box id>", "fixedPoint": [fx, fy], "mode": "orbit" }
139
+ ```
140
+
141
+ | Field | Type | Meaning |
142
+ |--------------|--------------------------|-------------------------------------------------------------------------|
143
+ | `id` | `string` | Required. Id of a box shape (bindings to unknown or non-box ids are dropped) |
144
+ | `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 |
145
+ | `mode` | `"orbit"` \| `"inside"` | Optional, default `"orbit"` |
146
+
147
+ - **Omit `fixedPoint`** when you do not care where the connector attaches:
148
+ the endpoint auto-aims from the box center at the other end of the
149
+ connector, so the line always meets the outline on the facing side.
150
+ - **Set `fixedPoint` to an edge midpoint** when the attach edge matters (a
151
+ decision's "yes" leaves at the bottom `[0.5,1]`, "no" at the right
152
+ `[1,0.5]`). In the ```diagram skeleton the same thing is spelled
153
+ `"side":"bottom"`.
154
+ - `mode: "orbit"` (default) projects the endpoint onto the box outline with
155
+ a 6px gap. `mode: "inside"` pins the endpoint exactly on the fixed point
156
+ (useful for lines that end inside a shape).
157
+
158
+ Bound endpoints are re-anchored by the editor, so the connector's own
159
+ `x`/`y`/`w`/`h` only need to be approximately right: place them near the
160
+ intended boxes and the resolver snaps them. Don't bind both ends of one
161
+ connector to the same box.
162
+
163
+ ## Routing
164
+
165
+ Omit `routing` for a straight (possibly diagonal) connector.
166
+
167
+ `"routing":"elbow"` renders an auto-routed orthogonal path: it leaves the
168
+ box perpendicular to the attach side, avoids every box on the canvas, and
169
+ uses as few bends as possible. `elbow` (0 to 1) is an optional override of
170
+ the middle segment's position along the span; it is applied only when the
171
+ routed path is a simple three-segment Z, and ignored otherwise.
172
+
173
+ If `waypoints` is non-empty it defines the path (`start, waypoints..., end`)
174
+ and `routing`/`elbow` are ignored. Waypoints are absolute canvas
106
175
  coordinates; align consecutive points on x or y to get right angles.
107
176
 
108
177
  ## Recommended colors
109
178
 
110
- Any CSS color works; the editor's palette (dark-mode safe the canvas
179
+ Any CSS color works; the editor's palette (dark-mode safe, the canvas
111
180
  inverts colors Excalidraw-style in dark themes):
112
181
 
113
- - Strokes: `#1e1e1e` (default), `#e03131` red, `#2f9e44` green, `#1971c2` blue, `#f08c00` orange
114
- - Fills: `transparent`, `#ffc9c9`, `#b2f2bb`, `#a5d8ff`, `#ffec99`
182
+ | Name | Stroke | Fill |
183
+ |--------|-----------|---------------|
184
+ | gray | `#1e1e1e` | `transparent` |
185
+ | red | `#e03131` | `#ffc9c9` |
186
+ | green | `#2f9e44` | `#b2f2bb` |
187
+ | blue | `#1971c2` | `#a5d8ff` |
188
+ | orange | `#f08c00` | `#ffec99` |
189
+ | purple | `#7048e8` | `#d0bfff` |
190
+
191
+ Pair a colored stroke with its pastel fill. `note` defaults to the yellow
192
+ fill `#ffec99`.
193
+
194
+ ## Prefer the ```diagram skeleton
195
+
196
+ A fenced block with language `diagram` holds a skeleton: what the boxes are
197
+ and how they connect. The editor expands it into a full ```drawing block
198
+ on import (auto layout, default sizes, palette colors). The expansion is
199
+ one-way: once opened, the document stores the concrete ```drawing block.
200
+
201
+ ```
202
+ {
203
+ "direction"?: "right" | "down", // auto-layout flow, default right
204
+ "canvasWidth"?: n, "canvasHeight"?: n, "width"?: "full" | "text" | "content",
205
+ "boxes": [ { "id", "type"?: <box type, default rect>, "label"?, "text"?, "footer"?,
206
+ "x"?, "y"?, // omitted: auto layout by connector rank
207
+ "w"?, "h"?, // omitted: sized to fit the text
208
+ "color"?: "gray"|"red"|"green"|"blue"|"orange"|"purple", // stroke + pastel fill
209
+ "stroke"?, "fill"? } ],
210
+ "connectors"?: [ { "id"?, "type"?: "arrow"|"line", "from": "<id>" | {"id","side"?}, "to": same,
211
+ "text"?, "bidirectional"?, "routing"?: "elbow"|"straight", "color"?, "stroke"? } ],
212
+ "texts"?: [ { "x", "y", "text", "color"? } ]
213
+ }
214
+ ```
215
+
216
+ Rules:
217
+
218
+ - `boxes` is required; every box needs a unique `id`. `type` is any box
219
+ type from the table above (default `rect`).
220
+ - `from`/`to` are box ids, or `{"id","side"}` to pick the attach edge.
221
+ - `color` picks a palette pair (stroke plus pastel fill) from the table
222
+ above. `stroke`/`fill` override it with explicit CSS colors. On
223
+ connectors only the stroke applies.
115
224
 
116
- Pair a colored stroke with its pastel fill (`#1971c2` + `#a5d8ff`, etc.).
225
+ - Auto layout: boxes are ranked along `direction` by the longest path from
226
+ the sources (cycles are broken). Ranks are 90px apart, boxes within a
227
+ rank 40px apart, starting at (32,32). Boxes with explicit `x`/`y` keep
228
+ them. Boxes without `w`/`h` are sized to fit their text. `canvasHeight`
229
+ is computed when omitted.
230
+ - `texts` are free annotations and need explicit coordinates.
231
+ - `width` is copied to the expanded drawing (see [Coordinate
232
+ system](#coordinate-system)); omit it for `"full"`.
117
233
 
118
- ## Examples
234
+ A three-box flow:
235
+
236
+ ````md
237
+ ```diagram
238
+ {"boxes":[
239
+ {"id":"web","label":"CLIENT","text":"Web App","footer":"React","color":"blue"},
240
+ {"id":"api","label":"SERVICE","text":"API","footer":"Kotlin","color":"green"},
241
+ {"id":"db","type":"cylinder","text":"Postgres"}
242
+ ],"connectors":[
243
+ {"from":"web","to":"api","text":"REST"},
244
+ {"from":"api","to":"db"}
245
+ ]}
246
+ ```
247
+ ````
248
+
249
+ A decision with attach sides and elbow routing, flowing down:
250
+
251
+ ````md
252
+ ```diagram
253
+ {"direction":"down","boxes":[
254
+ {"id":"user","type":"actor","text":"User"},
255
+ {"id":"check","type":"diamond","text":"Valid?"},
256
+ {"id":"save","text":"Save","color":"green"},
257
+ {"id":"err","type":"note","text":"Show error","color":"red"}
258
+ ],"connectors":[
259
+ {"from":"user","to":"check"},
260
+ {"from":{"id":"check","side":"bottom"},"to":"save","text":"yes","routing":"elbow"},
261
+ {"from":{"id":"check","side":"right"},"to":{"id":"err","side":"left"},"text":"no","routing":"elbow"}
262
+ ]}
263
+ ```
264
+ ````
265
+
266
+ Programmatic use: `parseDrawingSkeleton(json)` parses a skeleton,
267
+ `expandSkeleton(skeleton)` returns `DrawingData`, and
268
+ `DRAWING_SKELETON_JSON_SCHEMA` is the JSON Schema. Types: `DrawingSkeleton`,
269
+ `SkeletonBox`, `SkeletonConnector`.
270
+
271
+ ## Concrete examples
119
272
 
120
273
  Two bound cards with a labeled arrow:
121
274
 
122
275
  ````md
123
276
  ```drawing
124
- {"version":1,"height":260,"shapes":[
277
+ {"version":2,"canvasHeight":260,"shapes":[
125
278
  {"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
279
  {"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"}
280
+ {"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
281
  ]}
129
282
  ```
130
283
  ````
131
284
 
132
- An elbow arrow and a hand-routed multi-bend line:
285
+ An elbow arrow with attach sides, a database, and a hand-routed multi-bend
286
+ line:
133
287
 
134
288
  ````md
135
289
  ```drawing
136
- {"version":1,"height":300,"shapes":[
290
+ {"version":2,"canvasHeight":300,"canvasWidth":640,"shapes":[
137
291
  {"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}]}
292
+ {"id":"b","type":"cylinder","x":420,"y":180,"w":140,"h":110,"stroke":"#7048e8","fill":"#d0bfff","strokeWidth":2,"text":"B"},
293
+ {"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"},
294
+ {"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
295
  ]}
142
296
  ```
143
297
  ````
144
298
 
299
+ ## Mermaid export
300
+
301
+ The canvas has a **Copy as Mermaid** button. Programmatically,
302
+ `drawingToMermaid(data, { direction?: 'LR' | 'TD' })` returns a
303
+ `flowchart`:
304
+
305
+ - one node per box, in shape order: `rect` and `note` `["…"]`, `ellipse`
306
+ `(["…"])`, `diamond` `{"…"}`, `cylinder` `[("…")]`, `cloud` and `actor`
307
+ `(("…"))`, `queue` `[["…"]]`; node text joins `label`, `text` and
308
+ `footer`
309
+ - one edge per connector bound at both ends: `-->`, `<-->` (bidirectional),
310
+ `---` (line); connector `text` becomes `|text|`. Unbound connectors are
311
+ dropped
312
+ - a `style` line for every box with a non-default stroke or fill
313
+ - free `text` shapes as `%% note:` comments
314
+
315
+ When `direction` is omitted it is `LR` if most edges run horizontally,
316
+ otherwise `TD`.
317
+
145
318
  ## Generation checklist
146
319
 
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
320
+ 1. Prefer a ```diagram skeleton. Fall back to ```drawing only when you edit
321
+ an existing concrete payload or need exact positions.
322
+ 2. One JSON object per fence, no comments or trailing commas.
323
+ 3. Every `id` unique; every binding references an existing box id.
324
+ 4. Concrete boxes: at least 120 x 70 when they carry text slots (use the
325
+ default sizes from the table); leave about 60px between cards for
150
326
  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.
327
+ 5. Box text goes in `label`/`text`/`footer`, not in floating `text` shapes.
328
+ `actor` only has `text`.
329
+ 6. Connectors between cards bind both ends as objects (`{"id":"web"}`).
330
+ Add an edge-midpoint `fixedPoint` when the attach edge matters; omit it
331
+ otherwise.
332
+ 7. Use `"routing":"elbow"` for flowcharts; leave `elbow` and `waypoints`
333
+ out unless you need to override the router.
334
+ 8. `canvasHeight` large enough to contain every shape plus ~20px margin.