@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
@@ -1210,6 +1210,22 @@
1210
1210
  color: var(--u-fg-1);
1211
1211
  }
1212
1212
 
1213
+ .sb-sync {
1214
+ cursor: help;
1215
+ }
1216
+
1217
+ .sb-sync-dot {
1218
+ background: var(--u-status-warn);
1219
+ border-radius: 50%;
1220
+ flex: none;
1221
+ width: 7px;
1222
+ height: 7px;
1223
+ }
1224
+
1225
+ .sb-sync .sb-key {
1226
+ color: var(--u-fg-2);
1227
+ }
1228
+
1213
1229
  .sb-spacer {
1214
1230
  border-right: 1px solid var(--u-border-subtle);
1215
1231
  flex: 1 0 auto;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @file dom-selection.ts — selection-from-DOM helpers (leaf module)
3
+ * @scope plugins/design/dev-server/dom-selection.ts
4
+ * @purpose Pure DOM → Selection builders shared by the canvas chrome
5
+ * (canvas-shell.tsx) and the shell-owned comment mount layer
6
+ * (canvas-comment-mount.tsx). Lives in its own leaf module — no
7
+ * React, no canvas-lib import — so both consumers can lift the
8
+ * same `hoverTargetToSelection` / `deriveFile` logic without a
9
+ * cycle and without bundling the heavy DesignCanvas tree into the
10
+ * lite comment mount.
11
+ */
12
+
13
+ import type { HoverTarget } from './input-router.tsx';
14
+ import type { Selection } from './use-selection-set.tsx';
15
+
16
+ /**
17
+ * Canvas file path for the current page. Under the mount harness the page is
18
+ * `/_canvas-shell.html?canvas=<rel>&designRel=<root>`; for legacy `.html`
19
+ * mocks it's the served file path itself.
20
+ */
21
+ export function deriveFile(): string | undefined {
22
+ if (typeof window === 'undefined') return undefined;
23
+ try {
24
+ const p = window.location.pathname;
25
+ if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
26
+ const qs = new URLSearchParams(window.location.search);
27
+ const canvas = qs.get('canvas') ?? '';
28
+ const designRel = (qs.get('designRel') ?? '.design').replace(/^\/+|\/+$/g, '');
29
+ return canvas ? `${designRel}/${canvas}` : undefined;
30
+ }
31
+ return decodeURIComponent(p).replace(/^\//, '');
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ export function realClasses(el: Element | null): string {
38
+ if (!el) return '';
39
+ return (el.getAttribute('class') ?? '')
40
+ .trim()
41
+ .split(/\s+/)
42
+ .filter((c) => c && !c.startsWith('dgn-') && !c.startsWith('dc-cv-'))
43
+ .join(' ');
44
+ }
45
+
46
+ export function shortText(el: Element | null, max: number): string {
47
+ if (!el) return '';
48
+ const t = ((el as HTMLElement).innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
49
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
50
+ }
51
+
52
+ export function cssPath(el: Element | null): string {
53
+ if (!el) return '';
54
+ const path: string[] = [];
55
+ let cur: Element | null = el;
56
+ while (cur && cur.nodeType === 1 && path.length < 8) {
57
+ const dscEl = cur.getAttribute?.('data-dc-element');
58
+ if (dscEl) {
59
+ path.unshift(`[data-dc-element="${dscEl}"]`);
60
+ break;
61
+ }
62
+ const dscSc = cur.getAttribute?.('data-dc-screen');
63
+ if (dscSc) {
64
+ path.unshift(`[data-dc-screen="${dscSc}"]`);
65
+ break;
66
+ }
67
+ let sel = cur.nodeName.toLowerCase();
68
+ if (cur.id) {
69
+ sel = `#${cur.id}`;
70
+ path.unshift(sel);
71
+ break;
72
+ }
73
+ const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
74
+ if (cls.length) sel += `.${cls.join('.')}`;
75
+ let sib = 1;
76
+ let n: Element | null = cur.previousElementSibling;
77
+ while (n) {
78
+ sib++;
79
+ n = n.previousElementSibling;
80
+ }
81
+ sel += `:nth-child(${sib})`;
82
+ path.unshift(sel);
83
+ cur = cur.parentElement;
84
+ }
85
+ return path.join(' > ');
86
+ }
87
+
88
+ export function domPath(el: Element | null): string[] {
89
+ const hops: string[] = [];
90
+ let cur = el;
91
+ while (cur && cur.nodeType === 1 && hops.length < 8) {
92
+ let label = cur.nodeName.toLowerCase();
93
+ const dEl = cur.getAttribute?.('data-dc-element');
94
+ const dSc = cur.getAttribute?.('data-dc-screen');
95
+ if (dEl) label += `[data-dc-element="${dEl}"]`;
96
+ else if (dSc) label += `[data-dc-screen="${dSc}"]`;
97
+ else if (cur.id) label += `#${cur.id}`;
98
+ const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
99
+ if (cls.length && !dEl && !dSc) label += `.${cls.join('.')}`;
100
+ hops.unshift(label);
101
+ cur = cur.parentElement;
102
+ }
103
+ return hops;
104
+ }
105
+
106
+ export function cssEscape(s: string): string {
107
+ // Minimal CSS.escape polyfill — only handles chars actually present in
108
+ // pipeline-stamped IDs (alphanumerics + `-` + `_`).
109
+ return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
110
+ }
111
+
112
+ /**
113
+ * Build the wire-shape `Selection` for a resolved hover target. `file`
114
+ * defaults to `deriveFile()`; the comment mount layer passes it explicitly
115
+ * so all three consumers (router, overlay, mount) agree on the same key.
116
+ */
117
+ export function hoverTargetToSelection(target: HoverTarget, file?: string): Selection {
118
+ const el = target.el;
119
+ const rect =
120
+ el && (el as HTMLElement).getBoundingClientRect
121
+ ? (el as HTMLElement).getBoundingClientRect()
122
+ : null;
123
+ // `cdId` is the hit element's OWN data-cd-id (deep mode); resolver never
124
+ // climbs to an ancestor. Falls back to cssPath of the hit when no stable
125
+ // anchor exists.
126
+ const cdId = target.cdId;
127
+ // Selector resolution order:
128
+ // 1. data-cd-id anchor — stable pipeline-stamped id (preferred).
129
+ // 2. data-dc-screen — chrome click promoted to whole-artboard select
130
+ // (T24.5 G8 multi-artboard gesture).
131
+ // 3. cssPath of the hit — last-resort path string.
132
+ const selector = cdId
133
+ ? `[data-cd-id="${cdId}"]`
134
+ : !cdId && target.artboardId
135
+ ? `[data-dc-screen="${target.artboardId}"]`
136
+ : cssPath(el);
137
+ return {
138
+ file: file ?? deriveFile(),
139
+ id: cdId ?? undefined,
140
+ selector,
141
+ artboardId: target.artboardId,
142
+ tag: el?.tagName.toLowerCase() ?? '',
143
+ classes: realClasses(el),
144
+ text: shortText(el, 240),
145
+ dom_path: domPath(el),
146
+ bounds: rect
147
+ ? {
148
+ x: Math.round(rect.left),
149
+ y: Math.round(rect.top),
150
+ w: Math.round(rect.width),
151
+ h: Math.round(rect.height),
152
+ }
153
+ : null,
154
+ html: el ? (el.outerHTML ?? '').slice(0, 4000) : '',
155
+ };
156
+ }
@@ -48,7 +48,7 @@ const STYLES = `
48
48
  .dc-cv-eq-pill {
49
49
  position: absolute;
50
50
  transform: translate(-50%, calc(-100% - 8px));
51
- font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
51
+ font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, monospace);
52
52
  font-size: 10px;
53
53
  padding: 2px 5px;
54
54
  background: #FF24BD;
@@ -113,35 +113,35 @@ const ExportDialogContext = createContext<ExportDialogValue | null>(null);
113
113
 
114
114
  const DIALOG_CSS = `
115
115
  .dc-export-dialog {
116
- border: 1px solid var(--u-fg-0, #1c1917);
116
+ border: 1px solid var(--maude-chrome-fg-0, #1c1917);
117
117
  padding: 0;
118
118
  border-radius: 8px;
119
- background: var(--u-bg-0, var(--bg-0, #fff));
120
- box-shadow: 0 6px 24px color-mix(in oklab, var(--u-fg-0, #1c1917) 10%, transparent);
121
- font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
122
- color: var(--u-fg-0, var(--fg-0, #1a1a1a));
119
+ background: var(--maude-chrome-bg-0, #fff);
120
+ box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
121
+ font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
122
+ color: var(--maude-chrome-fg-0, #1a1a1a);
123
123
  width: min(640px, 100vw - 48px);
124
124
  max-height: min(560px, 100vh - 48px);
125
125
  overflow: hidden;
126
126
  }
127
127
  .dc-export-dialog::backdrop { background: rgba(20, 20, 30, 0.32); }
128
- .dc-export-dialog header { padding: 16px 20px; border-bottom: 1px solid var(--u-border-subtle, rgba(0,0,0,0.08)); display: flex; justify-content: space-between; align-items: center; }
128
+ .dc-export-dialog header { padding: 16px 20px; border-bottom: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08)); display: flex; justify-content: space-between; align-items: center; }
129
129
  .dc-export-dialog header h2 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.005em; }
130
- .dc-export-dialog header .dc-ed-close { background: transparent; border: 0; cursor: pointer; padding: 4px 8px; color: var(--fg-1, rgba(40,30,20,0.6)); font: inherit; font-size: 12px; }
130
+ .dc-export-dialog header .dc-ed-close { background: transparent; border: 0; cursor: pointer; padding: 4px 8px; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.6)); font: inherit; font-size: 12px; }
131
131
  .dc-export-dialog .dc-ed-body { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px 20px; }
132
- .dc-export-dialog label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-2, rgba(40,30,20,0.5)); margin-bottom: 6px; }
133
- .dc-export-dialog select { width: 100%; padding: 8px 10px; border-radius: 0; border: 1px solid var(--u-fg-0, rgba(0,0,0,0.12)); background: var(--u-bg-1, var(--bg-0, #fafafa)); font: inherit; font-size: 13px; color: inherit; }
134
- .dc-export-dialog .dc-ed-desc { font-size: 12px; color: var(--fg-1, rgba(40,30,20,0.65)); margin-top: 6px; line-height: 1.4; }
135
- .dc-export-dialog .dc-ed-recent { padding: 12px 20px; border-top: 1px solid var(--u-border-subtle, rgba(0,0,0,0.08)); background: var(--bg-2, rgba(0,0,0,0.02)); }
136
- .dc-export-dialog .dc-ed-recent h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-2, rgba(40,30,20,0.5)); margin: 0 0 8px; }
132
+ .dc-export-dialog label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.5)); margin-bottom: 6px; }
133
+ .dc-export-dialog select { width: 100%; padding: 8px 10px; border-radius: 0; border: 1px solid var(--maude-chrome-fg-0, rgba(0,0,0,0.12)); background: var(--maude-chrome-bg-1, #fafafa); font: inherit; font-size: 13px; color: inherit; }
134
+ .dc-export-dialog .dc-ed-desc { font-size: 12px; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.65)); margin-top: 6px; line-height: 1.4; }
135
+ .dc-export-dialog .dc-ed-recent { padding: 12px 20px; border-top: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08)); background: var(--maude-chrome-bg-2, rgba(0,0,0,0.02)); }
136
+ .dc-export-dialog .dc-ed-recent h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.5)); margin: 0 0 8px; }
137
137
  .dc-export-dialog .dc-ed-recent ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; max-height: 120px; overflow-y: auto; }
138
138
  .dc-export-dialog .dc-ed-recent button { display: flex; justify-content: space-between; gap: 12px; padding: 5px 12px; background: transparent; border: 1px solid transparent; border-radius: 0; cursor: pointer; font: inherit; font-size: 12px; color: inherit; width: 100%; text-align: left; }
139
- .dc-export-dialog .dc-ed-recent button:hover { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.06); }
140
- .dc-export-dialog footer { padding: 12px 20px; border-top: 1px solid var(--u-border-subtle, rgba(0,0,0,0.08)); display: flex; justify-content: flex-end; gap: 8px; }
141
- .dc-export-dialog footer button { padding: 8px 14px; border-radius: 0; border: 1px solid var(--u-fg-0, rgba(0,0,0,0.12)); background: var(--u-bg-1, var(--bg-0, #fafafa)); font: inherit; font-size: 12px; cursor: pointer; color: inherit; }
139
+ .dc-export-dialog .dc-ed-recent button:hover { background: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 5%, transparent); border-color: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 8%, transparent); }
140
+ .dc-export-dialog footer { padding: 12px 20px; border-top: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08)); display: flex; justify-content: flex-end; gap: 8px; }
141
+ .dc-export-dialog footer button { padding: 8px 14px; border-radius: 0; border: 1px solid var(--maude-chrome-fg-0, rgba(0,0,0,0.12)); background: var(--maude-chrome-bg-1, #fafafa); font: inherit; font-size: 12px; cursor: pointer; color: inherit; }
142
142
  .dc-export-dialog footer button.dc-ed-primary { background: var(--maude-hud-accent, #1a1a1a); color: var(--maude-hud-accent-fg, #fff); border-color: transparent; }
143
143
  .dc-export-dialog footer button:disabled { opacity: 0.4; cursor: not-allowed; }
144
- .dc-export-dialog .dc-ed-status { padding: 8px 20px; font-size: 12px; color: var(--fg-1, rgba(40,30,20,0.65)); border-top: 1px solid var(--u-border-subtle, rgba(0,0,0,0.08)); }
144
+ .dc-export-dialog .dc-ed-status { padding: 8px 20px; font-size: 12px; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.65)); border-top: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08)); }
145
145
  .dc-export-dialog .dc-ed-status.is-error { color: var(--status-error, #c0392b); }
146
146
  `;
147
147
 
@@ -371,7 +371,9 @@ const DialogShell = (() => {
371
371
  <span>
372
372
  {FORMAT_META[h.format].label} · {SCOPE_META[h.scope].label}
373
373
  </span>
374
- <span style={{ color: 'var(--fg-2, rgba(40,30,20,0.5))' }}>{h.filename}</span>
374
+ <span style={{ color: 'var(--maude-chrome-fg-1, rgba(40,30,20,0.5))' }}>
375
+ {h.filename}
376
+ </span>
375
377
  </button>
376
378
  </li>
377
379
  ))}
@@ -40,6 +40,7 @@ export function createFsWatch(ctx: Context): FsWatch {
40
40
  // Skip our own runtime artifacts.
41
41
  if (filename.startsWith('_server.json')) return;
42
42
  if (filename.startsWith('_active.json')) return;
43
+ if (filename.startsWith('_sync.json')) return;
43
44
  if (filename.startsWith(`_history${path.sep}`) || filename.startsWith('_history/')) return;
44
45
  emit(filename);
45
46
  });
@@ -17,6 +17,7 @@
17
17
  // loaded via the importmap + Bun.build-produced ESM — there's no React Fast
18
18
  // Refresh runtime to register with. Full-reload is the reliable path.
19
19
 
20
+ import { existsSync } from 'node:fs';
20
21
  import path from 'node:path';
21
22
 
22
23
  import type { Context } from './context.ts';
@@ -64,26 +65,11 @@ export function createHmrBroadcaster(
64
65
  }
65
66
 
66
67
  function classify(filename: string): HmrMessage | null {
67
- const rel = filename.replace(/\\/g, '/');
68
- const ext = path.extname(rel).toLowerCase();
69
- const version = Date.now();
70
- if (rel.startsWith('_lib/')) {
71
- return { type: 'canvas-hmr', mode: 'hard', scope: 'lib', version };
72
- }
73
- if (ext === '.css') {
74
- return { type: 'canvas-hmr', mode: 'css', file: rel, version, scope: 'canvas' };
75
- }
76
- // Phase 8 — canvas-meta sidecar (`<base>.meta.json`) carries the
77
- // artboard layout / viewport. Emit a `meta` mode so foreign tabs can
78
- // re-fetch + re-apply the layout WITHOUT a full reload (which would
79
- // lose React state like tool mode, undo stack, scroll position).
80
- if (rel.endsWith('.meta.json')) {
81
- return { type: 'canvas-hmr', mode: 'meta', file: rel, version, scope: 'canvas' };
82
- }
83
- if (ext === '.tsx' || ext === '.jsx' || ext === '.ts' || ext === '.js') {
84
- return { type: 'canvas-hmr', mode: 'module', file: rel, version, scope: 'canvas' };
85
- }
86
- return null;
68
+ return classifyChange(filename, (cssRel) => {
69
+ const root = ctx.paths?.designRoot;
70
+ if (!root) return false; // no root resolved → can't probe; keep css swap
71
+ return existsSync(path.join(root, cssRel.replace(/\.css$/i, '.tsx')));
72
+ });
87
73
  }
88
74
 
89
75
  function enqueue(msg: HmrMessage) {
@@ -125,3 +111,48 @@ export function createHmrBroadcaster(
125
111
  // Helpers — exported for tests.
126
112
 
127
113
  export const HMR_DEBOUNCE_MS = DEBOUNCE_MS;
114
+
115
+ /**
116
+ * Classify a changed design-root-relative path into an HMR message. Pure +
117
+ * fs-injected (`hasSiblingTsx`) so it unit-tests without touching disk.
118
+ *
119
+ * CSS routing is the load-bearing part. A canvas/specimen sibling stylesheet
120
+ * (e.g. `system/x/preview/motion.css` next to `motion.tsx`, pulled in via
121
+ * `import './motion.css'`) is INLINED into the built module as a `<style>` tag
122
+ * by canvas-build.ts — there is NO `<link>` for it. The iframe's `mode:'css'`
123
+ * handler swaps `<link href>` only, so a link swap for inlined CSS is a silent
124
+ * no-op and the edit never reaches the browser (the bug this fixes). For those
125
+ * we emit `mode:'module'` keyed on the sibling `.tsx`, so the open canvas
126
+ * reloads and re-inlines the fresh CSS via the existing module-reload path.
127
+ * Link-mounted CSS (DS tokens, `_components.css`, `_layout.css` — no sibling
128
+ * `.tsx`) keeps the fast, state-preserving css swap.
129
+ */
130
+ export function classifyChange(
131
+ filename: string,
132
+ hasSiblingTsx: (cssRel: string) => boolean
133
+ ): HmrMessage | null {
134
+ const rel = filename.replace(/\\/g, '/');
135
+ const ext = path.extname(rel).toLowerCase();
136
+ const version = Date.now();
137
+ if (rel.startsWith('_lib/')) {
138
+ return { type: 'canvas-hmr', mode: 'hard', scope: 'lib', version };
139
+ }
140
+ if (ext === '.css') {
141
+ if (hasSiblingTsx(rel)) {
142
+ const siblingTsx = rel.replace(/\.css$/i, '.tsx');
143
+ return { type: 'canvas-hmr', mode: 'module', file: siblingTsx, version, scope: 'canvas' };
144
+ }
145
+ return { type: 'canvas-hmr', mode: 'css', file: rel, version, scope: 'canvas' };
146
+ }
147
+ // Phase 8 — canvas-meta sidecar (`<base>.meta.json`) carries the
148
+ // artboard layout / viewport. Emit a `meta` mode so foreign tabs can
149
+ // re-fetch + re-apply the layout WITHOUT a full reload (which would
150
+ // lose React state like tool mode, undo stack, scroll position).
151
+ if (rel.endsWith('.meta.json')) {
152
+ return { type: 'canvas-hmr', mode: 'meta', file: rel, version, scope: 'canvas' };
153
+ }
154
+ if (ext === '.tsx' || ext === '.jsx' || ext === '.ts' || ext === '.js') {
155
+ return { type: 'canvas-hmr', mode: 'module', file: rel, version, scope: 'canvas' };
156
+ }
157
+ return null;
158
+ }