@1agh/maude 0.23.0 → 0.24.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 +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +32 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +126 -2
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +74 -10
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
- package/plugins/design/dev-server/annotations-layer.tsx +906 -137
- package/plugins/design/dev-server/api.ts +109 -4
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +94 -11
- package/plugins/design/dev-server/canvas-cursors.ts +125 -0
- package/plugins/design/dev-server/canvas-icons.tsx +130 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +333 -19
- package/plugins/design/dev-server/client/app.jsx +155 -7
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +126 -13
- package/plugins/design/dev-server/dist/comment-mount.js +78 -11
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +19 -17
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +260 -20
- package/plugins/design/dev-server/input-router.tsx +26 -3
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +21 -0
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
- package/plugins/design/dev-server/tool-palette.tsx +18 -16
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
- package/plugins/design/dev-server/use-tool-mode.tsx +15 -12
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +49 -1
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
# [--include-system 0|1] (default 1)
|
|
11
11
|
# [--timeout <secs>] (default 8)
|
|
12
12
|
# [--engine auto|agent-browser|playwright]
|
|
13
|
+
# [--changed-only] (default off — screenshot only canvases changed
|
|
14
|
+
# since the last smoke run; escalates to the full
|
|
15
|
+
# set when dev-server / canvas-lib / templates changed)
|
|
13
16
|
#
|
|
14
17
|
# Reads:
|
|
15
18
|
# $DESIGN_ROOT/_server.json (must exist — caller runs server-up.sh first)
|
|
@@ -31,6 +34,7 @@ OUT_DIR=""
|
|
|
31
34
|
INCLUDE_SYSTEM=1
|
|
32
35
|
TIMEOUT=8
|
|
33
36
|
ENGINE="auto"
|
|
37
|
+
CHANGED_ONLY=0
|
|
34
38
|
|
|
35
39
|
while [ $# -gt 0 ]; do
|
|
36
40
|
case "$1" in
|
|
@@ -39,8 +43,9 @@ while [ $# -gt 0 ]; do
|
|
|
39
43
|
--include-system) INCLUDE_SYSTEM="$2"; shift 2 ;;
|
|
40
44
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
41
45
|
--engine) ENGINE="$2"; shift 2 ;;
|
|
46
|
+
--changed-only) CHANGED_ONLY=1; shift ;;
|
|
42
47
|
--help|-h)
|
|
43
|
-
sed -n '2,
|
|
48
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
44
49
|
exit 0
|
|
45
50
|
;;
|
|
46
51
|
*)
|
|
@@ -73,6 +78,10 @@ if [ -z "$OUT_DIR" ]; then
|
|
|
73
78
|
OUT_DIR="$DESIGN_ROOT/_history/_smoke/$TS"
|
|
74
79
|
fi
|
|
75
80
|
mkdir -p "$OUT_DIR"
|
|
81
|
+
# agent-browser ignores RELATIVE screenshot paths (writes to ~/.agent-browser/tmp
|
|
82
|
+
# instead), which silently strands every PNG. Canonicalize to absolute so the
|
|
83
|
+
# captured evidence actually lands in OUT_DIR. See DDR-021.
|
|
84
|
+
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
|
|
76
85
|
|
|
77
86
|
# ---------- engine resolution ----------
|
|
78
87
|
if [ "$ENGINE" = "auto" ]; then
|
|
@@ -107,6 +116,57 @@ COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
|
107
116
|
[ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
|
|
108
117
|
echo "→ found $COUNT canvases" >&2
|
|
109
118
|
|
|
119
|
+
# ---------- --changed-only incremental filter (Phase C / DDR-061) ----------
|
|
120
|
+
# Default smoke screenshots every canvas. --changed-only narrows to the canvases
|
|
121
|
+
# touched since the last recorded smoke run, UNLESS the diff touches an
|
|
122
|
+
# "everything could break" shape (dev-server, canvas-lib, canvas templates) — then
|
|
123
|
+
# it escalates back to the full set. Correctness > speed: no baseline, no git, or
|
|
124
|
+
# an escalation trigger all fall back to the full set.
|
|
125
|
+
MARKER="$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
126
|
+
if [ "$CHANGED_ONLY" = "1" ]; then
|
|
127
|
+
LAST_SHA=""
|
|
128
|
+
if [ -f "$MARKER" ] && command -v jq >/dev/null 2>&1; then
|
|
129
|
+
LAST_SHA=$(jq -r '.sha // empty' "$MARKER" 2>/dev/null)
|
|
130
|
+
fi
|
|
131
|
+
if ! command -v git >/dev/null 2>&1 || ! git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
132
|
+
echo "→ --changed-only: no git in $REPO — running full set" >&2
|
|
133
|
+
elif [ -z "$LAST_SHA" ]; then
|
|
134
|
+
echo "→ --changed-only: no prior smoke baseline ($MARKER) — running full set" >&2
|
|
135
|
+
else
|
|
136
|
+
# All paths changed between the last smoke and the current working tree
|
|
137
|
+
# (committed + uncommitted), plus untracked canvases.
|
|
138
|
+
CHANGED=$(
|
|
139
|
+
{ git -C "$REPO" diff --name-only "$LAST_SHA" -- 2>/dev/null
|
|
140
|
+
git -C "$REPO" ls-files --others --exclude-standard -- .design/ui .design/system 2>/dev/null
|
|
141
|
+
} | sort -u
|
|
142
|
+
)
|
|
143
|
+
if printf '%s\n' "$CHANGED" | grep -qE 'dev-server/|canvas-lib\.tsx|canvas[^/]*\.tsx\.template'; then
|
|
144
|
+
echo "→ --changed-only: dev-server / canvas-lib / template changed — escalating to FULL set" >&2
|
|
145
|
+
else
|
|
146
|
+
# Keep only canvases whose repo-relative path is in the changed set.
|
|
147
|
+
FILTERED=""
|
|
148
|
+
while IFS= read -r CANVAS; do
|
|
149
|
+
[ -z "$CANVAS" ] && continue
|
|
150
|
+
REL_REPO="${CANVAS#$REPO/}"
|
|
151
|
+
if printf '%s\n' "$CHANGED" | grep -qxF "$REL_REPO"; then
|
|
152
|
+
FILTERED="$FILTERED$CANVAS"$'\n'
|
|
153
|
+
fi
|
|
154
|
+
done <<< "$CANVASES"
|
|
155
|
+
CANVASES=$(printf '%s' "$FILTERED" | sed '/^$/d')
|
|
156
|
+
COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
157
|
+
if [ "$COUNT" -eq 0 ]; then
|
|
158
|
+
echo "→ --changed-only: no canvas .tsx changed since last smoke ($LAST_SHA) — nothing to screenshot" >&2
|
|
159
|
+
# Refresh the marker so the next run diffs from here, then exit clean.
|
|
160
|
+
mkdir -p "$(dirname "$MARKER")"
|
|
161
|
+
printf '{"sha":"%s","ts":"%s","mode":"changed-only-noop"}' \
|
|
162
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$MARKER"
|
|
163
|
+
exit 0
|
|
164
|
+
fi
|
|
165
|
+
echo "→ --changed-only: $COUNT changed canvas(es) since $LAST_SHA" >&2
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
110
170
|
# ---------- helpers ----------
|
|
111
171
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
112
172
|
SLUG_HELPER="$SCRIPT_DIR/slug.sh"
|
|
@@ -160,6 +220,8 @@ probe_agent_browser() {
|
|
|
160
220
|
for (const s of sel) { const el = document.querySelector(s); if (el && el.innerText) return el.innerText.slice(0, 120); }
|
|
161
221
|
const body = document.body && document.body.innerText || '';
|
|
162
222
|
if (body.startsWith('Error:') || body.startsWith('SyntaxError:') || body.startsWith('ReferenceError:')) return body.slice(0, 120);
|
|
223
|
+
const t = body.trim();
|
|
224
|
+
if (t === 'Not found' || t.startsWith('Forbidden (canvas origin)')) return 'route-error: ' + t.slice(0, 80);
|
|
163
225
|
return '';
|
|
164
226
|
})()" 2>/dev/null)
|
|
165
227
|
# Strip wrapping quotes that agent-browser eval adds for strings.
|
|
@@ -172,16 +234,24 @@ probe_agent_browser() {
|
|
|
172
234
|
return
|
|
173
235
|
fi
|
|
174
236
|
|
|
237
|
+
# The PNG IS the evidence (DDR-021). A missing/empty file means agent-browser
|
|
238
|
+
# never wrote it (relative path, capture crash, …) — that is a FAILURE, not an
|
|
239
|
+
# OK. The old code fell straight through to "OK" here, which is exactly how a
|
|
240
|
+
# broken canvas route ("Not found" 404 + no PNG) masqueraded as 43/43 green.
|
|
241
|
+
if [ ! -s "$out_png" ]; then
|
|
242
|
+
echo "ERROR"
|
|
243
|
+
echo "screenshot-not-written"
|
|
244
|
+
echo "$out_png"
|
|
245
|
+
return
|
|
246
|
+
fi
|
|
175
247
|
# PNG size sanity — < 2 KB usually means blank background only.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
fi
|
|
248
|
+
local size
|
|
249
|
+
size=$(wc -c < "$out_png" 2>/dev/null | tr -d ' ')
|
|
250
|
+
if [ -n "$size" ] && [ "$size" -lt 2048 ]; then
|
|
251
|
+
echo "BLANK"
|
|
252
|
+
echo "png-${size}B"
|
|
253
|
+
echo "$out_png"
|
|
254
|
+
return
|
|
185
255
|
fi
|
|
186
256
|
|
|
187
257
|
echo "OK"
|
|
@@ -211,7 +281,11 @@ while IFS= read -r CANVAS; do
|
|
|
211
281
|
N=$((N + 1))
|
|
212
282
|
REL="${CANVAS#$DESIGN_ROOT/}"
|
|
213
283
|
REL_ENC=$(urlencode_path "$REL")
|
|
214
|
-
|
|
284
|
+
# Canvases render through the canvas shell, not the bare path. The bare
|
|
285
|
+
# `/<rel>` route 404s when the canvas-origin sandbox is on (default since
|
|
286
|
+
# phase-9.1) — only `/_canvas-shell.html?canvas=<rel>` mounts the canvas
|
|
287
|
+
# (works in both split-on and legacy same-origin modes).
|
|
288
|
+
URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL_ENC"
|
|
215
289
|
SLUG=$(bash "$SLUG_HELPER" "$REL" 2>/dev/null || printf '%s' "$REL" | tr '/ ' '__' | tr '[:upper:]' '[:lower:]')
|
|
216
290
|
OUT_PNG="$OUT_DIR/$SLUG.png"
|
|
217
291
|
|
|
@@ -264,6 +338,15 @@ done <<< "$CANVASES"
|
|
|
264
338
|
echo "→ report: $MD" >&2
|
|
265
339
|
echo "→ tsv: $TSV" >&2
|
|
266
340
|
|
|
341
|
+
# Record this run as the baseline for the next --changed-only diff (Phase C / DDR-061).
|
|
342
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse HEAD >/dev/null 2>&1; then
|
|
343
|
+
mkdir -p "$DESIGN_ROOT/_history/_smoke"
|
|
344
|
+
printf '{"sha":"%s","ts":"%s","mode":"%s","count":%d,"failed":%d}' \
|
|
345
|
+
"$(git -C "$REPO" rev-parse HEAD 2>/dev/null)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
346
|
+
"$([ "$CHANGED_ONLY" = "1" ] && echo changed-only || echo full)" "$COUNT" "$FAILED" \
|
|
347
|
+
> "$DESIGN_ROOT/_history/_smoke/.last-smoke.json"
|
|
348
|
+
fi
|
|
349
|
+
|
|
267
350
|
if [ $FAILED -gt 0 ]; then
|
|
268
351
|
echo "✗ smoke: $FAILED / $COUNT canvases failed" >&2
|
|
269
352
|
exit 3
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file canvas-cursors.ts — Phase 21 custom tool cursors
|
|
3
|
+
* @scope plugins/design/dev-server/canvas-cursors.ts
|
|
4
|
+
* @purpose FigJam/Figma-style custom cursors for the annotation + nav tools.
|
|
5
|
+
* The native `crosshair` / `text` / `cell` cursors are tiny, thin,
|
|
6
|
+
* and vanish on busy canvases (the "pen is almost invisible"
|
|
7
|
+
* complaint). These are 32×32 SVG cursors with a WHITE outline halo
|
|
8
|
+
* so the glyph reads on any background, light or dark, and each one
|
|
9
|
+
* mirrors its tool-palette icon so the cursor and the tool button
|
|
10
|
+
* share one visual identity.
|
|
11
|
+
*
|
|
12
|
+
* Delivered as CSS `cursor: url("data:image/svg+xml,…") hx hy, fb`
|
|
13
|
+
* — no runtime dependency, no asset files (Figma ships cursors the
|
|
14
|
+
* same way). 32×32 is the cross-browser-safe ceiling; the hotspot
|
|
15
|
+
* (hx, hy) is the pixel the click actually registers at.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Tool } from './input-router.tsx';
|
|
19
|
+
|
|
20
|
+
/** Build a CSS cursor value from an inline SVG + hotspot + native fallback. */
|
|
21
|
+
function svgCursor(svg: string, hx: number, hy: number, fallback: string): string {
|
|
22
|
+
// encodeURIComponent is the safest escaping for a data: URI inside url("…").
|
|
23
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg.trim())}") ${hx} ${hy}, ${fallback}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const W = `width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'`;
|
|
27
|
+
const HALO = "stroke='#ffffff' stroke-linejoin='round' stroke-linecap='round'";
|
|
28
|
+
const INK = '#1f1f1f';
|
|
29
|
+
|
|
30
|
+
// Precise crosshair with a centre gap — rect / ellipse / arrow. Hotspot 16,16.
|
|
31
|
+
const CROSSHAIR = svgCursor(
|
|
32
|
+
`<svg ${W}>
|
|
33
|
+
<path d='M16 4V13M16 19V28M4 16H13M19 16H28' fill='none' ${HALO} stroke-width='4'/>
|
|
34
|
+
<path d='M16 4V13M16 19V28M4 16H13M19 16H28' fill='none' stroke='${INK}' stroke-width='1.75' stroke-linecap='round'/>
|
|
35
|
+
<circle cx='16' cy='16' r='1.3' fill='${INK}'/>
|
|
36
|
+
</svg>`,
|
|
37
|
+
16,
|
|
38
|
+
16,
|
|
39
|
+
'crosshair'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Marker/pen — nib points at the draw point (bottom-left). Hotspot 6,26.
|
|
43
|
+
const PEN = svgCursor(
|
|
44
|
+
`<svg ${W}>
|
|
45
|
+
<path d='M22 4l6 6-15 15-7 1 1-7z' fill='${INK}' ${HALO} stroke-width='2.25'/>
|
|
46
|
+
<path d='M18.5 7.5l6 6' ${HALO} stroke-width='2'/>
|
|
47
|
+
<path d='M7 25l-1.5 1.5' ${HALO} stroke-width='2.5'/>
|
|
48
|
+
</svg>`,
|
|
49
|
+
6,
|
|
50
|
+
26,
|
|
51
|
+
'crosshair'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Bigger I-beam for text. Hotspot 16,16.
|
|
55
|
+
const TEXT = svgCursor(
|
|
56
|
+
`<svg ${W}>
|
|
57
|
+
<path d='M16 5V27M11 5H21M11 27H21' fill='none' ${HALO} stroke-width='4.5'/>
|
|
58
|
+
<path d='M16 5V27M11 5H21M11 27H21' fill='none' stroke='${INK}' stroke-width='2' stroke-linecap='round'/>
|
|
59
|
+
</svg>`,
|
|
60
|
+
16,
|
|
61
|
+
16,
|
|
62
|
+
'text'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Speech bubble — drops a comment at the tail tip (bottom-left). Hotspot 8,27.
|
|
66
|
+
const COMMENT = svgCursor(
|
|
67
|
+
`<svg ${W}>
|
|
68
|
+
<path d='M8 5H26a4 4 0 0 1 4 4V18a4 4 0 0 1-4 4H14l-6 6v-6H8a4 4 0 0 1-4-4V9a4 4 0 0 1 4-4Z' fill='${INK}' ${HALO} stroke-width='2.25'/>
|
|
69
|
+
<circle cx='12' cy='13.5' r='1.4' fill='#fff'/>
|
|
70
|
+
<circle cx='17' cy='13.5' r='1.4' fill='#fff'/>
|
|
71
|
+
<circle cx='22' cy='13.5' r='1.4' fill='#fff'/>
|
|
72
|
+
</svg>`,
|
|
73
|
+
8,
|
|
74
|
+
27,
|
|
75
|
+
'crosshair'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Open hand for pan/grab. Hotspot 16,17.
|
|
79
|
+
const HAND = svgCursor(
|
|
80
|
+
`<svg ${W}>
|
|
81
|
+
<path d='M10 19v-8a1.8 1.8 0 0 1 3.6 0v-1.2a1.8 1.8 0 0 1 3.6 0v.6a1.8 1.8 0 0 1 3.6 0V12a1.8 1.8 0 0 1 3.6 0v6.5a7.5 7.5 0 0 1-7.5 7.5h-1a7.5 7.5 0 0 1-6.4-3.6l-2.4-3.7a1.9 1.9 0 0 1 3-2.3z' fill='${INK}' ${HALO} stroke-width='2'/>
|
|
82
|
+
</svg>`,
|
|
83
|
+
16,
|
|
84
|
+
17,
|
|
85
|
+
'grab'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Eraser — working edge at bottom-left. Hotspot 8,24.
|
|
89
|
+
const ERASER = svgCursor(
|
|
90
|
+
`<svg ${W}>
|
|
91
|
+
<path d='M19 5l8 8-10 10H9l-4-4z' fill='${INK}' ${HALO} stroke-width='2.25'/>
|
|
92
|
+
<path d='M13 11l8 8' ${HALO} stroke-width='2'/>
|
|
93
|
+
</svg>`,
|
|
94
|
+
8,
|
|
95
|
+
24,
|
|
96
|
+
'cell'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Sticky — a mini yellow note (matches the card). Drag starts at its top-left.
|
|
100
|
+
const STICKY = svgCursor(
|
|
101
|
+
`<svg ${W}>
|
|
102
|
+
<path d='M7 6h11l8 8v12H7z' fill='#ffe27a' stroke='${INK}' stroke-width='2' stroke-linejoin='round'/>
|
|
103
|
+
<path d='M18 6v8h8' fill='none' stroke='${INK}' stroke-width='2' stroke-linejoin='round'/>
|
|
104
|
+
</svg>`,
|
|
105
|
+
8,
|
|
106
|
+
7,
|
|
107
|
+
'crosshair'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Tool → CSS cursor value. `move` keeps the system arrow (the universal
|
|
112
|
+
* select/move affordance); everything else gets a crafted cursor.
|
|
113
|
+
*/
|
|
114
|
+
export const TOOL_CURSORS: Record<Tool, string> = {
|
|
115
|
+
move: 'default',
|
|
116
|
+
hand: HAND,
|
|
117
|
+
comment: COMMENT,
|
|
118
|
+
pen: PEN,
|
|
119
|
+
rect: CROSSHAIR,
|
|
120
|
+
ellipse: CROSSHAIR,
|
|
121
|
+
sticky: STICKY,
|
|
122
|
+
arrow: CROSSHAIR,
|
|
123
|
+
text: TEXT,
|
|
124
|
+
eraser: ERASER,
|
|
125
|
+
};
|
|
@@ -102,6 +102,26 @@ export function IconEraser(props: IconProps) {
|
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Phase 21 — sticky note: folded-corner square (dog-ear at the top-right).
|
|
106
|
+
export function IconSticky(props: IconProps) {
|
|
107
|
+
return (
|
|
108
|
+
<Svg {...props}>
|
|
109
|
+
<path d="M4 5h12l4 4v10H4z" />
|
|
110
|
+
<path d="M16 5v4h4" />
|
|
111
|
+
</Svg>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Phase 21 — standalone text: capital-T glyph (cap bar + stem).
|
|
116
|
+
export function IconText(props: IconProps) {
|
|
117
|
+
return (
|
|
118
|
+
<Svg {...props}>
|
|
119
|
+
<path d="M4 6h16" />
|
|
120
|
+
<path d="M12 6v12" />
|
|
121
|
+
</Svg>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
export function IconPresentation(props: IconProps) {
|
|
106
126
|
return (
|
|
107
127
|
<Svg {...props}>
|
|
@@ -119,6 +139,114 @@ export function IconChevronDown(props: IconProps) {
|
|
|
119
139
|
);
|
|
120
140
|
}
|
|
121
141
|
|
|
142
|
+
// ── Phase 21 context-toolbar icons ──────────────────────────────────────────
|
|
143
|
+
// Corner radius: a square whose corners show the chosen rounding.
|
|
144
|
+
export function IconCornerSquare(props: IconProps) {
|
|
145
|
+
return (
|
|
146
|
+
<Svg {...props}>
|
|
147
|
+
<rect x="5" y="5" width="14" height="14" rx="0.5" />
|
|
148
|
+
</Svg>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
export function IconCornerSoft(props: IconProps) {
|
|
152
|
+
return (
|
|
153
|
+
<Svg {...props}>
|
|
154
|
+
<rect x="5" y="5" width="14" height="14" rx="4" />
|
|
155
|
+
</Svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
export function IconCornerPill(props: IconProps) {
|
|
159
|
+
return (
|
|
160
|
+
<Svg {...props}>
|
|
161
|
+
<rect x="5" y="5" width="14" height="14" rx="7" />
|
|
162
|
+
</Svg>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Arrow direction: a shaft with chevron head(s) on the chosen end(s).
|
|
167
|
+
export function IconArrowNone(props: IconProps) {
|
|
168
|
+
return (
|
|
169
|
+
<Svg {...props}>
|
|
170
|
+
<path d="M4 12h16" />
|
|
171
|
+
</Svg>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
export function IconArrowStartHead(props: IconProps) {
|
|
175
|
+
return (
|
|
176
|
+
<Svg {...props}>
|
|
177
|
+
<path d="M4 12h16" />
|
|
178
|
+
<path d="M9 7l-5 5 5 5" />
|
|
179
|
+
</Svg>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
export function IconArrowEndHead(props: IconProps) {
|
|
183
|
+
return (
|
|
184
|
+
<Svg {...props}>
|
|
185
|
+
<path d="M4 12h16" />
|
|
186
|
+
<path d="M15 7l5 5-5 5" />
|
|
187
|
+
</Svg>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
export function IconArrowBothHeads(props: IconProps) {
|
|
191
|
+
return (
|
|
192
|
+
<Svg {...props}>
|
|
193
|
+
<path d="M5 12h14" />
|
|
194
|
+
<path d="M9 7l-5 5 5 5" />
|
|
195
|
+
<path d="M15 7l5 5-5 5" />
|
|
196
|
+
</Svg>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Dashed line toggle.
|
|
201
|
+
export function IconDash(props: IconProps) {
|
|
202
|
+
return (
|
|
203
|
+
<Svg {...props}>
|
|
204
|
+
<path d="M3 12h4" />
|
|
205
|
+
<path d="M10 12h4" />
|
|
206
|
+
<path d="M17 12h4" />
|
|
207
|
+
</Svg>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Stroke weight: thin vs thick rule.
|
|
212
|
+
export function IconLineThin(props: IconProps) {
|
|
213
|
+
return (
|
|
214
|
+
<Svg {...props} strokeWidth={1.25}>
|
|
215
|
+
<path d="M4 12h16" />
|
|
216
|
+
</Svg>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
export function IconLineThick(props: IconProps) {
|
|
220
|
+
return (
|
|
221
|
+
<Svg {...props} strokeWidth={3.75}>
|
|
222
|
+
<path d="M4 12h16" />
|
|
223
|
+
</Svg>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Letter "A" — font-size chips render it at three sizes (S / M / L).
|
|
228
|
+
export function IconLetterA(props: IconProps) {
|
|
229
|
+
return (
|
|
230
|
+
<Svg {...props}>
|
|
231
|
+
<path d="M12 5L6 19" />
|
|
232
|
+
<path d="M12 5l6 14" />
|
|
233
|
+
<path d="M8.5 14h7" />
|
|
234
|
+
</Svg>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Trash — delete selected annotations.
|
|
239
|
+
export function IconTrash(props: IconProps) {
|
|
240
|
+
return (
|
|
241
|
+
<Svg {...props}>
|
|
242
|
+
<path d="M4 7h16" />
|
|
243
|
+
<path d="M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2" />
|
|
244
|
+
<path d="M6 7l1 12a1 1 0 001 1h8a1 1 0 001-1l1-12" />
|
|
245
|
+
<path d="M10 11v6M14 11v6" />
|
|
246
|
+
</Svg>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
122
250
|
export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
|
|
123
251
|
move: IconMove,
|
|
124
252
|
hand: IconHand,
|
|
@@ -126,6 +254,8 @@ export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
|
|
|
126
254
|
pen: IconPen,
|
|
127
255
|
rect: IconRect,
|
|
128
256
|
ellipse: IconEllipse,
|
|
257
|
+
sticky: IconSticky,
|
|
129
258
|
arrow: IconArrow,
|
|
259
|
+
text: IconText,
|
|
130
260
|
eraser: IconEraser,
|
|
131
261
|
};
|
|
@@ -125,10 +125,13 @@ const ENGINE_CSS = `
|
|
|
125
125
|
never visually melts into the selection halo during a drag-snap gesture.
|
|
126
126
|
OKLCH default approximates FigJam magenta in the project's color space. */
|
|
127
127
|
--guide-magenta: oklch(62% 0.28 350);
|
|
128
|
-
|
|
128
|
+
/* Canvas-shell chrome — the workspace plane + dotted grid follow the Maude
|
|
129
|
+
chrome theme (--maude-chrome-*), NOT the DS palette. See canvas-shell.tsx
|
|
130
|
+
HUD_TOKENS_CSS. Artboards (.dc-artboard) keep the DS theme. */
|
|
131
|
+
background-color: var(--maude-chrome-bg-1, #f4f1ea);
|
|
129
132
|
background-image:
|
|
130
|
-
linear-gradient(var(--border
|
|
131
|
-
linear-gradient(90deg, var(--border
|
|
133
|
+
linear-gradient(var(--maude-chrome-border, rgba(0,0,0,0.08)) 1px, transparent 1px),
|
|
134
|
+
linear-gradient(90deg, var(--maude-chrome-border, rgba(0,0,0,0.08)) 1px, transparent 1px);
|
|
132
135
|
background-size: 24px 24px;
|
|
133
136
|
}
|
|
134
137
|
/* DDR-046 — Snap guides. Sibling kind = confident magenta + glow + distance
|
|
@@ -146,13 +149,13 @@ const ENGINE_CSS = `
|
|
|
146
149
|
box-shadow: 0 0 4px color-mix(in oklab, var(--guide-magenta, oklch(62% 0.28 350)) 35%, transparent);
|
|
147
150
|
}
|
|
148
151
|
.dc-snap-guide--grid {
|
|
149
|
-
background: color-mix(in oklab, var(--
|
|
152
|
+
background: color-mix(in oklab, var(--maude-chrome-fg-1, #4a3f30) 40%, transparent);
|
|
150
153
|
}
|
|
151
154
|
.dc-snap-pill {
|
|
152
155
|
position: fixed;
|
|
153
156
|
pointer-events: none;
|
|
154
157
|
z-index: 7;
|
|
155
|
-
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
158
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
156
159
|
font-size: 11px;
|
|
157
160
|
font-weight: 600;
|
|
158
161
|
line-height: 1;
|
|
@@ -1718,7 +1721,8 @@ export function DCPostIt({ children }: { children: ReactNode }) {
|
|
|
1718
1721
|
// favor of a soft ambient. The hard offset stays on app-shell chrome only
|
|
1719
1722
|
// (menubar, header, tab strip) — that's the project's intentional brutalist
|
|
1720
1723
|
// identity. Floating layer = soft. App frame = hard.
|
|
1721
|
-
const FLOATING_SHADOW =
|
|
1724
|
+
const FLOATING_SHADOW =
|
|
1725
|
+
'0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent))';
|
|
1722
1726
|
const FLOATING_RADIUS = '8px';
|
|
1723
1727
|
|
|
1724
1728
|
const OVERLAY_CSS = `
|
|
@@ -1728,12 +1732,12 @@ const OVERLAY_CSS = `
|
|
|
1728
1732
|
bottom: 16px;
|
|
1729
1733
|
width: 196px;
|
|
1730
1734
|
height: 132px;
|
|
1731
|
-
background: var(--bg-0, #ffffff);
|
|
1732
|
-
border: 1px solid var(--fg-0, #1c1917);
|
|
1735
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
1736
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
1733
1737
|
border-radius: ${FLOATING_RADIUS};
|
|
1734
|
-
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1738
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1735
1739
|
font-size: 10px;
|
|
1736
|
-
color: var(--fg-1, rgba(40,30,20,0.7));
|
|
1740
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.7));
|
|
1737
1741
|
z-index: 6;
|
|
1738
1742
|
user-select: none;
|
|
1739
1743
|
box-shadow: ${FLOATING_SHADOW};
|
|
@@ -1741,11 +1745,11 @@ const OVERLAY_CSS = `
|
|
|
1741
1745
|
}
|
|
1742
1746
|
.dc-mm-hd {
|
|
1743
1747
|
padding: 5px 8px 4px;
|
|
1744
|
-
border-bottom: 1px solid rgba(0,0,0,0.08);
|
|
1748
|
+
border-bottom: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
1745
1749
|
letter-spacing: 0.05em;
|
|
1746
1750
|
text-transform: uppercase;
|
|
1747
1751
|
font-size: 9px;
|
|
1748
|
-
background: var(--bg-1, #f4f1ea);
|
|
1752
|
+
background: var(--maude-chrome-bg-1, #f4f1ea);
|
|
1749
1753
|
}
|
|
1750
1754
|
.dc-mm-body {
|
|
1751
1755
|
position: relative;
|
|
@@ -1753,12 +1757,12 @@ const OVERLAY_CSS = `
|
|
|
1753
1757
|
height: calc(100% - 22px);
|
|
1754
1758
|
overflow: hidden;
|
|
1755
1759
|
cursor: pointer;
|
|
1756
|
-
background: var(--bg-1, #f4f1ea);
|
|
1760
|
+
background: var(--maude-chrome-bg-1, #f4f1ea);
|
|
1757
1761
|
}
|
|
1758
1762
|
.dc-mm-rect {
|
|
1759
1763
|
position: absolute;
|
|
1760
|
-
background: color-mix(in oklab, var(--fg-0, #1c1917) 14%, transparent);
|
|
1761
|
-
border: 1px solid color-mix(in oklab, var(--fg-0, #1c1917) 28%, transparent);
|
|
1764
|
+
background: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 14%, transparent);
|
|
1765
|
+
border: 1px solid color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 28%, transparent);
|
|
1762
1766
|
border-radius: 1px;
|
|
1763
1767
|
}
|
|
1764
1768
|
/* Filled viewport indicator — FigJam / Figma both ship a tinted fill, not
|
|
@@ -1777,13 +1781,13 @@ const OVERLAY_CSS = `
|
|
|
1777
1781
|
transform: translateX(-50%);
|
|
1778
1782
|
display: flex;
|
|
1779
1783
|
align-items: stretch;
|
|
1780
|
-
background: var(--bg-0, #ffffff);
|
|
1781
|
-
border: 1px solid var(--fg-0, #1c1917);
|
|
1784
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
1785
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
1782
1786
|
border-radius: ${FLOATING_RADIUS};
|
|
1783
1787
|
overflow: hidden;
|
|
1784
|
-
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1788
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1785
1789
|
font-size: 11px;
|
|
1786
|
-
color: var(--fg-1, rgba(40,30,20,0.85));
|
|
1790
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.85));
|
|
1787
1791
|
z-index: 6;
|
|
1788
1792
|
box-shadow: ${FLOATING_SHADOW};
|
|
1789
1793
|
}
|
|
@@ -1791,7 +1795,7 @@ const OVERLAY_CSS = `
|
|
|
1791
1795
|
appearance: none;
|
|
1792
1796
|
background: transparent;
|
|
1793
1797
|
border: 0;
|
|
1794
|
-
border-right: 1px solid rgba(0,0,0,0.08);
|
|
1798
|
+
border-right: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
1795
1799
|
padding: 7px 12px;
|
|
1796
1800
|
font: inherit;
|
|
1797
1801
|
color: inherit;
|
|
@@ -1801,7 +1805,7 @@ const OVERLAY_CSS = `
|
|
|
1801
1805
|
transition: background 80ms linear;
|
|
1802
1806
|
}
|
|
1803
1807
|
.dc-zoom-tb button:last-child { border-right: 0; }
|
|
1804
|
-
.dc-zoom-tb button:hover { background: color-mix(in oklab, var(--fg-0, #1c1917) 5%, transparent); }
|
|
1808
|
+
.dc-zoom-tb button:hover { background: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 5%, transparent); }
|
|
1805
1809
|
.dc-zoom-tb button:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
1806
1810
|
.dc-zoom-tb-pct { font-variant-numeric: tabular-nums; min-width: 52px; }
|
|
1807
1811
|
`.trim();
|
|
@@ -18,6 +18,26 @@
|
|
|
18
18
|
"type": "string",
|
|
19
19
|
"description": "The full brief that produced this canvas (from /design:new). Preserved for handoff."
|
|
20
20
|
},
|
|
21
|
+
"brief_sha": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Hash of the brief that produced this canvas (stamped at /design:new step 11). Used by /design:new to short-circuit regeneration when a byte-identical brief in the same DS already produced a canvas."
|
|
24
|
+
},
|
|
25
|
+
"tags": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"uniqueItems": true,
|
|
28
|
+
"items": { "type": "string" },
|
|
29
|
+
"description": "Free-form labels for the canvas — feature slugs ('dark-mode'), surfaces ('settings'), or themes. Read by flow's /flow:plan design-canvas detection (Phase 11) to match a feature name against canvases via exact tag match (the canvas slug substring match is the other half of the heuristic). Optional."
|
|
30
|
+
},
|
|
31
|
+
"status": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"enum": ["draft", "in-review", "ready-for-handoff", "handed-off"],
|
|
34
|
+
"default": "draft",
|
|
35
|
+
"description": "Lifecycle state of the canvas. 'draft' = work in progress; 'in-review' = awaiting feedback; 'ready-for-handoff' = approved, eligible for /design:handoff; 'handed-off' = shipped to production code (see handoffCommit). Hand-set by the user, or flipped to 'handed-off' by flow's /flow:done handoff sweep (Phase 11) after a successful /design:handoff. Optional — absent means 'draft'."
|
|
36
|
+
},
|
|
37
|
+
"handoffCommit": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Git commit SHA (full or short) of the commit that handed this canvas off to production code. Written by /flow:done's handoff sweep when it flips status to 'handed-off'. Optional."
|
|
40
|
+
},
|
|
21
41
|
"platform": {
|
|
22
42
|
"type": "string",
|
|
23
43
|
"enum": ["desktop", "mobile", "tablet", "responsive", "tv", "watch", "other"],
|