@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.
Files changed (101) hide show
  1. package/README.md +34 -1
  2. package/cli/bin/maude.mjs +3 -0
  3. package/cli/commands/cache.mjs +181 -0
  4. package/cli/commands/cache.test.mjs +166 -0
  5. package/cli/commands/design-link.test.mjs +32 -2
  6. package/cli/commands/design.mjs +95 -4
  7. package/cli/commands/design.test.mjs +56 -0
  8. package/cli/commands/help.mjs +34 -0
  9. package/cli/commands/hub.mjs +255 -30
  10. package/cli/commands/hub.test.mjs +126 -2
  11. package/cli/commands/init.mjs +3 -0
  12. package/cli/commands/preflight.mjs +11 -0
  13. package/cli/commands/preflight.test.mjs +46 -0
  14. package/cli/commands/scenario-report.mjs +45 -0
  15. package/cli/lib/cache.mjs +407 -0
  16. package/cli/lib/cache.test.mjs +303 -0
  17. package/cli/lib/design-link.mjs +74 -10
  18. package/cli/lib/flow-design-integration.test.mjs +165 -0
  19. package/cli/lib/gitignore-block.mjs +90 -0
  20. package/cli/lib/gitignore-block.test.mjs +123 -0
  21. package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
  22. package/cli/lib/preflight.mjs +41 -10
  23. package/package.json +8 -8
  24. package/plugins/design/dev-server/ai-banner.tsx +2 -2
  25. package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
  26. package/plugins/design/dev-server/annotations-layer.tsx +906 -137
  27. package/plugins/design/dev-server/api.ts +109 -4
  28. package/plugins/design/dev-server/bin/preflight.sh +21 -10
  29. package/plugins/design/dev-server/bin/prep.sh +211 -0
  30. package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
  31. package/plugins/design/dev-server/bin/screenshot.sh +18 -1
  32. package/plugins/design/dev-server/bin/smoke.sh +94 -11
  33. package/plugins/design/dev-server/canvas-cursors.ts +125 -0
  34. package/plugins/design/dev-server/canvas-icons.tsx +130 -0
  35. package/plugins/design/dev-server/canvas-lib.tsx +25 -21
  36. package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
  37. package/plugins/design/dev-server/canvas-shell.tsx +333 -19
  38. package/plugins/design/dev-server/client/app.jsx +155 -7
  39. package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
  40. package/plugins/design/dev-server/collab/index.ts +87 -9
  41. package/plugins/design/dev-server/collab/persistence.ts +34 -3
  42. package/plugins/design/dev-server/collab/registry.ts +80 -2
  43. package/plugins/design/dev-server/collab/room.ts +21 -8
  44. package/plugins/design/dev-server/context-menu.tsx +167 -23
  45. package/plugins/design/dev-server/context.ts +24 -0
  46. package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
  47. package/plugins/design/dev-server/dist/client.bundle.js +126 -13
  48. package/plugins/design/dev-server/dist/comment-mount.js +78 -11
  49. package/plugins/design/dev-server/dist/styles.css +16 -0
  50. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  51. package/plugins/design/dev-server/export-dialog.tsx +19 -17
  52. package/plugins/design/dev-server/fs-watch.ts +1 -0
  53. package/plugins/design/dev-server/http.ts +260 -20
  54. package/plugins/design/dev-server/input-router.tsx +26 -3
  55. package/plugins/design/dev-server/participants-chrome.tsx +10 -10
  56. package/plugins/design/dev-server/server.ts +123 -1
  57. package/plugins/design/dev-server/sync/agent.ts +95 -0
  58. package/plugins/design/dev-server/sync/codec.ts +155 -0
  59. package/plugins/design/dev-server/sync/connection-state.ts +203 -0
  60. package/plugins/design/dev-server/sync/index.ts +479 -35
  61. package/plugins/design/dev-server/sync/materialize.ts +62 -0
  62. package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
  63. package/plugins/design/dev-server/sync/origins.ts +57 -0
  64. package/plugins/design/dev-server/sync/projection.ts +368 -0
  65. package/plugins/design/dev-server/sync/status.ts +115 -0
  66. package/plugins/design/dev-server/sync/untrusted.ts +153 -0
  67. package/plugins/design/dev-server/test/_helpers.ts +6 -2
  68. package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
  69. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
  70. package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
  71. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
  72. package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
  73. package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
  74. package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
  75. package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
  76. package/plugins/design/dev-server/test/input-router.test.ts +21 -0
  77. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
  78. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
  79. package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
  80. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
  81. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
  82. package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
  83. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
  84. package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
  85. package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
  86. package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
  87. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
  88. package/plugins/design/dev-server/tool-palette.tsx +18 -16
  89. package/plugins/design/dev-server/undo-hud.tsx +4 -4
  90. package/plugins/design/dev-server/undo-stack.ts +20 -4
  91. package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
  92. package/plugins/design/dev-server/use-tool-mode.tsx +15 -12
  93. package/plugins/design/dev-server/ws.ts +40 -1
  94. package/plugins/design/templates/_shell.html +49 -1
  95. package/plugins/design/templates/canvas.tsx.template +13 -0
  96. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
  97. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  98. package/plugins/flow/templates/ai-skeleton/README.md +1 -1
  99. package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
  100. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
  101. 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,28p' "$0" | sed 's/^# \?//'
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
- if [ -s "$out_png" ]; then
177
- local size
178
- size=$(wc -c < "$out_png" 2>/dev/null | tr -d ' ')
179
- if [ -n "$size" ] && [ "$size" -lt 2048 ]; then
180
- echo "BLANK"
181
- echo "png-${size}B"
182
- echo "$out_png"
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
- URL="http://localhost:$PORT/$REL_ENC"
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
- background-color: var(--bg-1, #f4f1ea);
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-subtle, rgba(0,0,0,0.08)) 1px, transparent 1px),
131
- linear-gradient(90deg, var(--border-subtle, rgba(0,0,0,0.08)) 1px, transparent 1px);
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(--fg-3, var(--fg-1, #4a3f30)) 40%, transparent);
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 = '0 6px 24px color-mix(in oklab, var(--fg-0, #1c1917) 10%, transparent)';
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"],