agentic-canvas 0.3.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/dist/embed.js ADDED
@@ -0,0 +1,535 @@
1
+ import {
2
+ PRINCIPAL_KINDS
3
+ } from "./chunk-SPP6PINW.js";
4
+
5
+ // ui/embed.tsx
6
+ import { ToastProvider, TooltipProvider } from "agentic-design/react";
7
+ import { Component } from "react";
8
+ import { createRoot } from "react-dom/client";
9
+
10
+ // ui/BoardView.tsx
11
+ import { Button as Button2, EmptyState, Input, PresenceChip, PresenceStack, Select, Textarea, Toolbar, ToolbarButton, ToolbarSeparator, useToast } from "agentic-design/react";
12
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
13
+
14
+ // ui/errors.ts
15
+ var isApiError = (error) => typeof error === "object" && error !== null && error.name === "CanvasApiError" && typeof error.code === "string" && typeof error.status === "number";
16
+ function describe(error) {
17
+ if (isApiError(error)) {
18
+ if (error.code === "UNAUTHORIZED") return "The server did not accept this token.";
19
+ if (error.code === "NETWORK") return "The server cannot be reached. Check the connection and try again.";
20
+ if (error.code === "FORBIDDEN") return "This token can read the board but not change it.";
21
+ return error.message;
22
+ }
23
+ return error instanceof Error ? error.message : "Something went wrong.";
24
+ }
25
+
26
+ // ui/Canvas.tsx
27
+ import { useEffect, useRef, useState } from "react";
28
+ import { jsx, jsxs } from "react/jsx-runtime";
29
+ var HOME = { x: 40, y: 40, k: 1 };
30
+ var PAN_STEP = 80;
31
+ var MIN_ZOOM = 0.2;
32
+ var MAX_ZOOM = 4;
33
+ var STEP = 10;
34
+ var TYPE_NAME = { rect: "Rectangle", ellipse: "Ellipse", diamond: "Diamond", text: "Text", sticky: "Sticky note", frame: "Frame", image: "Image", connector: "Connector" };
35
+ function byline(el) {
36
+ const made = `${el.machineMade ? "agent " : ""}${el.author.id}`;
37
+ const edit = el.lastEditMachineMade && !el.machineMade ? `, edited by agent ${el.editedBy.id}` : "";
38
+ return `made by ${made}${edit}`;
39
+ }
40
+ var label = (el) => `${TYPE_NAME[el.type] ?? el.type}${el.text ? `: ${el.text}` : ""}, ${byline(el)}`;
41
+ function wrap(text, width, maxLines) {
42
+ const per = Math.max(3, Math.floor(width / 7.5));
43
+ const lines = [];
44
+ for (const para of text.split("\n")) {
45
+ let line = "";
46
+ for (const word of para.split(/\s+/).filter(Boolean)) {
47
+ for (let w = word; ; ) {
48
+ const room = per - (line ? line.length + 1 : 0);
49
+ if (w.length <= room) {
50
+ line = line ? `${line} ${w}` : w;
51
+ break;
52
+ }
53
+ if (line) {
54
+ lines.push(line);
55
+ line = "";
56
+ } else {
57
+ lines.push(w.slice(0, per));
58
+ w = w.slice(per);
59
+ }
60
+ }
61
+ }
62
+ lines.push(line);
63
+ }
64
+ if (lines.length > maxLines) {
65
+ lines.length = Math.max(1, maxLines);
66
+ lines[lines.length - 1] = `${lines[lines.length - 1].slice(0, Math.max(0, per - 1))}\u2026`;
67
+ }
68
+ return lines;
69
+ }
70
+ function edgePoint(box, tx, ty) {
71
+ const cx = box.x + box.w / 2;
72
+ const cy = box.y + box.h / 2;
73
+ const dx = tx - cx;
74
+ const dy = ty - cy;
75
+ if (dx === 0 && dy === 0) return [cx, cy];
76
+ const s = Math.min(box.w / 2 / (Math.abs(dx) || Infinity), box.h / 2 / (Math.abs(dy) || Infinity));
77
+ return [cx + dx * s, cy + dy * s];
78
+ }
79
+ function Canvas({ data, selected, editable, view, onView, onSelect, onMove, onResize, onDelete }) {
80
+ const box = useRef(null);
81
+ const drag = useRef(void 0);
82
+ const [draft, setDraft] = useState();
83
+ const viewRef = useRef(view);
84
+ viewRef.current = view;
85
+ useEffect(() => {
86
+ const el = box.current;
87
+ const onWheel = (event) => {
88
+ event.preventDefault();
89
+ const rect = el.getBoundingClientRect();
90
+ zoomAt(event.clientX - rect.left, event.clientY - rect.top, event.deltaY < 0 ? 1.1 : 1 / 1.1);
91
+ };
92
+ el.addEventListener("wheel", onWheel, { passive: false });
93
+ return () => el.removeEventListener("wheel", onWheel);
94
+ });
95
+ function zoomAt(px, py, factor) {
96
+ const v = viewRef.current;
97
+ const k = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, v.k * factor));
98
+ onView({ k, x: px - (px - v.x) / v.k * k, y: py - (py - v.y) / v.k * k });
99
+ }
100
+ const shown = (n) => {
101
+ if (draft?.id !== n.id) return n;
102
+ return draft.kind === "move" ? { ...n, x: n.x + draft.dx, y: n.y + draft.dy } : { ...n, w: Math.max(20, n.w + draft.dx), h: Math.max(20, n.h + draft.dy) };
103
+ };
104
+ const nodes = new Map(data.nodes.map((n) => [n.id, n]));
105
+ function begin(event, kind, id) {
106
+ event.stopPropagation();
107
+ if (kind !== "pan") onSelect(id);
108
+ if (kind === "pan") onSelect(null);
109
+ if (kind !== "pan" && !editable) return;
110
+ box.current?.setPointerCapture(event.pointerId);
111
+ drag.current = { kind, id, sx: event.clientX, sy: event.clientY, vx: view.x, vy: view.y, moved: false };
112
+ }
113
+ function track(event) {
114
+ const d = drag.current;
115
+ if (!d) return;
116
+ const mx = event.clientX - d.sx;
117
+ const my = event.clientY - d.sy;
118
+ if (Math.abs(mx) + Math.abs(my) > 3) d.moved = true;
119
+ if (d.kind === "pan") onView({ ...view, x: d.vx + mx, y: d.vy + my });
120
+ else if (d.moved) setDraft({ id: d.id, kind: d.kind, dx: mx / view.k, dy: my / view.k });
121
+ }
122
+ function end() {
123
+ const d = drag.current;
124
+ drag.current = void 0;
125
+ if (!d || d.kind === "pan" || !draft || !d.moved) return setDraft(void 0);
126
+ const node = nodes.get(draft.id);
127
+ setDraft(void 0);
128
+ if (!node) return;
129
+ if (draft.kind === "move") onMove(draft.id, Math.round(draft.dx), Math.round(draft.dy));
130
+ else onResize(draft.id, Math.round(Math.max(20, node.w + draft.dx)), Math.round(Math.max(20, node.h + draft.dy)));
131
+ }
132
+ function key(event) {
133
+ const target = event.target;
134
+ if (target.closest("input, textarea, select")) return;
135
+ const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
136
+ const el = selected ? nodes.get(selected) ?? void 0 : void 0;
137
+ if (event.key in arrows) {
138
+ const [ax, ay] = arrows[event.key];
139
+ event.preventDefault();
140
+ if (el && editable) {
141
+ if (event.shiftKey) onResize(el.id, Math.max(20, el.w + ax * STEP), Math.max(20, el.h + ay * STEP));
142
+ else onMove(el.id, ax * STEP, ay * STEP);
143
+ } else if (!selected) onView({ ...view, x: view.x - ax * 40, y: view.y - ay * 40 });
144
+ } else if ((event.key === "Delete" || event.key === "Backspace") && selected && editable) {
145
+ event.preventDefault();
146
+ onDelete(selected);
147
+ } else if (event.key === "Escape") {
148
+ onSelect(null);
149
+ } else if (event.key === "+" || event.key === "=" || event.key === "-") {
150
+ const rect = box.current.getBoundingClientRect();
151
+ zoomAt(rect.width / 2, rect.height / 2, event.key === "-" ? 1 / 1.25 : 1.25);
152
+ } else if (event.key === "0") onView(HOME);
153
+ }
154
+ const itemProps = (el) => ({
155
+ className: `item${selected === el.id ? " selected" : ""}`,
156
+ tabIndex: 0,
157
+ role: "button",
158
+ "aria-pressed": selected === el.id,
159
+ "aria-label": label(el),
160
+ onFocus: () => onSelect(el.id),
161
+ onKeyDown: (event) => {
162
+ if (event.key === "Enter" || event.key === " ") {
163
+ event.preventDefault();
164
+ onSelect(el.id);
165
+ }
166
+ }
167
+ });
168
+ const badge = (el, x, y) => el.machineMade || el.lastEditMachineMade ? /* @__PURE__ */ jsxs("g", { className: "agent-badge", "data-badge-for": el.id, "aria-hidden": "true", pointerEvents: "none", children: [
169
+ /* @__PURE__ */ jsx("rect", { x, y: y - 20, width: el.machineMade ? 50 : 82, height: 18, rx: 4 }),
170
+ /* @__PURE__ */ jsx("text", { x: x + 6, y: y - 7, children: el.machineMade ? "Agent" : "Agent edit" })
171
+ ] }) : null;
172
+ return /* @__PURE__ */ jsx("div", { ref: box, className: "canvas", role: "group", "aria-label": "Board canvas. Tab moves between items. Arrow keys move the selected item, Shift with arrows resizes it, Delete removes it, Escape clears the selection.", tabIndex: 0, onKeyDown: key, onPointerMove: track, onPointerUp: end, onPointerCancel: end, children: /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", onPointerDown: (e) => begin(e, "pan"), children: [
173
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", { id: "arrow", viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "8", markerHeight: "8", orient: "auto-start-reverse", children: /* @__PURE__ */ jsx("path", { d: "M0 0L10 5L0 10z", className: "arrowhead" }) }) }),
174
+ /* @__PURE__ */ jsxs("g", { transform: `translate(${view.x} ${view.y}) scale(${view.k})`, children: [
175
+ data.edges.map((e) => {
176
+ const a = "id" in e.from ? nodes.get(e.from.id) : void 0;
177
+ const b = "id" in e.to ? nodes.get(e.to.id) : void 0;
178
+ if ("id" in e.from && !a || "id" in e.to && !b) return null;
179
+ const A = a ? shown(a) : null;
180
+ const B = b ? shown(b) : null;
181
+ const to0 = B ? [B.x + B.w / 2, B.y + B.h / 2] : [e.to.x, e.to.y];
182
+ const from0 = A ? [A.x + A.w / 2, A.y + A.h / 2] : [e.from.x, e.from.y];
183
+ const [x1, y1] = A ? edgePoint(A, ...to0) : from0;
184
+ const [x2, y2] = B ? edgePoint(B, ...from0) : to0;
185
+ return /* @__PURE__ */ jsxs("g", { children: [
186
+ /* @__PURE__ */ jsxs("g", { ...itemProps(e), "data-id": e.id, onPointerDown: (ev) => (ev.stopPropagation(), onSelect(e.id)), children: [
187
+ /* @__PURE__ */ jsx("line", { x1, y1, x2, y2, className: "hit" }),
188
+ /* @__PURE__ */ jsx("line", { x1, y1, x2, y2, className: "edge", markerEnd: e.endArrow ? "url(#arrow)" : void 0, markerStart: e.startArrow ? "url(#arrow)" : void 0 }),
189
+ e.text && /* @__PURE__ */ jsx("text", { "aria-hidden": "true", x: (x1 + x2) / 2, y: (y1 + y2) / 2 - 6, className: "edge-text", textAnchor: "middle", children: e.text }),
190
+ /* @__PURE__ */ jsx("line", { x1, y1, x2, y2, className: "ring-line" })
191
+ ] }),
192
+ badge(e, (x1 + x2) / 2 - 25, (y1 + y2) / 2 - 14)
193
+ ] }, e.id);
194
+ }),
195
+ data.nodes.map((n) => {
196
+ const s = shown(n);
197
+ const lines = wrap(n.text, s.w - 16, Math.floor((s.h - 8) / 18));
198
+ const top = n.type === "frame" ? s.y + 18 : s.y + s.h / 2 - (lines.length - 1) * 18 / 2 + 5;
199
+ const anchor = n.type === "frame" ? "start" : "middle";
200
+ const tx = n.type === "frame" ? s.x + 8 : s.x + s.w / 2;
201
+ return /* @__PURE__ */ jsxs("g", { children: [
202
+ /* @__PURE__ */ jsxs("g", { ...itemProps(n), "data-id": n.id, onPointerDown: (ev) => begin(ev, "move", n.id), children: [
203
+ n.type === "ellipse" ? /* @__PURE__ */ jsx("ellipse", { cx: s.x + s.w / 2, cy: s.y + s.h / 2, rx: s.w / 2, ry: s.h / 2, className: "shape" }) : n.type === "diamond" ? /* @__PURE__ */ jsx("polygon", { points: `${s.x + s.w / 2},${s.y} ${s.x + s.w},${s.y + s.h / 2} ${s.x + s.w / 2},${s.y + s.h} ${s.x},${s.y + s.h / 2}`, className: "shape" }) : /* @__PURE__ */ jsx("rect", { x: s.x, y: s.y, width: s.w, height: s.h, rx: n.type === "sticky" ? 2 : 6, className: `shape ${n.type}` }),
204
+ /* @__PURE__ */ jsx("text", { "aria-hidden": "true", x: tx, y: top, textAnchor: anchor, className: n.type === "sticky" ? "label sticky-label" : "label", children: n.type === "image" && !n.text ? /* @__PURE__ */ jsx("tspan", { children: "Image" }) : lines.map((l, i) => /* @__PURE__ */ jsx("tspan", { x: tx, dy: i === 0 ? 0 : 18, children: i < lines.length - 1 ? `${l} ` : l }, i)) }),
205
+ /* @__PURE__ */ jsx("rect", { x: s.x - 4, y: s.y - 4, width: s.w + 8, height: s.h + 8, rx: 8, className: "ring" }),
206
+ selected === n.id && editable && /* @__PURE__ */ jsx("circle", { cx: s.x + s.w + 4, cy: s.y + s.h + 4, r: 7 / view.k, className: "handle", onPointerDown: (ev) => begin(ev, "resize", n.id) })
207
+ ] }),
208
+ badge(n, s.x, s.y)
209
+ ] }, n.id);
210
+ })
211
+ ] })
212
+ ] }) });
213
+ }
214
+
215
+ // ui/Failure.tsx
216
+ import { Button } from "agentic-design/react";
217
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
218
+ function Failure({ message, retry }) {
219
+ return /* @__PURE__ */ jsxs2("div", { role: "alert", className: "failure", children: [
220
+ /* @__PURE__ */ jsxs2("p", { children: [
221
+ /* @__PURE__ */ jsx2("strong", { children: "Something went wrong." }),
222
+ " ",
223
+ message
224
+ ] }),
225
+ /* @__PURE__ */ jsx2(Button, { variant: "secondary", onClick: retry, children: "Try again" })
226
+ ] });
227
+ }
228
+
229
+ // ui/presence.ts
230
+ import { SyncClient } from "agentic-realtime/client";
231
+ var WS_PROTOCOL = "agentic-canvas.bearer";
232
+ var b64url = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
233
+ var bearerSocket = (token) => class extends WebSocket {
234
+ constructor(url) {
235
+ super(url, [WS_PROTOCOL, b64url(token)]);
236
+ }
237
+ };
238
+ function roomUrl(baseUrl, board) {
239
+ const url = new URL(`${baseUrl.replace(/\/+$/, "")}/boards/${encodeURIComponent(board)}/ws`, window.location.href);
240
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
241
+ return url.toString();
242
+ }
243
+ var revOf = (peer) => {
244
+ const rev = peer.cursor?.rev;
245
+ return typeof rev === "number" && Number.isFinite(rev) ? rev : null;
246
+ };
247
+ function toEntry(peer) {
248
+ return { id: String(peer.clientId), name: peer.name, kind: peer.machine ? "agent" : "person" };
249
+ }
250
+ function joinRoom(options) {
251
+ const seen = /* @__PURE__ */ new Map();
252
+ const client = SyncClient.open({
253
+ url: roomUrl(options.baseUrl, options.board),
254
+ WebSocket: bearerSocket(options.token),
255
+ presence: { name: options.name, cursor: { rev: 0 } },
256
+ reconnect: { initialMs: 1e3, maxMs: 15e3 },
257
+ onStatus: (status) => options.onStatus(status === "connected")
258
+ });
259
+ const off = client.onPeers((peers) => {
260
+ options.onPeers(peers.map(toEntry));
261
+ let changed = false;
262
+ for (const peer of peers) {
263
+ const rev = revOf(peer);
264
+ if (rev !== null && seen.has(peer.clientId) && seen.get(peer.clientId) !== rev) changed = true;
265
+ if (rev !== null) seen.set(peer.clientId, rev);
266
+ }
267
+ if (changed) options.onChange();
268
+ });
269
+ return {
270
+ setRev: (rev) => client.setPresence({ cursor: { rev } }),
271
+ leave: () => {
272
+ off();
273
+ void client.disconnect();
274
+ }
275
+ };
276
+ }
277
+
278
+ // ui/BoardView.tsx
279
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
280
+ var icon = (d) => /* @__PURE__ */ jsx3("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.6", "aria-hidden": "true", children: d });
281
+ var TOOLS = [
282
+ { type: "rect", text: "Add rectangle", icon: icon(/* @__PURE__ */ jsx3("rect", { x: "3", y: "5", width: "14", height: "10", rx: "2" })) },
283
+ { type: "ellipse", text: "Add ellipse", icon: icon(/* @__PURE__ */ jsx3("ellipse", { cx: "10", cy: "10", rx: "7", ry: "5" })) },
284
+ { type: "diamond", text: "Add diamond", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M10 3l7 7-7 7-7-7z" })) },
285
+ { type: "text", text: "Add text", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 5h12M10 5v11" })) },
286
+ { type: "sticky", text: "Add sticky note", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 4h12v8l-4 4H4z M12 16v-4h4" })) }
287
+ ];
288
+ var POLL_MS = 5e3;
289
+ var DEFAULT_SIZE = { rect: [140, 70], ellipse: [140, 70], diamond: [140, 90], text: [140, 40], sticky: [150, 120] };
290
+ var DEFAULT_TEXT = { rect: "Rectangle", ellipse: "Ellipse", diamond: "Decision", text: "Text", sticky: "Note" };
291
+ function BoardView({ id, client, room: roomOptions, me, backHref }) {
292
+ const roomUrl2 = roomOptions?.baseUrl;
293
+ const roomToken = roomOptions?.token;
294
+ const toast = useToast();
295
+ const [board, setBoard] = useState2({ state: "loading" });
296
+ const [selected, setSelected] = useState2(null);
297
+ const [view, setView] = useState2(HOME);
298
+ const [peers, setPeers] = useState2([]);
299
+ const [live, setLive] = useState2(false);
300
+ const room = useRef2(void 0);
301
+ const rev = useRef2(0);
302
+ const queue = useRef2(Promise.resolve());
303
+ const canvasBox = useRef2(null);
304
+ const editable = me.role === "editor";
305
+ const [saving, setSaving] = useState2(0);
306
+ const pending = useRef2(0);
307
+ const generation = useRef2(0);
308
+ const lastRead = useRef2(0);
309
+ const timer = useRef2(void 0);
310
+ const read = useCallback(async () => {
311
+ try {
312
+ const started = generation.current;
313
+ const data2 = await client.readBoard(id);
314
+ if (started !== generation.current || pending.current > 0) return;
315
+ setBoard({ state: "ready", data: data2 });
316
+ } catch (e) {
317
+ setBoard((prev) => prev.state === "ready" ? prev : { state: "error", message: describe(e), missing: isApiError(e) && e.status === 404 });
318
+ }
319
+ }, [client, id]);
320
+ const load = useCallback(async () => {
321
+ if (pending.current > 0) return;
322
+ const wait = lastRead.current + 1e3 - Date.now();
323
+ if (wait > 0) {
324
+ clearTimeout(timer.current);
325
+ timer.current = setTimeout(() => void load(), wait);
326
+ return;
327
+ }
328
+ lastRead.current = Date.now();
329
+ await read();
330
+ }, [read]);
331
+ useEffect2(() => {
332
+ setBoard({ state: "loading" });
333
+ lastRead.current = 0;
334
+ void load();
335
+ return () => clearTimeout(timer.current);
336
+ }, [load]);
337
+ useEffect2(() => {
338
+ if (saving === 0) return;
339
+ const guard = (event) => event.preventDefault();
340
+ window.addEventListener("beforeunload", guard);
341
+ return () => window.removeEventListener("beforeunload", guard);
342
+ }, [saving]);
343
+ useEffect2(() => {
344
+ if (roomUrl2 === void 0 || roomToken === void 0) return;
345
+ const joined = joinRoom({ board: id, baseUrl: roomUrl2, token: roomToken, name: me.actor, onPeers: setPeers, onChange: () => void load(), onStatus: setLive });
346
+ room.current = joined;
347
+ return () => (joined.leave(), room.current = void 0);
348
+ }, [id, roomUrl2, roomToken, me.actor, load]);
349
+ useEffect2(() => {
350
+ const timer2 = setInterval(() => void load(), POLL_MS);
351
+ return () => clearInterval(timer2);
352
+ }, [load]);
353
+ const write = useCallback(
354
+ (change, failure, select) => {
355
+ pending.current++;
356
+ generation.current++;
357
+ setSaving((n) => n + 1);
358
+ queue.current = queue.current.then(async () => {
359
+ try {
360
+ await change();
361
+ room.current?.setRev(++rev.current);
362
+ } catch (e) {
363
+ toast({ title: failure, description: describe(e), tone: "danger" });
364
+ }
365
+ pending.current--;
366
+ lastRead.current = 0;
367
+ await load();
368
+ if (select) setSelected(select);
369
+ setSaving((n) => n - 1);
370
+ });
371
+ },
372
+ [load, toast]
373
+ );
374
+ const data = board.state === "ready" ? board.data : void 0;
375
+ const item = data && selected ? [...data.nodes, ...data.edges].find((i) => i.id === selected) : void 0;
376
+ useEffect2(() => {
377
+ if (data && selected && !item) setSelected(null);
378
+ }, [data, selected, item]);
379
+ const patchLocal = (fn) => setBoard((b) => b.state === "ready" ? { state: "ready", data: { ...b.data, nodes: b.data.nodes.map(fn) } } : b);
380
+ const move = (target, dx, dy) => {
381
+ patchLocal((n) => n.id === target ? { ...n, x: n.x + dx, y: n.y + dy } : n);
382
+ write(() => client.move(id, target, dx, dy), "Could not move the item");
383
+ };
384
+ const resize = (target, w, h) => place(target, { w, h });
385
+ const place = (target, patch) => {
386
+ patchLocal((n) => n.id === target ? { ...n, ...patch } : n);
387
+ write(() => client.update(id, target, patch), "Could not change the item");
388
+ };
389
+ const remove = (target) => {
390
+ setSelected(null);
391
+ write(() => client.remove(id, target), "Could not delete the item");
392
+ canvasBox.current?.querySelector("[role=group]")?.focus();
393
+ };
394
+ const add = (type) => {
395
+ const [w, h] = DEFAULT_SIZE[type];
396
+ const n = data?.nodes.length ?? 0;
397
+ const box = canvasBox.current?.getBoundingClientRect();
398
+ const cx = ((box?.width ?? 600) / 2 - view.x) / view.k - w / 2 + n % 6 * 24;
399
+ const cy = ((box?.height ?? 400) / 2 - view.y) / view.k - h / 2 + n % 6 * 24;
400
+ const shapeId = `s-${crypto.randomUUID().slice(0, 8)}`;
401
+ write(() => client.addShape(id, { id: shapeId, type, x: Math.round(cx), y: Math.round(cy), w, h, text: DEFAULT_TEXT[type] }), "Could not add the item", shapeId);
402
+ };
403
+ const zoom = (factor) => setView((v) => ({ ...v, k: Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, v.k * factor)) }));
404
+ if (board.state === "loading") return /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading board\u2026" });
405
+ if (board.state === "error") {
406
+ return board.missing ? /* @__PURE__ */ jsx3(EmptyState, { title: "This board does not exist", description: backHref ? /* @__PURE__ */ jsx3("a", { href: backHref, children: "Back to all boards" }) : void 0 }) : /* @__PURE__ */ jsx3(Failure, { message: board.message, retry: () => (setBoard({ state: "loading" }), void load()) });
407
+ }
408
+ const { data: d } = board;
409
+ const status = saving > 0 ? "Saving\u2026" : "All changes saved";
410
+ const empty = d.nodes.length === 0 && d.edges.length === 0;
411
+ return /* @__PURE__ */ jsxs3("section", { className: "board", "aria-labelledby": "board-h", children: [
412
+ /* @__PURE__ */ jsxs3("div", { className: "row", children: [
413
+ /* @__PURE__ */ jsx3("h1", { id: "board-h", children: d.title || d.id }),
414
+ /* @__PURE__ */ jsx3(PresenceStack, { entries: [{ id: "me", name: me.actor, kind: me.kind === "human" ? "person" : "agent" }, ...peers], label: live ? "In this board" : "In this board (not live)" })
415
+ ] }),
416
+ /* @__PURE__ */ jsxs3(Toolbar, { label: "Board tools", children: [
417
+ TOOLS.map((t) => /* @__PURE__ */ jsx3(ToolbarButton, { label: t.text, icon: t.icon, disabled: !editable, onClick: () => add(t.type) }, t.type)),
418
+ /* @__PURE__ */ jsx3(ToolbarSeparator, {}),
419
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Zoom out", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 10h12" })), onClick: () => zoom(1 / 1.25) }),
420
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Zoom in", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 10h12M10 4v12" })), onClick: () => zoom(1.25) }),
421
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Pan left", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M12 4l-6 6 6 6" })), onClick: () => setView((v) => ({ ...v, x: v.x + PAN_STEP })) }),
422
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Pan right", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M8 4l6 6-6 6" })), onClick: () => setView((v) => ({ ...v, x: v.x - PAN_STEP })) }),
423
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Pan up", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 12l6-6 6 6" })), onClick: () => setView((v) => ({ ...v, y: v.y + PAN_STEP })) }),
424
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Pan down", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 8l6 6 6-6" })), onClick: () => setView((v) => ({ ...v, y: v.y - PAN_STEP })) }),
425
+ /* @__PURE__ */ jsx3(ToolbarButton, { label: "Reset view", icon: icon(/* @__PURE__ */ jsx3("path", { d: "M4 4h5M4 4v5M16 16h-5M16 16v-5" })), onClick: () => setView(HOME) })
426
+ ] }),
427
+ /* @__PURE__ */ jsx3("p", { className: "note", role: "status", children: editable ? status : "" }),
428
+ !editable && /* @__PURE__ */ jsx3("p", { className: "note", children: "This token can read the board but not change it." }),
429
+ /* @__PURE__ */ jsxs3("div", { className: "stage", ref: canvasBox, children: [
430
+ /* @__PURE__ */ jsx3(Canvas, { data: d, selected, editable, view, onView: setView, onSelect: setSelected, onMove: move, onResize: resize, onDelete: remove }),
431
+ empty && /* @__PURE__ */ jsx3("div", { className: "empty-overlay", children: /* @__PURE__ */ jsx3(EmptyState, { title: "This board is empty", description: editable ? "Add a shape or a note with the tools above." : "Nothing has been drawn yet." }) }),
432
+ /* @__PURE__ */ jsx3(Inspector, { item, nodes: d.nodes, editable, onText: (text) => item && write(() => client.update(id, item.id, { text }), "Could not save the text"), onPlace: place, onDelete: remove, onConnect: (to) => item && write(() => client.addConnector(id, { from: { id: item.id }, to: { id: to } }), "Could not connect") }, item?.id ?? "none")
433
+ ] })
434
+ ] });
435
+ }
436
+ var asEntry = (p) => ({ id: p.id, name: p.id, kind: p.kind === "human" ? "person" : "agent" });
437
+ function Inspector({ item, nodes, editable, onText, onPlace, onDelete, onConnect }) {
438
+ const [draft, setDraft] = useState2({});
439
+ const node = item && item.type !== "connector" ? item : void 0;
440
+ if (!item) return /* @__PURE__ */ jsx3("aside", { className: "inspector", "aria-label": "Selected item", children: /* @__PURE__ */ jsx3("p", { children: "Select an item to edit it." }) });
441
+ const value = (k) => draft[k] ?? String(k === "text" ? item.text : node?.[k] ?? "");
442
+ const edit = (k) => (e) => setDraft({ ...draft, [k]: e.target.value });
443
+ const submit = (event) => {
444
+ event.preventDefault();
445
+ if (draft.text !== void 0 && draft.text !== item.text) onText(draft.text);
446
+ if (node) {
447
+ const patch = {};
448
+ for (const k of ["x", "y", "w", "h"]) {
449
+ const typed = draft[k];
450
+ if (typed === void 0 || typed.trim() === "") continue;
451
+ const n = Number(typed);
452
+ if (Number.isFinite(n) && (k === "x" || k === "y" || n > 0) && n !== node[k]) patch[k] = n;
453
+ }
454
+ if (Object.keys(patch).length > 0) onPlace(node.id, patch);
455
+ }
456
+ setDraft({});
457
+ };
458
+ return /* @__PURE__ */ jsxs3("aside", { className: "inspector", "aria-label": "Selected item", children: [
459
+ /* @__PURE__ */ jsx3("h2", { children: TYPE_NAME[item.type] ?? item.type }),
460
+ /* @__PURE__ */ jsx3("p", { className: "by", children: "Made by" }),
461
+ /* @__PURE__ */ jsx3(PresenceChip, { entry: asEntry(item.author) }),
462
+ item.lastEditMachineMade && !item.machineMade && /* @__PURE__ */ jsxs3(Fragment, { children: [
463
+ /* @__PURE__ */ jsx3("p", { className: "by", children: "Last edit" }),
464
+ /* @__PURE__ */ jsx3(PresenceChip, { entry: asEntry(item.editedBy) })
465
+ ] }),
466
+ /* @__PURE__ */ jsx3("span", { className: "sr-only", children: byline(item) }),
467
+ /* @__PURE__ */ jsxs3("form", { onSubmit: submit, children: [
468
+ /* @__PURE__ */ jsx3(Textarea, { label: "Text", value: value("text"), onChange: edit("text"), rows: 3, maxLength: 1e4, disabled: !editable }),
469
+ node && /* @__PURE__ */ jsxs3("div", { className: "sizes", children: [
470
+ /* @__PURE__ */ jsx3(Input, { label: "X", type: "number", value: value("x"), onChange: edit("x"), disabled: !editable }),
471
+ /* @__PURE__ */ jsx3(Input, { label: "Y", type: "number", value: value("y"), onChange: edit("y"), disabled: !editable }),
472
+ /* @__PURE__ */ jsx3(Input, { label: "Width", type: "number", min: 20, value: value("w"), onChange: edit("w"), disabled: !editable }),
473
+ /* @__PURE__ */ jsx3(Input, { label: "Height", type: "number", min: 20, value: value("h"), onChange: edit("h"), disabled: !editable })
474
+ ] }),
475
+ editable && /* @__PURE__ */ jsx3(Button2, { type: "submit", variant: "secondary", children: "Save changes" })
476
+ ] }),
477
+ editable && node && nodes.length > 1 && /* @__PURE__ */ jsxs3(Select, { label: "Connect to", value: "", onChange: (e) => e.target.value && onConnect(e.target.value), children: [
478
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Choose a shape\u2026" }),
479
+ nodes.filter((n) => n.id !== node.id).map((n) => /* @__PURE__ */ jsxs3("option", { value: n.id, children: [
480
+ TYPE_NAME[n.type],
481
+ n.text ? `: ${n.text.slice(0, 30)}` : ""
482
+ ] }, n.id))
483
+ ] }),
484
+ editable && /* @__PURE__ */ jsx3(Button2, { variant: "danger", onClick: () => onDelete(item.id), children: "Delete" })
485
+ ] });
486
+ }
487
+
488
+ // ui/embed.tsx
489
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
490
+ function checkPrincipal(principal) {
491
+ if (!principal || !PRINCIPAL_KINDS.includes(principal.kind) || typeof principal.name !== "string" || principal.name.trim() === "") {
492
+ throw new TypeError(`the board needs a principal with a kind (${PRINCIPAL_KINDS.join(", ")}) and a name`);
493
+ }
494
+ }
495
+ var Boundary = class extends Component {
496
+ state = { failed: false };
497
+ static getDerivedStateFromError() {
498
+ return { failed: true };
499
+ }
500
+ render() {
501
+ return this.state.failed ? /* @__PURE__ */ jsx4("div", { role: "alert", className: "failure", children: /* @__PURE__ */ jsxs4("p", { children: [
502
+ /* @__PURE__ */ jsx4("strong", { children: "The board stopped." }),
503
+ " It has content this page cannot show, or a principal it does not know. Reload to try again."
504
+ ] }) }) : this.props.children;
505
+ }
506
+ };
507
+ function Board({ boardId, client, principal, readOnly, room, backHref }) {
508
+ checkPrincipal(principal);
509
+ const role = principal.role === "editor" && !readOnly ? "editor" : "viewer";
510
+ return /* @__PURE__ */ jsx4(BoardView, { id: boardId, client, room, backHref, me: { actor: principal.name, kind: principal.kind, role } }, boardId);
511
+ }
512
+ function BoardEditor(options) {
513
+ return /* @__PURE__ */ jsx4("div", { className: "cv-root", children: /* @__PURE__ */ jsx4(Boundary, { children: /* @__PURE__ */ jsx4(Board, { ...options }) }) });
514
+ }
515
+ function mount(element, options) {
516
+ checkPrincipal(options.principal);
517
+ const root = createRoot(element);
518
+ const render = (next) => root.render(
519
+ /* @__PURE__ */ jsx4(TooltipProvider, { children: /* @__PURE__ */ jsx4(ToastProvider, { children: /* @__PURE__ */ jsx4(BoardEditor, { ...next }) }) })
520
+ );
521
+ render(options);
522
+ let open = true;
523
+ return {
524
+ update: (next) => (checkPrincipal(next.principal), open && render(next)),
525
+ unmount: () => {
526
+ if (!open) return;
527
+ open = false;
528
+ root.unmount();
529
+ }
530
+ };
531
+ }
532
+ export {
533
+ BoardEditor,
534
+ mount
535
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { c as Board, P as Principal } from './read-BY6f-mmy.js';
2
- export { B as BoardData, d as CanvasElement, e as CanvasError, C as Connector, f as EdgeData, b as Endpoint, E as ErrorCode, G as Group, M as MAX_ELEMENTS, g as MAX_TEXT, N as NodeData, h as SHAPE_TYPES, a as Shape, S as ShapeType, p as principalSchema, r as readBoard } from './read-BY6f-mmy.js';
3
- import { B as BoardAccess } from './service-Cpl6wKeY.js';
4
- export { C as CanvasService, D as DrawOptions, F as FlowEdge, a as FlowGraph, b as FlowNode, M as MAX_FLOW_EDGES, c as MAX_FLOW_NODES, P as Placement, d as drawFlow, l as layoutFlow, p as parseFlow } from './service-Cpl6wKeY.js';
1
+ import { B as Board, P as Principal } from './read-BupIJSPJ.js';
2
+ export { a as BoardData, b as CanvasElement, e as CanvasError, C as Connector, f as EdgeData, d as Endpoint, E as ErrorCode, G as Group, M as MAX_ELEMENTS, g as MAX_TEXT, N as NodeData, h as SHAPE_TYPES, S as Shape, c as ShapeType, p as principalSchema, r as readBoard } from './read-BupIJSPJ.js';
3
+ import { B as BoardAccess } from './service-CR4-r1yx.js';
4
+ export { C as CanvasService, D as DrawOptions, F as FlowEdge, a as FlowGraph, b as FlowNode, M as MAX_FLOW_EDGES, c as MAX_FLOW_NODES, P as Placement, d as drawFlow, l as layoutFlow, p as parseFlow } from './service-CR4-r1yx.js';
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  export { FileStoreOptions, createFileStore, openLocalStore } from './store-file.js';
7
7
  export { PRINCIPAL_KINDS, PrincipalKind, isMachineMade } from 'agentic-identity';
package/dist/index.js CHANGED
@@ -6,14 +6,15 @@ import {
6
6
  operatorActor,
7
7
  runMcp,
8
8
  svgToPng
9
- } from "./chunk-AGODNBN7.js";
9
+ } from "./chunk-VHHJIBAB.js";
10
10
  import {
11
11
  createFileStore,
12
12
  openLocalStore
13
13
  } from "./chunk-UKST5A57.js";
14
14
  import {
15
15
  CanvasService
16
- } from "./chunk-W7NRP3EX.js";
16
+ } from "./chunk-G4YTESPS.js";
17
+ import "./chunk-NWQED42L.js";
17
18
  import {
18
19
  Board,
19
20
  CanvasError,
@@ -21,17 +22,18 @@ import {
21
22
  MAX_FLOW_EDGES,
22
23
  MAX_FLOW_NODES,
23
24
  MAX_TEXT,
24
- PRINCIPAL_KINDS,
25
25
  SHAPE_TYPES,
26
26
  drawFlow,
27
- isMachineMade,
28
27
  layoutFlow,
29
28
  parseFlow,
30
- principalSchema,
31
29
  readBoard,
32
30
  toSvg
33
- } from "./chunk-GGQPE65T.js";
34
- import "./chunk-NWQED42L.js";
31
+ } from "./chunk-JCYNPYYJ.js";
32
+ import {
33
+ PRINCIPAL_KINDS,
34
+ isMachineMade,
35
+ principalSchema
36
+ } from "./chunk-SPP6PINW.js";
35
37
  export {
36
38
  ActorConfigError,
37
39
  BRAND,
@@ -5,10 +5,10 @@ import 'agentic-identity';
5
5
  import 'agentic-realtime';
6
6
  import 'agentic-identity/cloudflare';
7
7
  import 'hono';
8
- import './read-BY6f-mmy.js';
8
+ import './read-BupIJSPJ.js';
9
9
  import 'yjs';
10
10
  import 'zod';
11
- import './service-Cpl6wKeY.js';
11
+ import './service-CR4-r1yx.js';
12
12
 
13
13
  type Namespace = Parameters<typeof createDurableObjectRealtime>[0];
14
14
  interface LiveEnv extends WorkerEnv {
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-V477KSZQ.js";
4
- import "./chunk-W7NRP3EX.js";
3
+ } from "./chunk-V74PWJKY.js";
4
+ import "./chunk-G4YTESPS.js";
5
+ import "./chunk-NWQED42L.js";
5
6
  import {
6
7
  CanvasError
7
- } from "./chunk-GGQPE65T.js";
8
- import "./chunk-NWQED42L.js";
8
+ } from "./chunk-JCYNPYYJ.js";
9
+ import "./chunk-SPP6PINW.js";
9
10
 
10
11
  // src/live-worker.ts
11
12
  import { parseRegion } from "agentic-identity";
package/dist/live.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as Board, P as Principal, B as BoardData, a as Shape, C as Connector } from './read-BY6f-mmy.js';
1
+ import { B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-BupIJSPJ.js';
2
2
  import { Peer } from 'agentic-realtime/client';
3
- import { B as BoardAccess, D as DrawOptions } from './service-Cpl6wKeY.js';
3
+ import { B as BoardAccess, D as DrawOptions } from './service-CR4-r1yx.js';
4
4
  import 'yjs';
5
5
  import 'zod';
6
6
  import 'agentic-identity';
@@ -79,12 +79,17 @@ declare class LiveService implements BoardAccess {
79
79
  id: string;
80
80
  title: string;
81
81
  }>;
82
+ list(): Promise<{
83
+ id: string;
84
+ title: string;
85
+ }[]>;
82
86
  read(id: string): Promise<BoardData & {
83
87
  id: string;
84
88
  }>;
85
89
  exportSvg(id: string): Promise<string>;
86
90
  add(id: string, shape: unknown, _actor?: Principal): Promise<Shape>;
87
91
  connect(id: string, connector: unknown, _actor?: Principal): Promise<Connector>;
92
+ update(id: string, element: string, patch: unknown, _actor?: Principal): Promise<CanvasElement>;
88
93
  move(id: string, element: string, dx: number, dy: number, _actor?: Principal): Promise<{
89
94
  moved: string[];
90
95
  }>;