@1agh/maude 0.22.2 → 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 (125) 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 +84 -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 +135 -10
  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 +222 -11
  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/hubs-config.mjs +42 -4
  22. package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
  23. package/cli/lib/preflight.mjs +41 -10
  24. package/package.json +8 -8
  25. package/plugins/design/dev-server/ai-banner.tsx +2 -2
  26. package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
  27. package/plugins/design/dev-server/annotations-layer.tsx +906 -137
  28. package/plugins/design/dev-server/api.ts +109 -4
  29. package/plugins/design/dev-server/bin/preflight.sh +21 -10
  30. package/plugins/design/dev-server/bin/prep.sh +211 -0
  31. package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
  32. package/plugins/design/dev-server/bin/screenshot.sh +18 -1
  33. package/plugins/design/dev-server/bin/smoke.sh +94 -11
  34. package/plugins/design/dev-server/build.ts +69 -3
  35. package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
  36. package/plugins/design/dev-server/canvas-cursors.ts +125 -0
  37. package/plugins/design/dev-server/canvas-icons.tsx +130 -0
  38. package/plugins/design/dev-server/canvas-lib.tsx +47 -27
  39. package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
  40. package/plugins/design/dev-server/canvas-shell.tsx +358 -245
  41. package/plugins/design/dev-server/client/app.jsx +191 -21
  42. package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
  43. package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
  44. package/plugins/design/dev-server/collab/index.ts +87 -9
  45. package/plugins/design/dev-server/collab/persistence.ts +34 -3
  46. package/plugins/design/dev-server/collab/registry.ts +131 -2
  47. package/plugins/design/dev-server/collab/room.ts +21 -8
  48. package/plugins/design/dev-server/config.schema.json +20 -0
  49. package/plugins/design/dev-server/context-menu.tsx +167 -23
  50. package/plugins/design/dev-server/context.ts +31 -0
  51. package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
  52. package/plugins/design/dev-server/dist/client.bundle.js +144 -22
  53. package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
  54. package/plugins/design/dev-server/dist/styles.css +16 -0
  55. package/plugins/design/dev-server/dom-selection.ts +156 -0
  56. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  57. package/plugins/design/dev-server/export-dialog.tsx +19 -17
  58. package/plugins/design/dev-server/fs-watch.ts +1 -0
  59. package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
  60. package/plugins/design/dev-server/http.ts +260 -20
  61. package/plugins/design/dev-server/input-router.tsx +125 -64
  62. package/plugins/design/dev-server/participants-chrome.tsx +10 -10
  63. package/plugins/design/dev-server/server.ts +141 -1
  64. package/plugins/design/dev-server/sync/agent.ts +418 -0
  65. package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
  66. package/plugins/design/dev-server/sync/codec.ts +324 -0
  67. package/plugins/design/dev-server/sync/connection-state.ts +203 -0
  68. package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
  69. package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
  70. package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
  71. package/plugins/design/dev-server/sync/index.ts +918 -0
  72. package/plugins/design/dev-server/sync/materialize.ts +62 -0
  73. package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
  74. package/plugins/design/dev-server/sync/origins.ts +57 -0
  75. package/plugins/design/dev-server/sync/projection.ts +368 -0
  76. package/plugins/design/dev-server/sync/status.ts +115 -0
  77. package/plugins/design/dev-server/sync/untrusted.ts +153 -0
  78. package/plugins/design/dev-server/test/_helpers.ts +6 -2
  79. package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
  80. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
  81. package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
  82. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
  83. package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
  84. package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
  85. package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
  86. package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
  87. package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
  88. package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
  89. package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +21 -0
  91. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
  92. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
  93. package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
  94. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
  95. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
  96. package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
  97. package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
  98. package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
  99. package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
  100. package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
  101. package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
  102. package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
  103. package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
  104. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
  105. package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
  106. package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
  107. package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
  108. package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
  109. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
  110. package/plugins/design/dev-server/tool-palette.tsx +18 -16
  111. package/plugins/design/dev-server/undo-hud.tsx +4 -4
  112. package/plugins/design/dev-server/undo-stack.ts +20 -4
  113. package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
  114. package/plugins/design/dev-server/use-collab.tsx +157 -13
  115. package/plugins/design/dev-server/use-selection-set.tsx +12 -0
  116. package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
  117. package/plugins/design/dev-server/ws.ts +40 -1
  118. package/plugins/design/templates/_shell.html +63 -5
  119. package/plugins/design/templates/canvas.tsx.template +13 -0
  120. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
  121. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  122. package/plugins/flow/templates/ai-skeleton/README.md +1 -1
  123. package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
  124. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
  125. package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
@@ -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
  };
@@ -95,7 +95,7 @@ import {
95
95
  import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
96
96
  import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
97
97
  import { useSelectionSetOptional } from './use-selection-set.tsx';
98
- import { ToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
98
+ import { MaybeToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
99
99
  import { UndoStackProvider, useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
100
100
 
101
101
  // ─────────────────────────────────────────────────────────────────────────────
@@ -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;
@@ -1161,7 +1164,10 @@ interface DesignCanvasProps {
1161
1164
  *
1162
1165
  * `ToolProvider` lives above `DesignCanvasInner` so the viewport
1163
1166
  * controller's `isPanDragActive` predicate can read the live tool state
1164
- * via `useToolModeOptional` (hand-mode bare-drag pan).
1167
+ * via `useToolModeOptional` (hand-mode bare-drag pan). It's wrapped in
1168
+ * `MaybeToolProvider` so when the shell-owned comment mount layer
1169
+ * (canvas-comment-mount.tsx) already provides a ToolProvider, DesignCanvas
1170
+ * consumes that single instance instead of double-mounting.
1165
1171
  */
1166
1172
  export function DesignCanvas(props: DesignCanvasProps) {
1167
1173
  // Phase 20 — per-canvas undo/redo stack (DDR-050 rev 2). The provider
@@ -1178,9 +1184,9 @@ export function DesignCanvas(props: DesignCanvasProps) {
1178
1184
  // CollabProvider is omitted; useCollab() falls back gracefully to null.
1179
1185
  const collabSlug = canvasSlugFromPath(canvasFile);
1180
1186
  const inner = (
1181
- <ToolProvider>
1187
+ <MaybeToolProvider>
1182
1188
  <DesignCanvasInner {...props} />
1183
- </ToolProvider>
1189
+ </MaybeToolProvider>
1184
1190
  );
1185
1191
  return (
1186
1192
  <UndoStackProvider canvasFile={canvasFile}>
@@ -1715,7 +1721,8 @@ export function DCPostIt({ children }: { children: ReactNode }) {
1715
1721
  // favor of a soft ambient. The hard offset stays on app-shell chrome only
1716
1722
  // (menubar, header, tab strip) — that's the project's intentional brutalist
1717
1723
  // identity. Floating layer = soft. App frame = hard.
1718
- 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))';
1719
1726
  const FLOATING_RADIUS = '8px';
1720
1727
 
1721
1728
  const OVERLAY_CSS = `
@@ -1725,12 +1732,12 @@ const OVERLAY_CSS = `
1725
1732
  bottom: 16px;
1726
1733
  width: 196px;
1727
1734
  height: 132px;
1728
- background: var(--bg-0, #ffffff);
1729
- 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);
1730
1737
  border-radius: ${FLOATING_RADIUS};
1731
- 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);
1732
1739
  font-size: 10px;
1733
- color: var(--fg-1, rgba(40,30,20,0.7));
1740
+ color: var(--maude-chrome-fg-1, rgba(40,30,20,0.7));
1734
1741
  z-index: 6;
1735
1742
  user-select: none;
1736
1743
  box-shadow: ${FLOATING_SHADOW};
@@ -1738,11 +1745,11 @@ const OVERLAY_CSS = `
1738
1745
  }
1739
1746
  .dc-mm-hd {
1740
1747
  padding: 5px 8px 4px;
1741
- 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));
1742
1749
  letter-spacing: 0.05em;
1743
1750
  text-transform: uppercase;
1744
1751
  font-size: 9px;
1745
- background: var(--bg-1, #f4f1ea);
1752
+ background: var(--maude-chrome-bg-1, #f4f1ea);
1746
1753
  }
1747
1754
  .dc-mm-body {
1748
1755
  position: relative;
@@ -1750,12 +1757,12 @@ const OVERLAY_CSS = `
1750
1757
  height: calc(100% - 22px);
1751
1758
  overflow: hidden;
1752
1759
  cursor: pointer;
1753
- background: var(--bg-1, #f4f1ea);
1760
+ background: var(--maude-chrome-bg-1, #f4f1ea);
1754
1761
  }
1755
1762
  .dc-mm-rect {
1756
1763
  position: absolute;
1757
- background: color-mix(in oklab, var(--fg-0, #1c1917) 14%, transparent);
1758
- 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);
1759
1766
  border-radius: 1px;
1760
1767
  }
1761
1768
  /* Filled viewport indicator — FigJam / Figma both ship a tinted fill, not
@@ -1774,13 +1781,13 @@ const OVERLAY_CSS = `
1774
1781
  transform: translateX(-50%);
1775
1782
  display: flex;
1776
1783
  align-items: stretch;
1777
- background: var(--bg-0, #ffffff);
1778
- 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);
1779
1786
  border-radius: ${FLOATING_RADIUS};
1780
1787
  overflow: hidden;
1781
- 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);
1782
1789
  font-size: 11px;
1783
- color: var(--fg-1, rgba(40,30,20,0.85));
1790
+ color: var(--maude-chrome-fg-1, rgba(40,30,20,0.85));
1784
1791
  z-index: 6;
1785
1792
  box-shadow: ${FLOATING_SHADOW};
1786
1793
  }
@@ -1788,7 +1795,7 @@ const OVERLAY_CSS = `
1788
1795
  appearance: none;
1789
1796
  background: transparent;
1790
1797
  border: 0;
1791
- 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));
1792
1799
  padding: 7px 12px;
1793
1800
  font: inherit;
1794
1801
  color: inherit;
@@ -1798,7 +1805,7 @@ const OVERLAY_CSS = `
1798
1805
  transition: background 80ms linear;
1799
1806
  }
1800
1807
  .dc-zoom-tb button:last-child { border-right: 0; }
1801
- .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); }
1802
1809
  .dc-zoom-tb button:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
1803
1810
  .dc-zoom-tb-pct { font-variant-numeric: tabular-nums; min-width: 52px; }
1804
1811
  `.trim();
@@ -2300,11 +2307,18 @@ export const MOTION_ROLE_DEFAULTS: Record<MotionRole, RoleConfig> = {
2300
2307
  spring: {
2301
2308
  durationToken: '--dur-panel',
2302
2309
  easingToken: 'spring',
2303
- keyframes: { y: [0, -16, 0] },
2310
+ // Spring physics animate toward a single target; a 3-point array
2311
+ // (`[0,-16,0]`) makes motion/react's spring no-op (no movement). Use a
2312
+ // 2-point target and let `repeatType: 'reverse'` carry the return leg.
2313
+ keyframes: { y: [0, -16] },
2304
2314
  fallbackMs: 320,
2305
2315
  },
2306
2316
  scroll: {
2307
- durationToken: '--dur-route',
2317
+ // `--dur-route` is intentionally ~instant (route changes are snap, often
2318
+ // 1ms), which makes a scroll-linked drift imperceptible. Bind to the
2319
+ // longer `--dur-soft` so the demo (and any time-driven scroll fallback)
2320
+ // is actually visible.
2321
+ durationToken: '--dur-soft',
2308
2322
  easingToken: '--ease-in-out',
2309
2323
  keyframes: { x: [0, 24, 0] },
2310
2324
  fallbackMs: 480,
@@ -2425,6 +2439,11 @@ export function MotionDemo({
2425
2439
  const repeat = reduced || loop === 'once' ? 0 : Number.POSITIVE_INFINITY;
2426
2440
  const repeatType: 'reverse' | 'loop' = loop === 'always' ? 'reverse' : 'loop';
2427
2441
  const animate = reduced ? undefined : cfg.keyframes;
2442
+ // DS durations are micro-interaction speeds (often <200ms). Looping them with
2443
+ // no gap strobes ~10×/s. Insert a rest between cycles so each loop replays the
2444
+ // REAL token speed, then pauses — readable cadence, not a flicker. Spring's
2445
+ // own settle is the pause, so it skips the extra delay.
2446
+ const repeatDelay = loop === 'always' && !isSpring ? 0.9 : 0;
2428
2447
 
2429
2448
  return (
2430
2449
  <div
@@ -2441,6 +2460,7 @@ export function MotionDemo({
2441
2460
  type: isSpring ? 'spring' : 'tween',
2442
2461
  repeat,
2443
2462
  repeatType,
2463
+ repeatDelay,
2444
2464
  }}
2445
2465
  className="motion-demo__target"
2446
2466
  aria-label={label}
@@ -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"],