@zuilib/text-editor 0.3.1 → 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,7 +1,70 @@
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
 
53
+ - **Table density**, Slab-style: the floating table toolbar (caret in a
54
+ table) gains *compact* / *comfortable* (default) / *spacious* row padding
55
+ and font size, next to the width toggle
56
+ - The settings comment above a GFM table now lists every non-default
57
+ setting, e.g. `<!-- width: content; density: compact -->`; the 0.3.1
58
+ `<!-- width: content -->` form is unchanged for width-only tables
59
+ - `useMarkdownEditor()` gains `tableDensity` / `setTableDensity`
60
+ - New exports: `TableDensity`, `TableSettings`, `DEFAULT_TABLE_SETTINGS`,
61
+ `$getTableSettings` / `$setTableSettings`, `$getTableDensity` /
62
+ `$setTableDensity`, `parseTableSettingsMarker` /
63
+ `formatTableSettingsMarker`. `TABLE_WIDTH_MARKER` is deprecated in favour
64
+ of `formatTableSettingsMarker`
65
+
66
+ ## 0.3.1
67
+
5
68
  - **Block width** for tables and drawings: switch any table or drawing
6
69
  between *full width* (spans the editor, the previous behaviour) and
7
70
  *content width* (shrinks to fit its columns / shapes, left-aligned with
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.