agentic-canvas 0.10.0 → 0.11.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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  A shared whiteboard for diagrams, drawn by people and agents.
9
9
 
10
- > Status: 0.10.0. The board model, the agent operations, the exports, the CLI, the
10
+ > Status: 0.11.0. The board model, the agent operations, the exports, the CLI, the
11
11
  > MCP server, a small Worker API, and live co-editing work. The CLI, the MCP server,
12
12
  > and the SDK can join a live room. A remote client calls the Worker API. A small web UI, served by the Worker, is new in 0.4.0.
13
13
  > The package exports `agentic-canvas/ui`, a board view you can mount in your own app, new in 0.5.0.
@@ -283,6 +283,39 @@ server still checks every write against the token. The host page owns the theme.
283
283
  For a React app, `BoardEditor` is the same component without a root. Wrap it in the providers of
284
284
  `agentic-design/react`.
285
285
 
286
+ ### Use your own Yjs document
287
+
288
+ An app that keeps the board in its own Yjs room gives it as `host`. The UI then reads and writes that document as a
289
+ board (its `elements` and `meta` maps, the shape this package stores) and redraws as soon as the document changes.
290
+ It makes no HTTP call, does not poll, and opens no live room. `client` is not needed. The default is still client
291
+ mode: without `host`, the UI works as described above.
292
+
293
+ ```ts
294
+ const board = mount(element, {
295
+ boardId: "b1",
296
+ principal: { kind: "human", name: "Ada", id: "ada", role: "editor" }, // id is stamped on what Ada writes; the default is name
297
+ host: {
298
+ doc, // a Y.Doc
299
+ presence: sync, // optional: a SyncClient of agentic-realtime, or { peers, onPeers }
300
+ status: () => status, // optional: "connecting" | "connected" | "disconnected"
301
+ onStatus: (listener) => stop, // optional: tell the UI when the status changes
302
+ },
303
+ });
304
+ ```
305
+
306
+ - Use the same `yjs` copy as the UI. A document from another copy is refused with a `TypeError`.
307
+ - Each element the UI creates or edits is stamped with the principal's `id` and `kind`. A delete has no stamp. The `id` follows the identity rule of the
308
+ SDK (letters, digits, `.`, `_`, `-`). An agent kind marks its elements as machine-made. A refused write shows a message
309
+ and leaves the document alone. The server does not check these writes: your app and its room decide who may write.
310
+ - In host mode `boardId` only keys the view. The UI shows the one board that the document holds (its `elements` and `meta` maps), so keep one board in each document.
311
+ - A document pushed past the element cap (10,000) can no longer be read. If that happens after the first draw, the UI keeps the last board it drew. At mount it shows the error page with Retry. Your app should stop a peer from writing that far.
312
+ - `client` or `room` together with `host` throws a `TypeError`, so a board never half-writes to a server. (`BoardEditor` shows an alert for these, as it does for a bad principal.) Give presence in `host.presence`.
313
+ - The principal's `id` (default: `name`) must pass the identity rule, so a display name with a space needs an explicit `id`. A bad one throws a `TypeError` at mount.
314
+ - The UI redraws only for changes in the document's `elements` and `meta` maps. Other data in the same document is left alone.
315
+ - `status` and `onStatus` are read when the UI opens `doc`. A new function on `update()` does not open it again. `onStatus` alone starts as not live, until it reports "connected". With neither, the board shows as live. Give `onStatus` at mount to follow later changes. It is not picked up by a later `update()` for the same `doc`.
316
+ - With `presence`, the UI shows the peers. It never sends a cursor, and never closes your room or your document.
317
+ - On `unmount` the UI removes its observers from your document and leaves it as it is.
318
+
286
319
  Develop and test the UI:
287
320
 
288
321
  ```sh
@@ -291,7 +324,7 @@ npx playwright-core install chromium
291
324
  npm run test:e2e # Playwright against `wrangler dev`, with axe (WCAG 2.2 AA)
292
325
  ```
293
326
 
294
- The E2E run creates a board, adds a shape and a note, moves them, reloads, and checks each view
327
+ `E2E_PORT` sets the port of the E2E server (default 18900). The E2E run creates a board, adds a shape and a note, moves them, reloads, and checks each view
295
328
  with axe in the light and the dark theme. It saves screenshots to `test-results/screens/`. The committed set in `docs/ui/` shows the UI at this release. Run `UPDATE_SCREENSHOTS=1 npm run test:e2e` to refresh it.
296
329
 
297
330
  A browser cannot set a header on a WebSocket, so the UI sends its token to the live room as a WebSocket sub-protocol (`Sec-WebSocket-Protocol`). A proxy that logs request headers can record it. The API itself takes the token only in an `Authorization` header, and the sub-protocol only on a WebSocket upgrade.
@@ -408,6 +441,7 @@ old author stays on its elements. Only new writes follow the strict rule.
408
441
  | `mount(element, options)` | Render the board UI into an element. It returns `update(options)` and `unmount()`. See Mount the board in your app. |
409
442
  | `BoardEditor(options)` | The same board UI as a React component, without a root. |
410
443
  | `BoardEditorOptions` (type), `BoardPrincipal` (type), `MountedBoard` (type) | Types for the options, the principal, and the object that `mount` returns. |
444
+ | `HostBoard` (type) | The `host` option: `doc`, and optionally `presence`, `status`, and `onStatus`. |
411
445
  | `CanvasClient` (type), `RoomOptions` (type) | The client type, as in `agentic-canvas/client`, and the live room's base URL and token for the `room` option. |
412
446
 
413
447
  The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-design/styles.css` too, for the tokens.
@@ -3,10 +3,10 @@ import {
3
3
  MAX_LIVE_BYTES,
4
4
  isBoardId,
5
5
  isStoreId
6
- } from "./chunk-M74ENUWR.js";
6
+ } from "./chunk-Z6KLGAFP.js";
7
7
  import {
8
8
  CanvasError
9
- } from "./chunk-ZSPZMGHG.js";
9
+ } from "./chunk-MVWUIBGV.js";
10
10
 
11
11
  // src/worker.ts
12
12
  import { createStoreAuth, hasScope as hasScope2, parsePrincipal, parseRegion } from "agentic-identity";
@@ -0,0 +1,290 @@
1
+ import {
2
+ COLOR_PATTERN,
3
+ CanvasError,
4
+ IMAGE_PATTERN,
5
+ MAX_ELEMENTS,
6
+ SHAPE_TYPES
7
+ } from "./chunk-MVWUIBGV.js";
8
+
9
+ // src/core/flow.ts
10
+ import dagre from "@dagrejs/dagre";
11
+ import * as Y from "yjs";
12
+ var MAX_FLOW_NODES = 500;
13
+ var MAX_FLOW_EDGES = 2e3;
14
+ var MAX_FLOW_CHARS = 2e5;
15
+ var NODE = String.raw`([A-Za-z0-9_-]{1,48})(?:\[([^\]]*)\]|\(([^)]*)\)|\{([^}]*)\})?`;
16
+ var EDGE = new RegExp(String.raw`^${NODE}\s*->\s*${NODE}\s*(?::(.*))?$`);
17
+ var ALONE = new RegExp(String.raw`^${NODE}$`);
18
+ var SHAPES = ["rect", "ellipse", "diamond"];
19
+ function parseFlow(source) {
20
+ if (source.length > MAX_FLOW_CHARS) throw new CanvasError("LIMIT", `flow text is over ${MAX_FLOW_CHARS} characters`);
21
+ const graph = { direction: "TB", nodes: [], edges: [] };
22
+ const byId = /* @__PURE__ */ new Map();
23
+ const fail = (line, message) => {
24
+ throw new CanvasError("INVALID", `line ${line}: ${message}`);
25
+ };
26
+ const node = (line, m, at) => {
27
+ const id = m[at];
28
+ if (/^edge_\d+$/.test(id)) fail(line, `"${id}" is reserved for connectors. Pick another node id.`);
29
+ const brackets = m.slice(at + 1, at + 4);
30
+ const open = brackets.findIndex((b) => b !== void 0);
31
+ const shape = SHAPES[Math.max(open, 0)];
32
+ const text = (open < 0 ? "" : brackets[open].trim()) || id;
33
+ const known = byId.get(id);
34
+ if (!known) {
35
+ const created = { id, text, shape, explicit: open >= 0 };
36
+ byId.set(id, created);
37
+ graph.nodes.push(created);
38
+ } else if (open >= 0) {
39
+ if (known.explicit && (known.text !== text || known.shape !== shape)) fail(line, `"${id}" is defined twice with different text`);
40
+ Object.assign(known, { text, shape, explicit: true });
41
+ }
42
+ return id;
43
+ };
44
+ source.replace(/^/, "").split(/\r?\n/).forEach((raw, index) => {
45
+ const line = index + 1;
46
+ const text = raw.trim();
47
+ if (text === "" || text.startsWith("#") || text.startsWith("//")) return;
48
+ if (/[\u0000-\u0008\u000a-\u001f\u007f]/.test(text)) fail(line, "control characters are not allowed");
49
+ const direction = /^direction\s+(\S+)$/.exec(text);
50
+ if (direction) {
51
+ if (direction[1] !== "TB" && direction[1] !== "LR") fail(line, `direction must be TB or LR, got "${direction[1]}"`);
52
+ graph.direction = direction[1];
53
+ return;
54
+ }
55
+ const edge = EDGE.exec(text);
56
+ if (edge) {
57
+ const from = node(line, edge, 1);
58
+ const to = node(line, edge, 5);
59
+ graph.edges.push({ from, to, text: (edge[9] ?? "").trim() });
60
+ } else {
61
+ const alone = ALONE.exec(text);
62
+ if (!alone) fail(line, `cannot read "${text.slice(0, 40)}". Use "A -> B: label".`);
63
+ else node(line, alone, 1);
64
+ }
65
+ if (graph.nodes.length > MAX_FLOW_NODES) throw new CanvasError("LIMIT", `a flow holds at most ${MAX_FLOW_NODES} nodes`);
66
+ if (graph.edges.length > MAX_FLOW_EDGES) throw new CanvasError("LIMIT", `a flow holds at most ${MAX_FLOW_EDGES} edges`);
67
+ });
68
+ if (graph.nodes.length === 0) throw new CanvasError("INVALID", "the flow has no nodes");
69
+ return { ...graph, nodes: graph.nodes.map(({ id, text, shape }) => ({ id, text, shape })) };
70
+ }
71
+ function sizeOf(n) {
72
+ const w = Math.min(360, Math.max(120, 24 + 8 * n.text.length));
73
+ return n.shape === "diamond" ? { w: Math.min(360, Math.round(w * 1.5)), h: 90 } : { w, h: 60 };
74
+ }
75
+ function layoutFlow(graph) {
76
+ const g = new dagre.graphlib.Graph({ multigraph: true });
77
+ g.setGraph({ rankdir: graph.direction, nodesep: 40, ranksep: 70, marginx: 0, marginy: 0 });
78
+ g.setDefaultEdgeLabel(() => ({}));
79
+ const index = new Map(graph.nodes.map((n, i) => [n.id, `n${i}`]));
80
+ const sizes = graph.nodes.map(sizeOf);
81
+ sizes.forEach(({ w, h }, i) => g.setNode(`n${i}`, { width: w, height: h }));
82
+ graph.edges.forEach((e, i) => g.setEdge(index.get(e.from), index.get(e.to), {}, String(i)));
83
+ dagre.layout(g);
84
+ const boxes = graph.nodes.map((n, i) => {
85
+ const { w, h } = sizes[i];
86
+ const c = g.node(`n${i}`);
87
+ return { id: n.id, x: Math.round(c.x - w / 2), y: Math.round(c.y - h / 2), w, h };
88
+ });
89
+ const minX = Math.min(...boxes.map((b) => b.x));
90
+ const minY = Math.min(...boxes.map((b) => b.y));
91
+ return boxes.map((b) => ({ ...b, x: b.x - minX, y: b.y - minY }));
92
+ }
93
+ function drawFlow(board, source, actor, options = {}) {
94
+ const prefix = options.idPrefix ?? `${crypto.randomUUID().slice(0, 8)}-`;
95
+ if (!/^[A-Za-z0-9_-]{0,16}$/.test(prefix)) throw new CanvasError("INVALID", "idPrefix must be up to 16 of A-Z, a-z, 0-9, _ and -");
96
+ const graph = parseFlow(source);
97
+ const placed = layoutFlow(graph);
98
+ const shapes = board.list().flatMap((e) => e.type === "connector" || e.type === "group" ? [] : [e]);
99
+ const origin = options.origin ?? {
100
+ x: shapes.length ? Math.min(...shapes.map((s) => s.x)) : 0,
101
+ y: shapes.length ? Math.max(...shapes.map((s) => s.y + s.h)) + 80 : 0
102
+ };
103
+ const work = board.clone();
104
+ const text = new Map(graph.nodes.map((n) => [n.id, n]));
105
+ for (const p of placed) {
106
+ const n = text.get(p.id);
107
+ work.addShape({ id: prefix + p.id, type: n.shape, text: n.text, x: p.x + origin.x, y: p.y + origin.y, w: p.w, h: p.h }, actor);
108
+ }
109
+ const edges = graph.edges.map((e, i) => {
110
+ const id = `${prefix}edge_${i + 1}`;
111
+ work.addConnector({ id, from: { id: prefix + e.from }, to: { id: prefix + e.to }, text: e.text }, actor);
112
+ return id;
113
+ });
114
+ Y.applyUpdate(board.doc, Y.encodeStateAsUpdate(work.doc, Y.encodeStateVector(board.doc)));
115
+ return { nodes: placed.map((p) => prefix + p.id), edges };
116
+ }
117
+
118
+ // src/core/svg.ts
119
+ var LINE = 17;
120
+ var num = (n) => String(Math.round(n * 100) / 100);
121
+ var esc = (value) => (typeof value === "string" ? value : "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f￾￿]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
122
+ var paint = (value, fallback) => typeof value === "string" && COLOR_PATTERN.test(value) ? value : fallback;
123
+ var str = (value) => typeof value === "string" ? value : "";
124
+ var within = (n, low, high) => typeof n === "number" && Number.isFinite(n) && n >= low && n <= high;
125
+ var LIMIT_COORD = 1e7;
126
+ var TYPES = /* @__PURE__ */ new Set([...SHAPE_TYPES, "connector", "group"]);
127
+ var sane = (s) => within(s.x, -LIMIT_COORD, LIMIT_COORD) && within(s.y, -LIMIT_COORD, LIMIT_COORD) && within(s.w, 1e-6, LIMIT_COORD) && within(s.h, 1e-6, LIMIT_COORD);
128
+ var isShape = (v) => "w" in v;
129
+ var center = (s) => ({ x: s.x + s.w / 2, y: s.y + s.h / 2 });
130
+ function edgePoint(s, toward) {
131
+ const c = center(s);
132
+ const dx = toward.x - c.x;
133
+ const dy = toward.y - c.y;
134
+ if (dx === 0 && dy === 0) return c;
135
+ const a = s.w / 2;
136
+ const b = s.h / 2;
137
+ const k = s.type === "ellipse" ? 1 / Math.hypot(dx / a, dy / b) : s.type === "diamond" ? 1 / (Math.abs(dx) / a + Math.abs(dy) / b) : Math.min(a / Math.abs(dx), b / Math.abs(dy));
138
+ return { x: c.x + dx * k, y: c.y + dy * k };
139
+ }
140
+ function wrap(text, width) {
141
+ const max = Math.max(4, Math.floor((width - 16) / 7.5));
142
+ const out = [];
143
+ for (const paragraph of text.split(/\r?\n/)) {
144
+ let line = "";
145
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
146
+ const chars = [...word];
147
+ while (chars.length > max) {
148
+ if (line) out.push(line);
149
+ line = "";
150
+ out.push(chars.splice(0, max).join(""));
151
+ }
152
+ const rest = chars.join("");
153
+ if (line && [...line].length + 1 + chars.length > max) {
154
+ out.push(line);
155
+ line = rest;
156
+ } else line = line ? `${line} ${rest}` : rest;
157
+ }
158
+ out.push(line);
159
+ }
160
+ return out.slice(0, 40);
161
+ }
162
+ function label(text, cx, cy, width, extra = "") {
163
+ if (text === "") return "";
164
+ const lines = wrap(text, width);
165
+ const top = cy - (lines.length - 1) * LINE / 2;
166
+ const spans = lines.map((l, i) => `<tspan x="${num(cx)}" y="${num(top + i * LINE)}">${esc(l)}</tspan>`);
167
+ return `<text text-anchor="middle" dominant-baseline="central"${extra}>${spans.join("")}</text>`;
168
+ }
169
+ function shapeBody(s) {
170
+ const { x, y, w, h } = s;
171
+ const stroke = paint(s.stroke, "#1f2937");
172
+ const look = (fill, more = "") => `fill="${paint(s.fill, fill)}" stroke="${stroke}" stroke-width="2"${more}`;
173
+ const text = label(str(s.text), x + w / 2, y + h / 2, w);
174
+ switch (s.type) {
175
+ case "ellipse":
176
+ return `<ellipse cx="${num(x + w / 2)}" cy="${num(y + h / 2)}" rx="${num(w / 2)}" ry="${num(h / 2)}" ${look("#ffffff")}/>${text}`;
177
+ case "diamond": {
178
+ const pts = [[x + w / 2, y], [x + w, y + h / 2], [x + w / 2, y + h], [x, y + h / 2]];
179
+ return `<polygon points="${pts.map(([px, py]) => `${num(px)},${num(py)}`).join(" ")}" ${look("#ffffff")}/>${text}`;
180
+ }
181
+ case "text":
182
+ return text;
183
+ case "frame":
184
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" ${look("none", ' stroke-dasharray="8 6"')}/>${label(str(s.text), x + w / 2, y - 12, w)}`;
185
+ case "image":
186
+ if (typeof s.src === "string" && s.src.startsWith("data:") && IMAGE_PATTERN.test(s.src)) {
187
+ return `<image href="${s.src}" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" preserveAspectRatio="xMidYMid meet"/>`;
188
+ }
189
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" ${look("#f3f4f6", ' stroke-dasharray="4 4"')}/>${label("image", x + w / 2, y + h / 2, w)}`;
190
+ default:
191
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${s.type === "sticky" ? 2 : 6}" ${look(s.type === "sticky" ? "#fde68a" : "#ffffff")}/>${text}`;
192
+ }
193
+ }
194
+ var endKey = (e) => "id" in e ? e.id : `${e.x},${e.y}`;
195
+ var wellFormed = (e) => typeof e === "object" && e !== null && (typeof e.id === "string" || within(e.x, -LIMIT_COORD, LIMIT_COORD) && within(e.y, -LIMIT_COORD, LIMIT_COORD));
196
+ function connectorBody(c, shapes, lane) {
197
+ const resolve = (e) => "id" in e ? shapes.get(e.id) : e;
198
+ const a = resolve(c.from);
199
+ const b = resolve(c.to);
200
+ if (!a || !b) return "";
201
+ let d;
202
+ let at;
203
+ if (isShape(a) && a === b) {
204
+ const x0 = a.x + a.w * 0.35;
205
+ const x1 = a.x + a.w * 0.65;
206
+ const lift = 45 + 32 * lane.index;
207
+ d = `M${num(x0)} ${num(a.y)}C${num(x0)} ${num(a.y - lift)} ${num(x1)} ${num(a.y - lift)} ${num(x1)} ${num(a.y)}`;
208
+ at = { x: a.x + a.w / 2, y: a.y - lift * 0.75 - 6 };
209
+ } else {
210
+ const ca = isShape(a) ? center(a) : a;
211
+ const cb = isShape(b) ? center(b) : b;
212
+ const flip = endKey(c.from) > endKey(c.to);
213
+ const dx = (flip ? ca.x - cb.x : cb.x - ca.x) || 0;
214
+ const dy = (flip ? ca.y - cb.y : cb.y - ca.y) || 0;
215
+ const len = Math.hypot(dx, dy) || 1;
216
+ const offset = (lane.index - (lane.count - 1) / 2) * 44;
217
+ const mid = { x: (ca.x + cb.x) / 2, y: (ca.y + cb.y) / 2 };
218
+ const ctrl = { x: mid.x - dy / len * 2 * offset, y: mid.y + dx / len * 2 * offset };
219
+ const p = isShape(a) ? edgePoint(a, ctrl) : a;
220
+ const q = isShape(b) ? edgePoint(b, ctrl) : b;
221
+ d = offset === 0 ? `M${num(p.x)} ${num(p.y)}L${num(q.x)} ${num(q.y)}` : `M${num(p.x)} ${num(p.y)}Q${num(ctrl.x)} ${num(ctrl.y)} ${num(q.x)} ${num(q.y)}`;
222
+ at = offset === 0 ? { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 } : { x: (p.x + q.x) / 4 + ctrl.x / 2, y: (p.y + q.y) / 4 + ctrl.y / 2 };
223
+ }
224
+ const marks = `${c.startArrow ? ' marker-start="url(#arrow)"' : ""}${c.endArrow ? ' marker-end="url(#arrow)"' : ""}`;
225
+ const halo = ' stroke="#ffffff" stroke-width="4" paint-order="stroke"';
226
+ return `<path d="${d}" fill="none" stroke="${paint(c.stroke, "#1f2937")}" stroke-width="2"${marks}/>${label(str(c.text), at.x, at.y, 160, halo)}`;
227
+ }
228
+ var drawable = (el) => TYPES.has(el.type) && (el.type === "connector" ? wellFormed(el.from) && wellFormed(el.to) : el.type === "group" || sane(el));
229
+ function toSvg(board) {
230
+ const listed = board.list();
231
+ if (listed.length > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
232
+ const all = listed.filter((el) => drawable(el));
233
+ const shapes = /* @__PURE__ */ new Map();
234
+ const box = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
235
+ const grow = (x, y) => {
236
+ box.minX = Math.min(box.minX, x);
237
+ box.minY = Math.min(box.minY, y);
238
+ box.maxX = Math.max(box.maxX, x);
239
+ box.maxY = Math.max(box.maxY, y);
240
+ };
241
+ for (const el of all) {
242
+ if (el.type === "connector") {
243
+ for (const e of [el.from, el.to]) if (!("id" in e)) grow(e.x, e.y);
244
+ } else if (el.type !== "group") {
245
+ shapes.set(el.id, el);
246
+ grow(el.x, el.y);
247
+ grow(el.x + el.w, el.y + el.h);
248
+ }
249
+ }
250
+ const pad = 80;
251
+ const any = box.minX !== Infinity;
252
+ const minX = any ? box.minX - pad : 0;
253
+ const minY = any ? box.minY - pad : 0;
254
+ const width = any ? Math.ceil(box.maxX + pad - minX) : 240;
255
+ const height = any ? Math.ceil(box.maxY + pad - minY) : 120;
256
+ const lanes = /* @__PURE__ */ new Map();
257
+ const groups = /* @__PURE__ */ new Map();
258
+ for (const el of all) {
259
+ if (el.type !== "connector") continue;
260
+ const key = [endKey(el.from), endKey(el.to)].sort().join("|");
261
+ groups.set(key, [...groups.get(key) ?? [], el.id]);
262
+ }
263
+ for (const ids of groups.values()) ids.forEach((id, index) => lanes.set(id, { index, count: ids.length }));
264
+ const body = all.map((el) => {
265
+ if (el.type === "group") return "";
266
+ const inner = el.type === "connector" ? connectorBody(el, shapes, lanes.get(el.id)) : shapeBody(el);
267
+ if (inner === "") return "";
268
+ const kind = el.author.kind;
269
+ const mark = `data-id="${esc(el.id)}" data-type="${el.type}" data-author-kind="${kind}" data-machine-made="${kind !== "human"}"`;
270
+ return `<g ${mark}>${inner}</g>`;
271
+ });
272
+ const title = esc(board.title) || "Whiteboard";
273
+ return [
274
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${num(minX)} ${num(minY)} ${width} ${height}" font-family="sans-serif" font-size="14" fill="#111827" role="img" aria-label="${title}">`,
275
+ `<title>${title}</title>`,
276
+ '<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#1f2937"/></marker></defs>',
277
+ `<rect x="${num(minX)}" y="${num(minY)}" width="${width}" height="${height}" fill="#ffffff"/>`,
278
+ ...body.filter(Boolean),
279
+ "</svg>"
280
+ ].join("\n");
281
+ }
282
+
283
+ export {
284
+ MAX_FLOW_NODES,
285
+ MAX_FLOW_EDGES,
286
+ parseFlow,
287
+ layoutFlow,
288
+ drawFlow,
289
+ toSvg
290
+ };
@@ -1,14 +1,12 @@
1
1
  import {
2
2
  CanvasError,
3
+ PRINCIPAL_KINDS,
3
4
  SHAPE_TYPES
4
- } from "./chunk-ZSPZMGHG.js";
5
- import {
6
- PRINCIPAL_KINDS
7
- } from "./chunk-JOBTIAFJ.js";
5
+ } from "./chunk-MVWUIBGV.js";
8
6
 
9
7
  // src/version.ts
10
8
  import { createRequire } from "module";
11
- var VERSION = true ? "0.10.0" : readPackageVersion();
9
+ var VERSION = true ? "0.11.0" : readPackageVersion();
12
10
 
13
11
  // src/brand.ts
14
12
  var BRAND = {