@1agh/maude 0.29.0 → 0.31.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 +6 -6
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/ai-banner.tsx +0 -1
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/apps/studio/annotations-context-toolbar.tsx +436 -186
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +1714 -1661
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +433 -182
- package/apps/studio/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/bin/read-annotations.mjs +152 -17
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-arrowheads.ts +78 -9
- package/apps/studio/canvas-cursors.ts +2 -0
- package/apps/studio/canvas-edit.ts +257 -7
- package/apps/studio/canvas-icons.tsx +105 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +326 -9
- package/apps/studio/client/app.jsx +3579 -250
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/panels/ChatPanel.jsx +770 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +1326 -14
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +41 -2
- package/apps/studio/client/styles/6-acp-chat.css +771 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +116 -18
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/commands/annotation-strokes-command.ts +1 -1
- package/apps/studio/commands/move-artboards-command.ts +1 -1
- package/apps/studio/comments-overlay.tsx +7 -5
- package/apps/studio/context-menu.tsx +4 -3
- package/apps/studio/contextual-toolbar.tsx +14 -0
- package/apps/studio/cursors-overlay.tsx +37 -21
- package/apps/studio/dist/client.bundle.js +31795 -1829
- package/apps/studio/dist/comment-mount.js +97 -9
- package/apps/studio/dist/styles.css +7938 -1599
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/history.ts +20 -3
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +508 -6
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +70 -16
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +70 -4
- package/apps/studio/sync/agent.ts +311 -57
- package/apps/studio/sync/codec.ts +69 -0
- package/apps/studio/sync/cold-start.ts +198 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +202 -44
- package/apps/studio/sync/projection.ts +22 -3
- package/apps/studio/sync/status.ts +15 -4
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +78 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +43 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-bridge.test.ts +0 -2
- package/apps/studio/test/collab-room.test.ts +2 -7
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +78 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
- package/apps/studio/test/sync-agent.test.ts +272 -2
- package/apps/studio/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +304 -0
- package/apps/studio/test/sync-connection-state.test.ts +68 -0
- package/apps/studio/test/sync-hardening.test.ts +0 -8
- package/apps/studio/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/apps/studio/test/sync-runtime.test.ts +360 -5
- package/apps/studio/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/test/use-annotation-resize.test.ts +159 -0
- package/apps/studio/test/use-artboard-drag.test.ts +0 -1
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-tool-mode.test.tsx +5 -2
- package/apps/studio/tool-palette.tsx +30 -11
- package/apps/studio/undo-hud.tsx +0 -1
- package/apps/studio/use-agent-presence.tsx +4 -2
- package/apps/studio/use-annotation-resize.tsx +484 -54
- package/apps/studio/use-annotation-selection.tsx +16 -4
- package/apps/studio/use-canvas-activity.tsx +2 -2
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +434 -202
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +208 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +3 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +58 -12
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# annotate.sh — thin wrapper around annotate.mjs (the AI annotation WRITE
|
|
3
|
+
# verb). Runs under Bun, not node: the verb imports the CANONICAL TypeScript
|
|
4
|
+
# annotation model (annotations-model.ts — serializer + sanitizer) so it can
|
|
5
|
+
# never emit a shape the canvas wouldn't, and node can't import TS.
|
|
6
|
+
# Reached via `maude design annotate` (DDR-062), never a raw bin path.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# annotate.sh <rel-path> [--ops <file|->] [--flow <file|->] [--near <id>]
|
|
10
|
+
# [--canvas-state <path>] [--root <repo>] [--dry-run]
|
|
11
|
+
# (see annotate.mjs --help for the full contract)
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
14
|
+
echo "annotate: bun is required (the verb imports the canonical TS annotation model). Install: https://bun.sh" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
exec bun "$SCRIPT_DIR/annotate.mjs" "$@"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# chat-open.sh — Phase 31 (DDR-123). Backs `/design:chat` (via `maude design
|
|
3
|
+
# chat-open`): asks the running dev-server to surface the native ACP chat
|
|
4
|
+
# sidepanel by POSTing /_api/acp/focus. The server emits a bus event the shell
|
|
5
|
+
# (app.jsx, native-only) turns into "open the Assistant panel".
|
|
6
|
+
#
|
|
7
|
+
# Reads: $DESIGN_ROOT/_server.json (port the running server wrote)
|
|
8
|
+
# Usage: chat-open.sh [--root <repo>]
|
|
9
|
+
# Exits: 0 on success, 1 if no live server / focus request failed.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
REPO="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
13
|
+
while [ $# -gt 0 ]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--root) REPO="$2"; shift 2 ;;
|
|
16
|
+
*) shift ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
DESIGN_ROOT="$REPO/.design"
|
|
21
|
+
STATE="$DESIGN_ROOT/_server.json"
|
|
22
|
+
|
|
23
|
+
if [ ! -f "$STATE" ]; then
|
|
24
|
+
echo "chat-open: no running dev server ($STATE missing). Open Maude first." >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if command -v jq >/dev/null 2>&1; then
|
|
29
|
+
PORT="$(jq -r '.port // empty' "$STATE" 2>/dev/null)"
|
|
30
|
+
else
|
|
31
|
+
PORT="$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" 2>/dev/null | head -n1)"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [ -z "${PORT:-}" ]; then
|
|
35
|
+
echo "chat-open: could not read the server port from $STATE." >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if curl -fsS -X POST "http://127.0.0.1:${PORT}/_api/acp/focus" >/dev/null 2>&1; then
|
|
40
|
+
echo "→ opened the Assistant panel in Maude (port ${PORT})."
|
|
41
|
+
else
|
|
42
|
+
echo "chat-open: focus request to the dev server failed (port ${PORT})." >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
@@ -31,18 +31,21 @@
|
|
|
31
31
|
|
|
32
32
|
import { existsSync, readFileSync } from 'node:fs';
|
|
33
33
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
34
|
+
import { pathToFileURL } from 'node:url';
|
|
34
35
|
|
|
35
36
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
37
|
// Argv
|
|
37
38
|
|
|
38
39
|
function parseArgv(argv) {
|
|
39
|
-
const out = { positional: [], root: null, canvasState: null, help: false };
|
|
40
|
+
const out = { positional: [], root: null, canvasState: null, help: false, graph: false };
|
|
40
41
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
42
|
const a = argv[i];
|
|
42
43
|
if (a === '--help' || a === '-h') {
|
|
43
44
|
out.help = true;
|
|
44
45
|
} else if (a === '--json') {
|
|
45
46
|
/* default — accepted for symmetry with other verbs */
|
|
47
|
+
} else if (a === '--graph') {
|
|
48
|
+
out.graph = true;
|
|
46
49
|
} else if (a === '--root') {
|
|
47
50
|
i += 1;
|
|
48
51
|
out.root = argv[i];
|
|
@@ -69,10 +72,20 @@ Args:
|
|
|
69
72
|
<rel-path> Canvas path relative to the design root (e.g. "ui/Foo.tsx").
|
|
70
73
|
--root <repo> Repo root. Default: $CLAUDE_PROJECT_DIR, then cwd.
|
|
71
74
|
--canvas-state <p> JSON of artboard rects ([{id,x,y,w,h}] or {artboards:[…]});
|
|
72
|
-
each annotation is tagged with the artboard it overlaps
|
|
75
|
+
each annotation is tagged with the artboard it overlaps,
|
|
76
|
+
plus artboard-relative coords (rel) and a W3C-style
|
|
77
|
+
target block { source, selector, geometry }.
|
|
78
|
+
--graph Emit { annotations, graph } instead of the bare array:
|
|
79
|
+
graph.edges = bound arrows (from/to host ids), graph.nodes
|
|
80
|
+
= the strokes those arrows connect (with labels) — a
|
|
81
|
+
bound flow diagram reads back as a graph.
|
|
73
82
|
--json No-op (JSON is the only output form).
|
|
74
83
|
|
|
75
|
-
|
|
84
|
+
FigJam v3 additive fields per annotation: z (render order), groupIds (deepest →
|
|
85
|
+
shallowest), author ("ai" for annotate-verb strokes), and from/to host ids on
|
|
86
|
+
bound arrows.
|
|
87
|
+
|
|
88
|
+
Emits JSON to stdout. A missing annotation file emits [] (exit 0).`;
|
|
76
89
|
|
|
77
90
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
91
|
// Path + slug resolution — mirror the dev-server (api.ts fileSlug, context.ts
|
|
@@ -172,6 +185,22 @@ function readText(attrs, inner) {
|
|
|
172
185
|
return o;
|
|
173
186
|
}
|
|
174
187
|
|
|
188
|
+
// FigJam v3 — section container: geometry off the inner <rect>, label off
|
|
189
|
+
// data-label (the inner <text> is presentational chrome).
|
|
190
|
+
function readSection(attrs, inner) {
|
|
191
|
+
const rectAttrs = inner.match(/<rect\b([^>]*?)\/?>/)?.[1] ?? '';
|
|
192
|
+
return {
|
|
193
|
+
tool: 'section',
|
|
194
|
+
id: attr(attrs, 'data-id') || '',
|
|
195
|
+
x: num(attr(rectAttrs, 'x')),
|
|
196
|
+
y: num(attr(rectAttrs, 'y')),
|
|
197
|
+
w: num(attr(rectAttrs, 'width')),
|
|
198
|
+
h: num(attr(rectAttrs, 'height')),
|
|
199
|
+
text: decodeEntities(attr(attrs, 'data-label') || ''),
|
|
200
|
+
color: attr(attrs, 'fill') || null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
function readSticky(attrs, inner) {
|
|
176
205
|
// Geometry off the inner <rect>; paper tint off the group fill; body off the
|
|
177
206
|
// inner <text>. (annotations-layer.tsx strokeToSvgEl 'sticky' branch.)
|
|
@@ -189,6 +218,20 @@ function readSticky(attrs, inner) {
|
|
|
189
218
|
};
|
|
190
219
|
}
|
|
191
220
|
|
|
221
|
+
// FigJam v3 — a bound endpoint persists as `data-(start|end)-bind="<hostId>
|
|
222
|
+
// <nx> <ny>"`. Malformed / out-of-range values are rejected (mirror of the
|
|
223
|
+
// canonical parser's clamp).
|
|
224
|
+
function parseBind(raw) {
|
|
225
|
+
if (!raw) return null;
|
|
226
|
+
const parts = String(raw).trim().split(/\s+/);
|
|
227
|
+
if (parts.length !== 3) return null;
|
|
228
|
+
const nx = Number.parseFloat(parts[1]);
|
|
229
|
+
const ny = Number.parseFloat(parts[2]);
|
|
230
|
+
if (!parts[0] || !Number.isFinite(nx) || !Number.isFinite(ny)) return null;
|
|
231
|
+
if (nx < 0 || nx > 1 || ny < 0 || ny > 1) return null;
|
|
232
|
+
return { hostId: parts[0], nx, ny };
|
|
233
|
+
}
|
|
234
|
+
|
|
192
235
|
function readArrow(attrs, inner) {
|
|
193
236
|
const id = attr(attrs, 'data-id') || '';
|
|
194
237
|
const color = attr(attrs, 'stroke') || null;
|
|
@@ -325,34 +368,92 @@ function readUnknown(tool, attrs, inner) {
|
|
|
325
368
|
// and NO container nests inside the same container — so a non-greedy backref
|
|
326
369
|
// match recovers each top-level element in document order. (Task-2 drift guard
|
|
327
370
|
// keeps this invariant honest against the canonical serializer.)
|
|
371
|
+
// FigJam v3 — shared root attrs every tool can carry: group membership,
|
|
372
|
+
// provenance, render order (z = document position). Applied uniformly after
|
|
373
|
+
// the per-tool extraction; arrows additionally surface bound endpoints as
|
|
374
|
+
// `from`/`to` host ids so a bound diagram reads back as a GRAPH.
|
|
375
|
+
function withShared(o, attrs, z) {
|
|
376
|
+
o.z = z;
|
|
377
|
+
const g = attr(attrs, 'data-group-ids');
|
|
378
|
+
if (g) {
|
|
379
|
+
const ids = g.split(/\s+/).filter(Boolean);
|
|
380
|
+
if (ids.length) o.groupIds = ids;
|
|
381
|
+
}
|
|
382
|
+
if (attr(attrs, 'data-author') === 'ai') o.author = 'ai';
|
|
383
|
+
if (o.tool === 'arrow') {
|
|
384
|
+
const sb = parseBind(attr(attrs, 'data-start-bind'));
|
|
385
|
+
if (sb) o.from = sb.hostId;
|
|
386
|
+
const eb = parseBind(attr(attrs, 'data-end-bind'));
|
|
387
|
+
if (eb) o.to = eb.hostId;
|
|
388
|
+
}
|
|
389
|
+
return o;
|
|
390
|
+
}
|
|
391
|
+
|
|
328
392
|
function parseAnnotations(svg) {
|
|
329
393
|
const out = [];
|
|
330
394
|
if (!svg || !/<svg[\s>]/i.test(svg)) return out;
|
|
331
|
-
const re =
|
|
395
|
+
const re =
|
|
396
|
+
/<(g|text)\b([^>]*)>([\s\S]*?)<\/\1>|<(path|rect|ellipse|polygon|image)\b([^>]*?)\/?>/g;
|
|
332
397
|
let m = re.exec(svg);
|
|
333
398
|
while (m !== null) {
|
|
334
399
|
if (m[1]) {
|
|
335
400
|
const attrs = m[2];
|
|
336
401
|
const inner = m[3];
|
|
337
402
|
const tool = attr(attrs, 'data-tool');
|
|
338
|
-
if (tool === 'sticky') out.push(readSticky(attrs, inner));
|
|
339
|
-
else if (tool === '
|
|
340
|
-
|
|
341
|
-
else if (tool) out.push(
|
|
403
|
+
if (tool === 'sticky') out.push(withShared(readSticky(attrs, inner), attrs, out.length));
|
|
404
|
+
else if (tool === 'section')
|
|
405
|
+
out.push(withShared(readSection(attrs, inner), attrs, out.length));
|
|
406
|
+
else if (tool === 'arrow') out.push(withShared(readArrow(attrs, inner), attrs, out.length));
|
|
407
|
+
else if (tool === 'text') out.push(withShared(readText(attrs, inner), attrs, out.length));
|
|
408
|
+
else if (tool) out.push(withShared(readUnknown(tool, attrs, inner), attrs, out.length));
|
|
342
409
|
} else {
|
|
343
410
|
const attrs = m[5];
|
|
344
411
|
const tool = attr(attrs, 'data-tool');
|
|
345
|
-
if (tool === 'pen') out.push(readPen(attrs));
|
|
346
|
-
else if (tool === 'rect') out.push(readRect(attrs));
|
|
347
|
-
else if (tool === 'ellipse') out.push(readEllipse(attrs));
|
|
348
|
-
else if (tool === 'polygon') out.push(readPolygon(attrs));
|
|
349
|
-
else if (tool) out.push(readUnknown(tool, attrs, ''));
|
|
412
|
+
if (tool === 'pen') out.push(withShared(readPen(attrs), attrs, out.length));
|
|
413
|
+
else if (tool === 'rect') out.push(withShared(readRect(attrs), attrs, out.length));
|
|
414
|
+
else if (tool === 'ellipse') out.push(withShared(readEllipse(attrs), attrs, out.length));
|
|
415
|
+
else if (tool === 'polygon') out.push(withShared(readPolygon(attrs), attrs, out.length));
|
|
416
|
+
else if (tool) out.push(withShared(readUnknown(tool, attrs, ''), attrs, out.length));
|
|
350
417
|
}
|
|
351
418
|
m = re.exec(svg);
|
|
352
419
|
}
|
|
353
420
|
return out;
|
|
354
421
|
}
|
|
355
422
|
|
|
423
|
+
// FigJam v3 — derive the node/edge view of a bound diagram. Edges are arrows
|
|
424
|
+
// with at least one bound end; nodes are the strokes those arrows reference,
|
|
425
|
+
// labelled by their own text or by anchored text targeting them.
|
|
426
|
+
function buildGraph(annotations) {
|
|
427
|
+
const edges = [];
|
|
428
|
+
for (const a of annotations) {
|
|
429
|
+
if (a.tool === 'arrow' && (a.from || a.to)) {
|
|
430
|
+
edges.push({ id: a.id, from: a.from ?? null, to: a.to ?? null });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const refIds = new Set();
|
|
434
|
+
for (const e of edges) {
|
|
435
|
+
if (e.from) refIds.add(e.from);
|
|
436
|
+
if (e.to) refIds.add(e.to);
|
|
437
|
+
}
|
|
438
|
+
const labelByAnchor = new Map();
|
|
439
|
+
for (const a of annotations) {
|
|
440
|
+
if (a.tool === 'text' && a.anchorId && a.text) labelByAnchor.set(a.anchorId, a.text);
|
|
441
|
+
}
|
|
442
|
+
const nodes = annotations
|
|
443
|
+
.filter((a) => refIds.has(a.id))
|
|
444
|
+
.map((a) => ({
|
|
445
|
+
id: a.id,
|
|
446
|
+
tool: a.tool,
|
|
447
|
+
label: a.text || labelByAnchor.get(a.id) || null,
|
|
448
|
+
x: a.x,
|
|
449
|
+
y: a.y,
|
|
450
|
+
w: a.w,
|
|
451
|
+
h: a.h,
|
|
452
|
+
artboard: a.artboard ?? null,
|
|
453
|
+
}));
|
|
454
|
+
return { nodes, edges };
|
|
455
|
+
}
|
|
456
|
+
|
|
356
457
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
458
|
// --canvas-state artboard overlap tagging.
|
|
358
459
|
|
|
@@ -374,18 +475,43 @@ function loadArtboards(p) {
|
|
|
374
475
|
}
|
|
375
476
|
}
|
|
376
477
|
|
|
377
|
-
function
|
|
478
|
+
function findArtboard(ann, artboards) {
|
|
378
479
|
if (ann.x == null || ann.y == null) return null;
|
|
379
480
|
const ax2 = ann.x + (ann.w || 0);
|
|
380
481
|
const ay2 = ann.y + (ann.h || 0);
|
|
381
482
|
for (const r of artboards) {
|
|
382
483
|
const rx2 = r.x + r.w;
|
|
383
484
|
const ry2 = r.y + r.h;
|
|
384
|
-
if (ann.x <= rx2 && ax2 >= r.x && ann.y <= ry2 && ay2 >= r.y) return r
|
|
485
|
+
if (ann.x <= rx2 && ax2 >= r.x && ann.y <= ry2 && ay2 >= r.y) return r;
|
|
385
486
|
}
|
|
386
487
|
return null;
|
|
387
488
|
}
|
|
388
489
|
|
|
490
|
+
function tagArtboard(ann, artboards) {
|
|
491
|
+
return findArtboard(ann, artboards)?.id ?? null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* FigJam v3 — anchor an annotation to its artboard for AI consumers: the
|
|
496
|
+
* overlapping artboard id, artboard-RELATIVE coords (what survives an artboard
|
|
497
|
+
* move), and a W3C Web-Annotation-style target (anchor by stable id first,
|
|
498
|
+
* geometry as the refinement/fallback).
|
|
499
|
+
*/
|
|
500
|
+
function anchorToArtboard(ann, artboards) {
|
|
501
|
+
const r = findArtboard(ann, artboards);
|
|
502
|
+
if (!r) return { ...ann, artboard: null };
|
|
503
|
+
return {
|
|
504
|
+
...ann,
|
|
505
|
+
artboard: r.id,
|
|
506
|
+
rel: ann.x != null ? { x: ann.x - r.x, y: ann.y - r.y } : null,
|
|
507
|
+
target: {
|
|
508
|
+
source: r.id,
|
|
509
|
+
selector: { type: 'AnnotationIdSelector', value: ann.id },
|
|
510
|
+
geometry: ann.x != null ? { x: ann.x, y: ann.y, w: ann.w ?? 0, h: ann.h ?? 0 } : null,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
389
515
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
390
516
|
// Main
|
|
391
517
|
|
|
@@ -428,11 +554,20 @@ function main() {
|
|
|
428
554
|
: resolve(process.cwd(), args.canvasState);
|
|
429
555
|
const artboards = loadArtboards(csPath);
|
|
430
556
|
if (artboards.length) {
|
|
431
|
-
annotations = annotations.map((a) => (
|
|
557
|
+
annotations = annotations.map((a) => anchorToArtboard(a, artboards));
|
|
432
558
|
}
|
|
433
559
|
}
|
|
434
560
|
|
|
561
|
+
if (args.graph) {
|
|
562
|
+
process.stdout.write(`${JSON.stringify({ annotations, graph: buildGraph(annotations) })}\n`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
435
565
|
process.stdout.write(`${JSON.stringify(annotations)}\n`);
|
|
436
566
|
}
|
|
437
567
|
|
|
438
|
-
|
|
568
|
+
// FigJam v3 — the parsing core is importable (the `annotate` write verb reuses
|
|
569
|
+
// it for host-geometry lookups); main() runs only when invoked as a script.
|
|
570
|
+
export { buildGraph, fileSlug, loadArtboards, parseAnnotations, resolveDesignRoot, tagArtboard };
|
|
571
|
+
|
|
572
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
573
|
+
if (isMain) main();
|
package/apps/studio/build.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Per DDR-009 (Bun runtime authoritative) + DDR-012 (React 19 unified) + DDR-014 (Lightning CSS).
|
|
17
17
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
-
import { dirname, join, relative
|
|
19
|
+
import { dirname, join, relative } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
|
|
22
22
|
import { browserslistToTargets, bundle as lcssBundle } from 'lightningcss';
|
|
@@ -59,6 +59,28 @@ export interface ArrowGeom {
|
|
|
59
59
|
endHead?: ArrowHead;
|
|
60
60
|
dashed?: boolean;
|
|
61
61
|
lineType?: ArrowLineType;
|
|
62
|
+
/**
|
|
63
|
+
* FigJam v3 — magnetic binding anchors (normalized over the host bbox;
|
|
64
|
+
* `ArrowBind` is structurally assignable). A BOUND curved arrow exits its
|
|
65
|
+
* host perpendicular to the bound side (cubic with exit normals — the
|
|
66
|
+
* FigJam connector look) and takes the sleeker connector head; unbound
|
|
67
|
+
* arrows keep the legacy byte-identical geometry.
|
|
68
|
+
*/
|
|
69
|
+
startBind?: { nx: number; ny: number };
|
|
70
|
+
endBind?: { nx: number; ny: number };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Exit direction OUT of a bound side — the normalized vector from the host's
|
|
75
|
+
* center toward the anchor magnet. A center anchor (0.5, 0.5) has no exit
|
|
76
|
+
* direction (null → the curve falls back to chord-based control points).
|
|
77
|
+
*/
|
|
78
|
+
function bindNormal(b: { nx: number; ny: number }): [number, number] | null {
|
|
79
|
+
const dx = b.nx - 0.5;
|
|
80
|
+
const dy = b.ny - 0.5;
|
|
81
|
+
const len = Math.hypot(dx, dy);
|
|
82
|
+
if (len < 0.05) return null;
|
|
83
|
+
return [dx / len, dy / len];
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
/**
|
|
@@ -73,10 +95,13 @@ export function arrowHeadPoints(
|
|
|
73
95
|
y1: number,
|
|
74
96
|
x2: number,
|
|
75
97
|
y2: number,
|
|
76
|
-
width: number
|
|
98
|
+
width: number,
|
|
99
|
+
// FigJam v3 — optional head length override (sleeker connector heads on
|
|
100
|
+
// BOUND arrows). Default = the legacy byte-frozen formula.
|
|
101
|
+
lenOverride?: number
|
|
77
102
|
): string {
|
|
78
103
|
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
79
|
-
const len = 12 + width * 2;
|
|
104
|
+
const len = lenOverride ?? 12 + width * 2;
|
|
80
105
|
const wing = Math.PI / 7;
|
|
81
106
|
const ax = x2 - Math.cos(angle - wing) * len;
|
|
82
107
|
const ay = y2 - Math.sin(angle - wing) * len;
|
|
@@ -154,25 +179,39 @@ function headPrimitives(
|
|
|
154
179
|
fromX: number,
|
|
155
180
|
fromY: number,
|
|
156
181
|
width: number,
|
|
157
|
-
color: string
|
|
182
|
+
color: string,
|
|
183
|
+
lenOverride?: number
|
|
158
184
|
): SvgPrimitive[] {
|
|
159
185
|
switch (head) {
|
|
160
186
|
case 'none':
|
|
161
187
|
return [];
|
|
162
188
|
case 'triangle':
|
|
163
|
-
// Legacy filled head — byte-identical to Phase 5.1
|
|
189
|
+
// Legacy filled head — byte-identical to Phase 5.1 (unless a bound
|
|
190
|
+
// arrow passes the sleeker connector length).
|
|
164
191
|
return [
|
|
165
|
-
{
|
|
192
|
+
{
|
|
193
|
+
el: 'polyline',
|
|
194
|
+
points: arrowHeadPoints(fromX, fromY, tipX, tipY, width, lenOverride),
|
|
195
|
+
fill: color,
|
|
196
|
+
},
|
|
166
197
|
];
|
|
167
198
|
case 'line':
|
|
168
199
|
// Open chevron — same 3 points, unfilled (no implicit close).
|
|
169
200
|
return [
|
|
170
|
-
{
|
|
201
|
+
{
|
|
202
|
+
el: 'polyline',
|
|
203
|
+
points: arrowHeadPoints(fromX, fromY, tipX, tipY, width, lenOverride),
|
|
204
|
+
fill: 'none',
|
|
205
|
+
},
|
|
171
206
|
];
|
|
172
207
|
case 'triangle-outline':
|
|
173
208
|
// Closed triangle outline — polygon auto-closes the third edge.
|
|
174
209
|
return [
|
|
175
|
-
{
|
|
210
|
+
{
|
|
211
|
+
el: 'polygon',
|
|
212
|
+
points: arrowHeadPoints(fromX, fromY, tipX, tipY, width, lenOverride),
|
|
213
|
+
fill: 'none',
|
|
214
|
+
},
|
|
176
215
|
];
|
|
177
216
|
case 'diamond':
|
|
178
217
|
return [
|
|
@@ -199,6 +238,7 @@ export function arrowPrimitives(s: ArrowGeom): SvgPrimitive[] {
|
|
|
199
238
|
const endHead = s.endHead ?? 'triangle';
|
|
200
239
|
const dashed = s.dashed ?? false;
|
|
201
240
|
const lineType = s.lineType ?? 'straight';
|
|
241
|
+
const bound = Boolean(s.startBind || s.endBind);
|
|
202
242
|
const out: SvgPrimitive[] = [];
|
|
203
243
|
|
|
204
244
|
// Default tangent sources (straight): each head points away from the far end.
|
|
@@ -207,6 +247,28 @@ export function arrowPrimitives(s: ArrowGeom): SvgPrimitive[] {
|
|
|
207
247
|
|
|
208
248
|
if (lineType === 'straight') {
|
|
209
249
|
out.push({ el: 'line', x1: s.x1, y1: s.y1, x2: s.x2, y2: s.y2, dash: dashed });
|
|
250
|
+
} else if (lineType === 'curved' && bound) {
|
|
251
|
+
// FigJam v3 — connector curve: a cubic whose control points extend along
|
|
252
|
+
// each bound side's EXIT NORMAL (perpendicular out of the side, then
|
|
253
|
+
// toward the target) — the smooth "leaves the box sideways" look. A free
|
|
254
|
+
// / center-anchored end falls back to a chord-aligned control point.
|
|
255
|
+
const dx = s.x2 - s.x1;
|
|
256
|
+
const dy = s.y2 - s.y1;
|
|
257
|
+
const chord = Math.hypot(dx, dy) || 1;
|
|
258
|
+
const reach = Math.min(160, Math.max(24, chord * 0.4));
|
|
259
|
+
const n1 = s.startBind ? bindNormal(s.startBind) : null;
|
|
260
|
+
const n2 = s.endBind ? bindNormal(s.endBind) : null;
|
|
261
|
+
const c1x = s.x1 + (n1 ? n1[0] : dx / chord) * reach;
|
|
262
|
+
const c1y = s.y1 + (n1 ? n1[1] : dy / chord) * reach;
|
|
263
|
+
const c2x = s.x2 + (n2 ? n2[0] : -dx / chord) * reach;
|
|
264
|
+
const c2y = s.y2 + (n2 ? n2[1] : -dy / chord) * reach;
|
|
265
|
+
out.push({
|
|
266
|
+
el: 'path',
|
|
267
|
+
d: `M${s.x1} ${s.y1} C${c1x} ${c1y} ${c2x} ${c2y} ${s.x2} ${s.y2}`,
|
|
268
|
+
dash: dashed,
|
|
269
|
+
});
|
|
270
|
+
startFrom = [c1x, c1y];
|
|
271
|
+
endFrom = [c2x, c2y];
|
|
210
272
|
} else {
|
|
211
273
|
const shaft = shaftPath(s.x1, s.y1, s.x2, s.y2, lineType);
|
|
212
274
|
out.push({ el: 'path', d: shaft.d, dash: dashed });
|
|
@@ -214,7 +276,14 @@ export function arrowPrimitives(s: ArrowGeom): SvgPrimitive[] {
|
|
|
214
276
|
endFrom = shaft.endFrom;
|
|
215
277
|
}
|
|
216
278
|
|
|
217
|
-
|
|
218
|
-
|
|
279
|
+
// Bound arrows take the sleeker connector head; unbound keep the legacy
|
|
280
|
+
// byte-frozen proportions.
|
|
281
|
+
const headLen = bound ? 9 + s.width * 1.4 : undefined;
|
|
282
|
+
out.push(
|
|
283
|
+
...headPrimitives(startHead, s.x1, s.y1, startFrom[0], startFrom[1], s.width, s.color, headLen)
|
|
284
|
+
);
|
|
285
|
+
out.push(
|
|
286
|
+
...headPrimitives(endHead, s.x2, s.y2, endFrom[0], endFrom[1], s.width, s.color, headLen)
|
|
287
|
+
);
|
|
219
288
|
return out;
|
|
220
289
|
}
|
|
@@ -171,6 +171,8 @@ export const TOOL_CURSORS: Record<Tool, string> = Object.freeze({
|
|
|
171
171
|
hand: HAND,
|
|
172
172
|
comment: COMMENT,
|
|
173
173
|
pen: PEN,
|
|
174
|
+
// FigJam v3 — the section container draws like a shape (crosshair glyph).
|
|
175
|
+
section: CROSSHAIR,
|
|
174
176
|
// Highlighter = Kenney `drawing_pencil` rot180; pen = `drawing_pen` rot180 —
|
|
175
177
|
// distinct glyphs, both from the Kenney pack (user steer 2026-06-04).
|
|
176
178
|
highlighter: HIGHLIGHTER,
|