@1agh/maude 0.40.0 → 0.41.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/apps/studio/ai-banner.tsx +2 -2
- package/apps/studio/annotations-layer.tsx +53 -2
- package/apps/studio/api.ts +504 -3
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +744 -11
- package/apps/studio/canvas-lib.tsx +219 -2
- package/apps/studio/canvas-shell.tsx +487 -20
- package/apps/studio/client/app.jsx +1451 -22
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/github.js +8 -0
- package/apps/studio/client/styles/3-shell-maude.css +48 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/context-menu.tsx +15 -5
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +20 -20
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +1 -1
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +223 -0
- package/apps/studio/input-router.tsx +12 -0
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-edit.test.ts +50 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +18 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/whats-new.json +28 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# canvas-rects.sh — the whiteboard AI toolkit's geometry manifest
|
|
3
|
+
# (feature-whiteboard-ai-toolkit). Emits `{ artboards, elements,
|
|
4
|
+
# elementsTruncated }` in WORLD coordinates for a canvas — the file
|
|
5
|
+
# `read-annotations --rects` and `annotate --in/--pin/--board` consume so
|
|
6
|
+
# neither verb ever hand-computes a coordinate. Reached via
|
|
7
|
+
# `maude design canvas-rects` (DDR-062), never a raw bin path.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# canvas-rects.sh <rel-path> [--root <repo>] [--port N] [--timeout 8]
|
|
11
|
+
# [--engine auto|agent-browser|playwright]
|
|
12
|
+
#
|
|
13
|
+
# Two lanes:
|
|
14
|
+
# - LIVE (preferred) — when a dev server is reachable (_server.json or
|
|
15
|
+
# --port), navigates the canvas-shell URL and reads
|
|
16
|
+
# `window.__maudeCanvasRects()` (canvas-lib.tsx) so element rects reflect
|
|
17
|
+
# actual CSS/content. Engine: agent-browser (default, matches
|
|
18
|
+
# screenshot.sh) or playwright (--engine playwright, or auto-fallback when
|
|
19
|
+
# agent-browser isn't on PATH).
|
|
20
|
+
# - STATIC fallback — no server reachable: artboard positions/sizes only
|
|
21
|
+
# (meta.json + JSX width/height, DDR-027), `elements: []`. No browser
|
|
22
|
+
# needed. A stderr note explains the degraded result.
|
|
23
|
+
#
|
|
24
|
+
# Unlike screenshot.sh this has no port-bounce retry (accepted v1 simplification
|
|
25
|
+
# — read-annotations/annotate don't have one either); a stale port just falls
|
|
26
|
+
# through to the static lane.
|
|
27
|
+
#
|
|
28
|
+
# Stdout: the manifest JSON (one line). Stderr: diagnostics/engine choice.
|
|
29
|
+
# Exit: 0 on success (either lane) / 2 on bad args.
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
32
|
+
ROOT=""
|
|
33
|
+
PORT=""
|
|
34
|
+
TIMEOUT=8
|
|
35
|
+
ENGINE="auto"
|
|
36
|
+
REL=""
|
|
37
|
+
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--root) ROOT="$2"; shift 2 ;;
|
|
41
|
+
--port) PORT="$2"; shift 2 ;;
|
|
42
|
+
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
43
|
+
--engine) ENGINE="$2"; shift 2 ;;
|
|
44
|
+
--help|-h) sed -n '2,24p' "$0" | sed 's/^# \?//'; exit 0 ;;
|
|
45
|
+
-*) echo "canvas-rects.sh: unknown arg '$1' (try --help)" >&2; exit 2 ;;
|
|
46
|
+
*) REL="$1"; shift ;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
[ -n "$REL" ] || { echo "canvas-rects.sh: missing <rel-path>" >&2; exit 2; }
|
|
51
|
+
|
|
52
|
+
REPO="${ROOT:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
|
|
53
|
+
STATE="$REPO/.design/_server.json"
|
|
54
|
+
|
|
55
|
+
emit_static() {
|
|
56
|
+
node "$SCRIPT_DIR/_canvas-rects-static.mjs" --rel "$REL" --root "$REPO"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ---------- server detection ----------
|
|
60
|
+
if [ -z "$PORT" ] && [ -f "$STATE" ]; then
|
|
61
|
+
if command -v jq >/dev/null 2>&1; then
|
|
62
|
+
PORT=$(jq -r .port "$STATE" 2>/dev/null)
|
|
63
|
+
else
|
|
64
|
+
PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ -z "$PORT" ] || ! curl -fsS -o /dev/null -m 2 "http://localhost:$PORT/_health" 2>/dev/null; then
|
|
69
|
+
echo "→ no live dev server reachable — static artboard-only fallback" >&2
|
|
70
|
+
emit_static
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ---------- designRel (for the canvas= query param) ----------
|
|
75
|
+
DESIGN_REL=".design"
|
|
76
|
+
CFG="$REPO/.design/config.json"
|
|
77
|
+
if [ -f "$CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
78
|
+
CAND=$(jq -r '.designRoot // empty' "$CFG" 2>/dev/null)
|
|
79
|
+
[ -n "$CAND" ] && DESIGN_REL="$CAND"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Canvases mount through the canvas shell; the bare `/<rel>` route 404s when
|
|
83
|
+
# the canvas-origin sandbox is on (default since phase-9.1) — mirrors
|
|
84
|
+
# screenshot.sh's URL construction.
|
|
85
|
+
REL_ENC=$(printf '%s' "$DESIGN_REL/$REL" | sed 's/ /%20/g')
|
|
86
|
+
URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${REL_ENC}"
|
|
87
|
+
|
|
88
|
+
# ---------- engine resolution (mirrors screenshot.sh) ----------
|
|
89
|
+
AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
|
|
90
|
+
if [ "$ENGINE" = "auto" ]; then
|
|
91
|
+
if command -v "$AB" >/dev/null 2>&1; then
|
|
92
|
+
ENGINE="agent-browser"
|
|
93
|
+
else
|
|
94
|
+
ENGINE="playwright"
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
echo "→ canvas-rects engine: $ENGINE | url: $URL" >&2
|
|
98
|
+
|
|
99
|
+
# ---------- agent-browser lane ----------
|
|
100
|
+
if [ "$ENGINE" = "agent-browser" ]; then
|
|
101
|
+
if ! command -v "$AB" >/dev/null 2>&1; then
|
|
102
|
+
echo "→ agent-browser not on PATH — static artboard-only fallback" >&2
|
|
103
|
+
emit_static
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
"$AB" open "$URL" >&2
|
|
107
|
+
|
|
108
|
+
# Poll for the hook itself (more precise than screenshot.sh's generic
|
|
109
|
+
# [data-dc-screen] mount check — this is exactly what we're about to call).
|
|
110
|
+
poll=0
|
|
111
|
+
ready=0
|
|
112
|
+
while [ $poll -lt "$TIMEOUT" ]; do
|
|
113
|
+
sleep 1
|
|
114
|
+
poll=$((poll + 1))
|
|
115
|
+
raw=$("$AB" eval "typeof window.__maudeCanvasRects" 2>/dev/null)
|
|
116
|
+
case "$raw" in
|
|
117
|
+
*function*) ready=1; break ;;
|
|
118
|
+
esac
|
|
119
|
+
done
|
|
120
|
+
if [ $ready -eq 0 ]; then
|
|
121
|
+
echo "→ hook never installed after ${TIMEOUT}s (non-canvas page?) — static artboard-only fallback" >&2
|
|
122
|
+
emit_static
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
RAW_JSON=$("$AB" eval "JSON.stringify(window.__maudeCanvasRects())" 2>/dev/null)
|
|
127
|
+
if [ -z "$RAW_JSON" ]; then
|
|
128
|
+
echo "→ eval returned nothing — static artboard-only fallback" >&2
|
|
129
|
+
emit_static
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
if command -v jq >/dev/null 2>&1; then
|
|
133
|
+
printf '%s\n' "$RAW_JSON" | jq -r .
|
|
134
|
+
else
|
|
135
|
+
# No jq — agent-browser's output is a JSON-encoded STRING (quoted,
|
|
136
|
+
# escaped); node unwraps it the same way `jq -r .` would.
|
|
137
|
+
printf '%s' "$RAW_JSON" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s)+'\n'))"
|
|
138
|
+
fi
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# ---------- playwright lane ----------
|
|
143
|
+
if [ "$ENGINE" = "playwright" ]; then
|
|
144
|
+
if ! node "$SCRIPT_DIR/_canvas-rects-playwright.mjs" --url "$URL" --timeout "$TIMEOUT"; then
|
|
145
|
+
echo "→ playwright capture failed — static artboard-only fallback" >&2
|
|
146
|
+
emit_static
|
|
147
|
+
fi
|
|
148
|
+
exit 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
echo "canvas-rects.sh: unknown --engine '$ENGINE'" >&2
|
|
152
|
+
exit 2
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fetch-asset.sh — hardened download-first image fetch. Thin shim over
|
|
3
|
+
# _fetch-asset.mjs; reached via `maude design fetch-asset` (DDR-062), never a
|
|
4
|
+
# raw bin path. See _fetch-asset.mjs for the full security rationale
|
|
5
|
+
# (DDR-147 § Security follow-up item 1).
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# fetch-asset.sh <https-url> --root <repo> [--design-root .design]
|
|
9
|
+
# [--max-bytes N] [--max-time S] [--json]
|
|
10
|
+
#
|
|
11
|
+
# stdout on success = the canvas reference path (e.g. /assets/a44d3d60.png).
|
|
12
|
+
# Exit: 0 ok · 2 usage · 3 SSRF/validation reject · 4 download/http error ·
|
|
13
|
+
# 5 unsupported media type · 6 write/containment error · 1 other.
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
|
|
16
|
+
case "$1" in
|
|
17
|
+
--help|-h) sed -n '2,17p' "$0" | sed 's/^# \?//'; exit 0 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
21
|
+
echo "fetch-asset.sh: curl is required (hardened download uses fixed curl args)." >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Prefer node (always present with a maude install); fall back to bun in a dev
|
|
26
|
+
# tree that has bun but a shimmed node. The module is pure Node ESM — no .ts.
|
|
27
|
+
if command -v node >/dev/null 2>&1; then
|
|
28
|
+
exec node "$SCRIPT_DIR/_fetch-asset.mjs" "$@"
|
|
29
|
+
elif command -v bun >/dev/null 2>&1; then
|
|
30
|
+
exec bun run "$SCRIPT_DIR/_fetch-asset.mjs" "$@"
|
|
31
|
+
else
|
|
32
|
+
echo "fetch-asset.sh: node (or bun) is required." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
@@ -37,7 +37,14 @@ import { pathToFileURL } from 'node:url';
|
|
|
37
37
|
// Argv
|
|
38
38
|
|
|
39
39
|
function parseArgv(argv) {
|
|
40
|
-
const out = {
|
|
40
|
+
const out = {
|
|
41
|
+
positional: [],
|
|
42
|
+
root: null,
|
|
43
|
+
canvasState: null,
|
|
44
|
+
rects: null,
|
|
45
|
+
help: false,
|
|
46
|
+
graph: false,
|
|
47
|
+
};
|
|
41
48
|
for (let i = 0; i < argv.length; i += 1) {
|
|
42
49
|
const a = argv[i];
|
|
43
50
|
if (a === '--help' || a === '-h') {
|
|
@@ -56,6 +63,11 @@ function parseArgv(argv) {
|
|
|
56
63
|
out.canvasState = argv[i];
|
|
57
64
|
} else if (a.startsWith('--canvas-state=')) {
|
|
58
65
|
out.canvasState = a.slice('--canvas-state='.length);
|
|
66
|
+
} else if (a === '--rects') {
|
|
67
|
+
i += 1;
|
|
68
|
+
out.rects = argv[i];
|
|
69
|
+
} else if (a.startsWith('--rects=')) {
|
|
70
|
+
out.rects = a.slice('--rects='.length);
|
|
59
71
|
} else {
|
|
60
72
|
out.positional.push(a);
|
|
61
73
|
}
|
|
@@ -67,6 +79,7 @@ const HELP = `read-annotations.mjs — headless SVG → JSON annotation reader (
|
|
|
67
79
|
|
|
68
80
|
Usage:
|
|
69
81
|
maude design read-annotations <rel-path> [--root <repo>] [--canvas-state <path>]
|
|
82
|
+
[--rects <path>]
|
|
70
83
|
|
|
71
84
|
Args:
|
|
72
85
|
<rel-path> Canvas path relative to the design root (e.g. "ui/Foo.tsx").
|
|
@@ -75,6 +88,16 @@ Args:
|
|
|
75
88
|
each annotation is tagged with the artboard it overlaps,
|
|
76
89
|
plus artboard-relative coords (rel) and a W3C-style
|
|
77
90
|
target block { source, selector, geometry }.
|
|
91
|
+
--rects <p> A \`maude design canvas-rects\` geometry manifest
|
|
92
|
+
({ artboards, elements }) — feature-whiteboard-ai-toolkit.
|
|
93
|
+
Adds ELEMENT-level context: an annotation whose center
|
|
94
|
+
falls inside an element's world rect gets
|
|
95
|
+
element: { cdId, selector, index, artboard, rect, tag,
|
|
96
|
+
text } (or element: null). Also supplies artboard
|
|
97
|
+
tagging (as --canvas-state does) when --canvas-state is
|
|
98
|
+
not separately given. When an element resolves, the W3C
|
|
99
|
+
target.selector upgrades from AnnotationIdSelector to
|
|
100
|
+
{ type: "CssSelector", value: <element.selector> }.
|
|
78
101
|
--graph Emit { annotations, graph } instead of the bare array:
|
|
79
102
|
graph.edges = bound arrows (from/to host ids), graph.nodes
|
|
80
103
|
= the strokes those arrows connect (with labels) — a
|
|
@@ -495,23 +518,103 @@ function tagArtboard(ann, artboards) {
|
|
|
495
518
|
* FigJam v3 — anchor an annotation to its artboard for AI consumers: the
|
|
496
519
|
* overlapping artboard id, artboard-RELATIVE coords (what survives an artboard
|
|
497
520
|
* move), and a W3C Web-Annotation-style target (anchor by stable id first,
|
|
498
|
-
* geometry as the refinement/fallback).
|
|
521
|
+
* geometry as the refinement/fallback). feature-whiteboard-ai-toolkit — when
|
|
522
|
+
* `anchorToElement` (below) already resolved `ann.element`, the target
|
|
523
|
+
* selector upgrades to a CssSelector naming that element instead of the bare
|
|
524
|
+
* AnnotationIdSelector.
|
|
499
525
|
*/
|
|
500
526
|
function anchorToArtboard(ann, artboards) {
|
|
501
527
|
const r = findArtboard(ann, artboards);
|
|
502
528
|
if (!r) return { ...ann, artboard: null };
|
|
529
|
+
const selector = ann.element
|
|
530
|
+
? { type: 'CssSelector', value: ann.element.selector }
|
|
531
|
+
: { type: 'AnnotationIdSelector', value: ann.id };
|
|
503
532
|
return {
|
|
504
533
|
...ann,
|
|
505
534
|
artboard: r.id,
|
|
506
535
|
rel: ann.x != null ? { x: ann.x - r.x, y: ann.y - r.y } : null,
|
|
507
536
|
target: {
|
|
508
537
|
source: r.id,
|
|
509
|
-
selector
|
|
538
|
+
selector,
|
|
510
539
|
geometry: ann.x != null ? { x: ann.x, y: ann.y, w: ann.w ?? 0, h: ann.h ?? 0 } : null,
|
|
511
540
|
},
|
|
512
541
|
};
|
|
513
542
|
}
|
|
514
543
|
|
|
544
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
545
|
+
// --rects element-level context (feature-whiteboard-ai-toolkit). Reads a
|
|
546
|
+
// `maude design canvas-rects` geometry manifest ({ artboards, elements }) —
|
|
547
|
+
// `loadArtboards` above already understands `raw.artboards`, so only the
|
|
548
|
+
// elements side needs a dedicated loader.
|
|
549
|
+
|
|
550
|
+
function loadElements(p) {
|
|
551
|
+
try {
|
|
552
|
+
const raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
553
|
+
const arr = Array.isArray(raw?.elements) ? raw.elements : [];
|
|
554
|
+
return arr.filter(
|
|
555
|
+
(e) =>
|
|
556
|
+
e &&
|
|
557
|
+
typeof e.selector === 'string' &&
|
|
558
|
+
e.selector &&
|
|
559
|
+
[e.x, e.y, e.w, e.h].every((n) => Number.isFinite(n))
|
|
560
|
+
);
|
|
561
|
+
} catch {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Deepest-element approximation over a FLAT rect list (no live DOM to walk,
|
|
568
|
+
* unlike `resolveHoverTarget` in the browser): the smallest-area element whose
|
|
569
|
+
* world rect contains the annotation's CENTER point. Smallest-containing-rect
|
|
570
|
+
* is the standard proxy for "innermost" when only bounding boxes are known —
|
|
571
|
+
* a wrapping card and the button inside it both contain the point, but the
|
|
572
|
+
* button's rect is smaller.
|
|
573
|
+
*/
|
|
574
|
+
function findElement(ann, elements) {
|
|
575
|
+
if (ann.x == null || ann.y == null) return null;
|
|
576
|
+
const cx = ann.x + (ann.w || 0) / 2;
|
|
577
|
+
const cy = ann.y + (ann.h || 0) / 2;
|
|
578
|
+
let best = null;
|
|
579
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
580
|
+
for (const el of elements) {
|
|
581
|
+
if (cx < el.x || cx > el.x + el.w || cy < el.y || cy > el.y + el.h) continue;
|
|
582
|
+
const area = Math.max(0, el.w) * Math.max(0, el.h);
|
|
583
|
+
if (area < bestArea) {
|
|
584
|
+
bestArea = area;
|
|
585
|
+
best = el;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return best;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Look up a manifest element BY IDENTITY (its `cdId` or its full `selector`
|
|
593
|
+
* string) rather than by point — what `annotate --pin <cdId|selector>` needs
|
|
594
|
+
* to resolve a placement target. First match in manifest order.
|
|
595
|
+
*/
|
|
596
|
+
function findElementById(elements, query) {
|
|
597
|
+
if (!query) return null;
|
|
598
|
+
return elements.find((el) => el.cdId === query || el.selector === query) ?? null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function anchorToElement(ann, elements) {
|
|
602
|
+
const el = findElement(ann, elements);
|
|
603
|
+
if (!el) return { ...ann, element: null };
|
|
604
|
+
return {
|
|
605
|
+
...ann,
|
|
606
|
+
element: {
|
|
607
|
+
cdId: el.cdId ?? null,
|
|
608
|
+
selector: el.selector,
|
|
609
|
+
index: el.index ?? 0,
|
|
610
|
+
artboard: el.artboard ?? null,
|
|
611
|
+
rect: { x: el.x, y: el.y, w: el.w, h: el.h },
|
|
612
|
+
tag: el.tag ?? '',
|
|
613
|
+
text: el.text ?? '',
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
515
618
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
516
619
|
// Main
|
|
517
620
|
|
|
@@ -548,14 +651,28 @@ function main() {
|
|
|
548
651
|
|
|
549
652
|
let annotations = parseAnnotations(svg);
|
|
550
653
|
|
|
654
|
+
// feature-whiteboard-ai-toolkit — --rects supplies element-level context
|
|
655
|
+
// (and, absent a separate --canvas-state, artboards too: loadArtboards
|
|
656
|
+
// already understands a canvas-rects manifest's `{ artboards, elements }`
|
|
657
|
+
// shape). Elements are resolved BEFORE artboards so anchorToArtboard can see
|
|
658
|
+
// the already-attached `element` and upgrade its W3C target.selector.
|
|
659
|
+
let artboards = [];
|
|
551
660
|
if (args.canvasState) {
|
|
552
661
|
const csPath = isAbsolute(args.canvasState)
|
|
553
662
|
? args.canvasState
|
|
554
663
|
: resolve(process.cwd(), args.canvasState);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
664
|
+
artboards = loadArtboards(csPath);
|
|
665
|
+
}
|
|
666
|
+
if (args.rects) {
|
|
667
|
+
const rectsPath = isAbsolute(args.rects) ? args.rects : resolve(process.cwd(), args.rects);
|
|
668
|
+
const elements = loadElements(rectsPath);
|
|
669
|
+
if (elements.length) {
|
|
670
|
+
annotations = annotations.map((a) => anchorToElement(a, elements));
|
|
558
671
|
}
|
|
672
|
+
if (!artboards.length) artboards = loadArtboards(rectsPath);
|
|
673
|
+
}
|
|
674
|
+
if (artboards.length) {
|
|
675
|
+
annotations = annotations.map((a) => anchorToArtboard(a, artboards));
|
|
559
676
|
}
|
|
560
677
|
|
|
561
678
|
if (args.graph) {
|
|
@@ -567,7 +684,21 @@ function main() {
|
|
|
567
684
|
|
|
568
685
|
// FigJam v3 — the parsing core is importable (the `annotate` write verb reuses
|
|
569
686
|
// it for host-geometry lookups); main() runs only when invoked as a script.
|
|
570
|
-
|
|
687
|
+
// feature-whiteboard-ai-toolkit — findElement/loadElements are exported too so
|
|
688
|
+
// `annotate --pin` (Task 4) can resolve an element's rect from the same
|
|
689
|
+
// canvas-rects manifest without a second implementation.
|
|
690
|
+
export {
|
|
691
|
+
anchorToElement,
|
|
692
|
+
buildGraph,
|
|
693
|
+
fileSlug,
|
|
694
|
+
findElement,
|
|
695
|
+
findElementById,
|
|
696
|
+
loadArtboards,
|
|
697
|
+
loadElements,
|
|
698
|
+
parseAnnotations,
|
|
699
|
+
resolveDesignRoot,
|
|
700
|
+
tagArtboard,
|
|
701
|
+
};
|
|
571
702
|
|
|
572
703
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
573
704
|
if (isMain) main();
|
package/apps/studio/bin/smoke.sh
CHANGED
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
# Stdout: one line per canvas, tab-separated:
|
|
41
41
|
# STATUS \t FILE \t SCREENSHOT \t DETAIL
|
|
42
42
|
# Stderr: diagnostic / progress.
|
|
43
|
-
# Exit: 0 = all green / 3 = at least one blank/error/unstyled/lint-fail / 1 = missing deps / 2 = bad args.
|
|
43
|
+
# Exit: 0 = all green / 3 = at least one blank/error/unstyled/broken-img/lint-fail / 1 = missing deps / 2 = bad args.
|
|
44
|
+
#
|
|
45
|
+
# BROKEN-IMG (agent-browser engine only) flags a VISIBLE <img> that finished
|
|
46
|
+
# loading with naturalWidth 0 — a CSP-blocked hotlink or a 404'd local asset the
|
|
47
|
+
# user sees as an empty thumbnail. onError-hidden scrap fallbacks are excluded.
|
|
44
48
|
|
|
45
49
|
REPO=""
|
|
46
50
|
OUT_DIR=""
|
|
@@ -356,6 +360,37 @@ probe_agent_browser() {
|
|
|
356
360
|
return
|
|
357
361
|
fi
|
|
358
362
|
|
|
363
|
+
# Broken-image gate (DDR-147 / 2026-07-06 RCA) — the moodboard + DS specimens
|
|
364
|
+
# fill photo slots with web imagery; a hotlink blocked by the canvas CSP
|
|
365
|
+
# (`img-src 'self'`, default-on split origin) or a 404'd local path renders as
|
|
366
|
+
# an empty "broken image" thumbnail the user sees but every check above sails
|
|
367
|
+
# past (it has text, no error, a >2 KB screenshot). Flag any <img> that
|
|
368
|
+
# FINISHED loading with naturalWidth 0 AND is actually VISIBLE — a slot whose
|
|
369
|
+
# `onError` hid the img and swapped in a scrap (display:none / hidden / opacity
|
|
370
|
+
# 0) is a graceful degrade, NOT a failure, so it's excluded. Catches the class
|
|
371
|
+
# of regression where download-first silently stopped working.
|
|
372
|
+
local broken
|
|
373
|
+
broken=$(agent-browser eval "(() => {
|
|
374
|
+
const bad = [...document.querySelectorAll('img')].filter((im) => {
|
|
375
|
+
if (!im.complete || im.naturalWidth > 0) return false;
|
|
376
|
+
const src = (im.getAttribute('src') || '').trim();
|
|
377
|
+
if (!src || src.startsWith('data:')) return false;
|
|
378
|
+
const cs = getComputedStyle(im);
|
|
379
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
|
380
|
+
return true;
|
|
381
|
+
});
|
|
382
|
+
if (!bad.length) return '';
|
|
383
|
+
const sample = bad.slice(0, 2).map((im) => (im.currentSrc || im.getAttribute('src') || '').slice(0, 60)).join(', ');
|
|
384
|
+
return bad.length + ' broken img(s): ' + sample;
|
|
385
|
+
})()" 2>/dev/null)
|
|
386
|
+
broken=$(printf '%s' "$broken" | sed 's/^\"//; s/\"$//')
|
|
387
|
+
if [ -n "$broken" ] && [ "$broken" != "null" ] && [ "$broken" != "undefined" ]; then
|
|
388
|
+
echo "BROKEN-IMG"
|
|
389
|
+
echo "$broken"
|
|
390
|
+
echo "$out_png"
|
|
391
|
+
return
|
|
392
|
+
fi
|
|
393
|
+
|
|
359
394
|
# Computed-style gate (DDR-068) — "mounts with content" ≠ "rendered styled". A
|
|
360
395
|
# specimen whose import graph dropped the token CSS passes every check above
|
|
361
396
|
# (it has text, no error, a >2 KB screenshot) yet every var()-driven rule is
|
|
@@ -456,11 +491,12 @@ while IFS= read -r CANVAS; do
|
|
|
456
491
|
SHOT=$(printf '%s\n' "$RESULT" | sed -n '3p')
|
|
457
492
|
|
|
458
493
|
case "$STATUS" in
|
|
459
|
-
OK)
|
|
460
|
-
BLANK)
|
|
461
|
-
ERROR)
|
|
462
|
-
UNSTYLED)
|
|
463
|
-
|
|
494
|
+
OK) SYM="✓"; echo "$SYM" >&2 ;;
|
|
495
|
+
BLANK) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM blank ($DETAIL)" >&2 ;;
|
|
496
|
+
ERROR) SYM="⚠"; FAILED=$((FAILED + 1)); echo "$SYM error ($DETAIL)" >&2 ;;
|
|
497
|
+
UNSTYLED) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM unstyled ($DETAIL)" >&2 ;;
|
|
498
|
+
BROKEN-IMG) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM broken-img ($DETAIL)" >&2 ;;
|
|
499
|
+
*) SYM="?"; FAILED=$((FAILED + 1)); echo "? unknown ($STATUS)" >&2 ;;
|
|
464
500
|
esac
|
|
465
501
|
|
|
466
502
|
printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL" >> "$TSV"
|
package/apps/studio/build.ts
CHANGED
|
@@ -197,6 +197,19 @@ async function buildCss(): Promise<{ outBytes: number; outPath: string }> {
|
|
|
197
197
|
// Pre-building all 6 sub-bundles at release time + shipping in dist/runtime/
|
|
198
198
|
// eliminates the runtime dependency on disk node_modules entirely.
|
|
199
199
|
|
|
200
|
+
// Dev builds skip the runtime-bundle regen whenever every bundle is already
|
|
201
|
+
// on disk: the committed dist/runtime/*.js are authoritative for releases
|
|
202
|
+
// (CI builds with MAUDE_SKIP_RUNTIME_BUILD=1), and a dev-mode regen writes
|
|
203
|
+
// UNMINIFIED output that dirties the tree at ~2× the shipped size. Runtime
|
|
204
|
+
// bundles depend only on dependency versions, never on client/server source,
|
|
205
|
+
// so after a dep bump regen deliberately:
|
|
206
|
+
// MAUDE_FORCE_RUNTIME_BUILD=1 bun run build.ts --release
|
|
207
|
+
async function runtimeBundlesComplete(): Promise<boolean> {
|
|
208
|
+
const { RUNTIME_PACKAGES, slugFor } = await import('./runtime-bundle.ts');
|
|
209
|
+
const outDir = join(DIST, 'runtime');
|
|
210
|
+
return RUNTIME_PACKAGES.every((pkg) => existsSync(join(outDir, `${slugFor(pkg)}.js`)));
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
async function buildRuntimeBundles(): Promise<{ outDir: string; count: number; bytes: number }> {
|
|
201
214
|
// Defer import so build.ts can run in --dry-run without pulling react.
|
|
202
215
|
const { RUNTIME_PACKAGES, getRuntimeBundle, slugFor } = await import('./runtime-bundle.ts');
|
|
@@ -478,6 +491,14 @@ async function main() {
|
|
|
478
491
|
console.log(
|
|
479
492
|
'[build] dist/runtime/*.js SKIPPED (MAUDE_SKIP_RUNTIME_BUILD=1 — using committed pre-built)'
|
|
480
493
|
);
|
|
494
|
+
} else if (
|
|
495
|
+
MODE === 'dev' &&
|
|
496
|
+
process.env.MAUDE_FORCE_RUNTIME_BUILD !== '1' &&
|
|
497
|
+
(await runtimeBundlesComplete())
|
|
498
|
+
) {
|
|
499
|
+
console.log(
|
|
500
|
+
'[build] dist/runtime/*.js SKIPPED (all present; committed bundles are authoritative — MAUDE_FORCE_RUNTIME_BUILD=1 to regen after a dep bump)'
|
|
501
|
+
);
|
|
481
502
|
} else {
|
|
482
503
|
const runtime = await buildRuntimeBundles();
|
|
483
504
|
const t2b = performance.now();
|