@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
|
@@ -98,25 +98,63 @@ import { useUndoStack } from './use-undo-stack.tsx';
|
|
|
98
98
|
// plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
|
|
99
99
|
// = invisible). No per-element class stamping is used.
|
|
100
100
|
|
|
101
|
-
// HUD
|
|
101
|
+
// HUD / chrome token block. System-review 2026-05-27 (D-4) flagged that the
|
|
102
102
|
// dev-server chrome (toolbar + minimap + halos + marquee + AI banner) used
|
|
103
103
|
// `var(--accent, …)` which inherited the canvas DS palette — a violet StudyFi
|
|
104
104
|
// canvas turned the floating cursor toolbar violet. The HUD owns its own
|
|
105
105
|
// `--maude-hud-*` token family, set on `:root` of the canvas iframe document
|
|
106
|
-
// here. Canvas DSs do NOT define
|
|
107
|
-
// this block regardless of what the imported `:root { --accent: … }`
|
|
106
|
+
// here. Canvas DSs do NOT define these names, so HUD/chrome CSS resolves
|
|
107
|
+
// against this block regardless of what the imported `:root { --accent: … }`
|
|
108
|
+
// looks like.
|
|
108
109
|
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
110
|
+
// Two sub-families:
|
|
111
|
+
// • `--maude-hud-accent*` — theme-agnostic brand orange-rust. Defaults match
|
|
112
|
+
// the existing inline fallback (`#d63b1f`) so the accent never changes with
|
|
113
|
+
// the theme. Users who want to re-theme the HUD can set these via a
|
|
114
|
+
// `<style>` block AFTER this one (CSS cascade — later wins).
|
|
115
|
+
// • `--maude-chrome-*` — NEUTRAL surface/text/border family that DOES follow
|
|
116
|
+
// the Maude chrome theme (system-review 2026-05-28 D9). The whole canvas-
|
|
117
|
+
// shell chrome (workspace plane, dotted grid, floating toolbar, minimap,
|
|
118
|
+
// zoom HUD, popovers, halos, context menu, undo HUD, presence chrome) reads
|
|
119
|
+
// these instead of the DS `--bg-*`/`--fg-*` palette, so the chrome flips
|
|
120
|
+
// dark↔light with the rest of the dev-server while ARTBOARDS keep the theme
|
|
121
|
+
// their design system defines. The set is selected by a `data-maude-theme`
|
|
122
|
+
// attribute on the iframe `documentElement`, propagated over the existing
|
|
123
|
+
// `dgn:*` postMessage bridge (see CanvasRouter onMessage + app.jsx).
|
|
124
|
+
//
|
|
125
|
+
// Values mirror the Maude app-chrome neutrals (client/styles/1-tokens.css) so
|
|
126
|
+
// the in-iframe chrome and the outer shell read as one product. The DARK set is
|
|
127
|
+
// also the default (attribute absent / "dark") so a canvas that never receives
|
|
128
|
+
// a theme message renders coherent-dark — matching the dev-server's own default
|
|
129
|
+
// theme (readInitialTheme() → 'dark'). DDR — mirrors the `--maude-hud-*`
|
|
130
|
+
// precedent; the `data-maude-theme` attribute is deliberately SEPARATE from the
|
|
131
|
+
// DS `data-theme` so chrome theming never touches artboard palettes.
|
|
113
132
|
const HUD_TOKENS_CSS = `
|
|
114
|
-
:root
|
|
133
|
+
:root,
|
|
134
|
+
:root[data-maude-theme="dark"] {
|
|
115
135
|
--maude-hud-accent: #d63b1f;
|
|
116
136
|
--maude-hud-accent-hover: #b8331b;
|
|
117
137
|
--maude-hud-accent-active: #962a16;
|
|
118
138
|
--maude-hud-accent-fg: #ffffff;
|
|
119
139
|
--maude-hud-accent-tint: color-mix(in oklab, #d63b1f 14%, transparent);
|
|
140
|
+
|
|
141
|
+
--maude-chrome-bg-0: oklch(13% 0.012 60);
|
|
142
|
+
--maude-chrome-bg-1: oklch(17% 0.014 60);
|
|
143
|
+
--maude-chrome-bg-2: oklch(20% 0.016 60);
|
|
144
|
+
--maude-chrome-fg-0: oklch(94% 0.014 80);
|
|
145
|
+
--maude-chrome-fg-1: oklch(78% 0.014 80);
|
|
146
|
+
--maude-chrome-border: oklch(28% 0.018 60);
|
|
147
|
+
--maude-chrome-shadow: rgba(0, 0, 0, 0.45);
|
|
148
|
+
--maude-chrome-font-mono: 'Berkeley Mono', 'TX-02', 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
149
|
+
}
|
|
150
|
+
:root[data-maude-theme="light"] {
|
|
151
|
+
--maude-chrome-bg-0: oklch(97.5% 0.008 78);
|
|
152
|
+
--maude-chrome-bg-1: oklch(95.5% 0.010 78);
|
|
153
|
+
--maude-chrome-bg-2: oklch(93.0% 0.012 78);
|
|
154
|
+
--maude-chrome-fg-0: oklch(20% 0.020 50);
|
|
155
|
+
--maude-chrome-fg-1: oklch(38% 0.018 50);
|
|
156
|
+
--maude-chrome-border: oklch(86% 0.014 70);
|
|
157
|
+
--maude-chrome-shadow: color-mix(in oklab, oklch(20% 0.020 50) 14%, transparent);
|
|
120
158
|
}
|
|
121
159
|
`;
|
|
122
160
|
|
|
@@ -138,7 +176,7 @@ const HALO_CSS = `
|
|
|
138
176
|
elements. NO ring, NO ticks. Synchronous paint (no debounce). */
|
|
139
177
|
.dc-cv-halo--hover {
|
|
140
178
|
border: 1.5px solid color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 60%, transparent);
|
|
141
|
-
box-shadow: inset 0 0 0 1px var(--bg-0, #ffffff);
|
|
179
|
+
box-shadow: inset 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
142
180
|
}
|
|
143
181
|
/* Selected (single) — 2px solid + 18% ring halo + 4 filled corner ticks.
|
|
144
182
|
Ticks are <i class="tick tick-*"> children at inset:-3px, 8x8, accent fill. */
|
|
@@ -152,7 +190,7 @@ const HALO_CSS = `
|
|
|
152
190
|
height: 8px;
|
|
153
191
|
background: var(--maude-hud-accent, #0d99ff);
|
|
154
192
|
border-radius: 1px;
|
|
155
|
-
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
193
|
+
box-shadow: 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
156
194
|
}
|
|
157
195
|
.dc-cv-halo--selected .tick-tl { top: -3px; left: -3px; }
|
|
158
196
|
.dc-cv-halo--selected .tick-tr { top: -3px; right: -3px; }
|
|
@@ -186,7 +224,7 @@ const HALO_CSS = `
|
|
|
186
224
|
height: 6px;
|
|
187
225
|
background: var(--maude-hud-accent, #0d99ff);
|
|
188
226
|
border-radius: 1px;
|
|
189
|
-
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
227
|
+
box-shadow: 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
190
228
|
}
|
|
191
229
|
.dc-cv-group-bbox .tick-tl { top: -3px; left: -3px; }
|
|
192
230
|
.dc-cv-group-bbox .tick-tr { top: -3px; right: -3px; }
|
|
@@ -313,6 +351,23 @@ export function CanvasShell({
|
|
|
313
351
|
children: ReactNode;
|
|
314
352
|
}) {
|
|
315
353
|
ensureHaloStyles();
|
|
354
|
+
|
|
355
|
+
// D9 — "Follow chrome" artboards. When the chrome theme flips (the theme
|
|
356
|
+
// postMessage handler in CanvasRouter updates `data-maude-theme` on <html>),
|
|
357
|
+
// re-point every artboard the user marked as a follower at the new theme.
|
|
358
|
+
// A single observer per canvas (cleaned up on unmount) — no per-artboard
|
|
359
|
+
// listeners to leak. The override itself is a CSS rule keyed by data-dc-screen
|
|
360
|
+
// (see setArtboardTheme), so it survives React re-renders without flicker.
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') return;
|
|
363
|
+
const obs = new MutationObserver(() => applyArtboardFollowers());
|
|
364
|
+
obs.observe(document.documentElement, {
|
|
365
|
+
attributes: true,
|
|
366
|
+
attributeFilter: ['data-maude-theme'],
|
|
367
|
+
});
|
|
368
|
+
return () => obs.disconnect();
|
|
369
|
+
}, []);
|
|
370
|
+
|
|
316
371
|
// ToolProvider is mounted by DesignCanvas one level up (so the viewport
|
|
317
372
|
// controller's `isPanDragActive` predicate can read the live tool state).
|
|
318
373
|
// SelectionSetProvider is mounted via MaybeSelectionSetProvider — the shell-
|
|
@@ -659,6 +714,194 @@ function CanvasCore({
|
|
|
659
714
|
);
|
|
660
715
|
}
|
|
661
716
|
|
|
717
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
718
|
+
// Per-artboard theme override (D9). The canvas-shell CHROME follows the Maude
|
|
719
|
+
// theme via `data-maude-theme`; ARTBOARDS keep their DS theme by default. This
|
|
720
|
+
// block backs the right-click `Theme ▸ DS default / Light / Dark / Follow
|
|
721
|
+
// chrome` submenu — it re-themes a single artboard by stamping its
|
|
722
|
+
// `.dc-artboard` <article> with the DS's theme-wrapper convention.
|
|
723
|
+
//
|
|
724
|
+
// DS theme-wrapper conventions vary (`.mdcc[data-theme]`, `.app[data-theme]`,
|
|
725
|
+
// bare `[data-theme]`, …) and there's no reliable config flag, so we DETECT it
|
|
726
|
+
// with a hidden computed-style probe: stamp a throwaway nested <div> with each
|
|
727
|
+
// candidate `<class>[data-theme=light|dark]` and keep the first whose resolved
|
|
728
|
+
// `--bg-0` differs between light and dark. Because the probe tests a NON-root
|
|
729
|
+
// element, "supported" is exactly "stamping one artboard will work" — a DS that
|
|
730
|
+
// only themes `:root[data-theme]` correctly reports unsupported (you genuinely
|
|
731
|
+
// can't theme a single artboard there).
|
|
732
|
+
|
|
733
|
+
interface DsThemeSupport {
|
|
734
|
+
supported: boolean;
|
|
735
|
+
/** The class the DS scopes its theme blocks to (`''` = bare `[data-theme]`). */
|
|
736
|
+
wrapperClass: string;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let _dsThemeSupport: DsThemeSupport | null = null;
|
|
740
|
+
|
|
741
|
+
function detectDsThemeSupport(): DsThemeSupport {
|
|
742
|
+
if (_dsThemeSupport) return _dsThemeSupport;
|
|
743
|
+
const fallback: DsThemeSupport = { supported: false, wrapperClass: '' };
|
|
744
|
+
if (typeof document === 'undefined' || !document.body) return fallback;
|
|
745
|
+
try {
|
|
746
|
+
const host = document.createElement('div');
|
|
747
|
+
host.setAttribute('aria-hidden', 'true');
|
|
748
|
+
host.style.cssText =
|
|
749
|
+
'position:absolute;left:-9999px;top:0;width:0;height:0;overflow:hidden;pointer-events:none;';
|
|
750
|
+
document.body.appendChild(host);
|
|
751
|
+
const read = (cls: string, theme: string): string => {
|
|
752
|
+
const el = document.createElement('div');
|
|
753
|
+
if (cls) el.className = cls;
|
|
754
|
+
el.setAttribute('data-theme', theme);
|
|
755
|
+
host.appendChild(el);
|
|
756
|
+
const v = getComputedStyle(el).getPropertyValue('--bg-0').trim();
|
|
757
|
+
host.removeChild(el);
|
|
758
|
+
return v;
|
|
759
|
+
};
|
|
760
|
+
// Bare attribute first (cleanest), then the common class conventions.
|
|
761
|
+
let found = fallback;
|
|
762
|
+
for (const cls of ['', 'mdcc', 'app']) {
|
|
763
|
+
const light = read(cls, 'light');
|
|
764
|
+
const dark = read(cls, 'dark');
|
|
765
|
+
if (light && dark && light !== dark) {
|
|
766
|
+
found = { supported: true, wrapperClass: cls };
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
document.body.removeChild(host);
|
|
771
|
+
_dsThemeSupport = found;
|
|
772
|
+
return found;
|
|
773
|
+
} catch {
|
|
774
|
+
return fallback;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Why not just stamp `data-theme` on the `.dc-artboard` element? Two reasons,
|
|
779
|
+
// both found the hard way: (1) React OWNS the artboard <article> + the canvas
|
|
780
|
+
// content's `rootClass` wrapper — it reconciles their className/attrs back to
|
|
781
|
+
// the JSX values on every re-render, wiping any imperative mutation; and (2)
|
|
782
|
+
// the canvas content carries its OWN `rootClass[data-theme]` wrapper (the DS
|
|
783
|
+
// default), which re-establishes the default tokens BELOW the artboard, so
|
|
784
|
+
// setting `data-theme` on the outer article never reaches the content. The
|
|
785
|
+
// robust mechanism is a single injected <style> keyed by the STABLE
|
|
786
|
+
// `data-dc-screen` attribute (which React always re-renders WITH), re-declaring
|
|
787
|
+
// the chosen theme's `--*` tokens scoped to the artboard's content wrapper.
|
|
788
|
+
// Survives re-renders, beats the wrapper on cascade order, zero flicker.
|
|
789
|
+
|
|
790
|
+
// Reject token VALUES that could turn the re-emitted <style> into a resource
|
|
791
|
+
// fetch / exfil beacon. Per the CSS spec a custom-property value can't contain
|
|
792
|
+
// an unmatched top-level `}` (so brace-breakout to other selectors is already
|
|
793
|
+
// impossible), but `url()` / `image()` / `@import` / comments survive verbatim.
|
|
794
|
+
// For a TRUSTED same-origin DS this is inert, but a DDR-054 untrusted synced
|
|
795
|
+
// canvas could ship `--bg-0: #fff url(https://attacker/x?leak)` — copying it
|
|
796
|
+
// here would fire that fetch when the artboard renders. Drop such values; the
|
|
797
|
+
// token simply falls back to the DS default for that artboard. (Security
|
|
798
|
+
// review F2, 2026-05-29 — defense-in-depth on top of the canvas-origin CSP.)
|
|
799
|
+
const _UNSAFE_TOKEN_VALUE = /url\(|image\(|image-set\(|-image-set\(|@import|\/\*|expression\(/i;
|
|
800
|
+
|
|
801
|
+
// Collect a DS theme block's custom-property declarations by scanning the
|
|
802
|
+
// loaded stylesheets for top-level rules whose selector targets that theme.
|
|
803
|
+
// Only `--*` props are copied (token re-definitions) — never layout/type, and
|
|
804
|
+
// never @media-nested rules (e.g. the prefers-reduced-motion 1ms collapse).
|
|
805
|
+
const _themeDecls: Partial<Record<'light' | 'dark', string>> = {};
|
|
806
|
+
function collectThemeDeclarations(theme: 'light' | 'dark'): string {
|
|
807
|
+
const cached = _themeDecls[theme];
|
|
808
|
+
if (cached != null) return cached;
|
|
809
|
+
let decls = '';
|
|
810
|
+
if (typeof document !== 'undefined') {
|
|
811
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
812
|
+
let rules: CSSRuleList | undefined;
|
|
813
|
+
try {
|
|
814
|
+
rules = sheet.cssRules;
|
|
815
|
+
} catch {
|
|
816
|
+
continue; // cross-origin / unreadable sheet
|
|
817
|
+
}
|
|
818
|
+
if (!rules) continue;
|
|
819
|
+
for (const rule of Array.from(rules)) {
|
|
820
|
+
if (!(rule instanceof CSSStyleRule)) continue;
|
|
821
|
+
if (!(rule.selectorText || '').includes(`[data-theme="${theme}"]`)) continue;
|
|
822
|
+
const style = rule.style;
|
|
823
|
+
for (let i = 0; i < style.length; i++) {
|
|
824
|
+
const prop = style[i];
|
|
825
|
+
if (!prop.startsWith('--')) continue;
|
|
826
|
+
const value = style.getPropertyValue(prop);
|
|
827
|
+
if (_UNSAFE_TOKEN_VALUE.test(value)) continue;
|
|
828
|
+
decls += `${prop}:${value};`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
_themeDecls[theme] = decls;
|
|
834
|
+
return decls;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const _artboardThemes = new Map<string, 'light' | 'dark'>();
|
|
838
|
+
const _artboardFollowers = new Set<string>();
|
|
839
|
+
|
|
840
|
+
function cssEsc(v: string): string {
|
|
841
|
+
return typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(v) : v;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Selector targeting the artboard's content wrapper (where the DS theme block
|
|
845
|
+
// is re-established). Beats `.<wrapper>[data-theme="…"]` because the injected
|
|
846
|
+
// <style> is appended after the DS stylesheet (later-wins on equal specificity).
|
|
847
|
+
function artboardScopeSelector(screenId: string): string {
|
|
848
|
+
const { wrapperClass } = detectDsThemeSupport();
|
|
849
|
+
const base = `[data-dc-screen="${cssEsc(screenId)}"]`;
|
|
850
|
+
if (wrapperClass) {
|
|
851
|
+
const c = cssEsc(wrapperClass);
|
|
852
|
+
return `${base} .${c},${base}.${c}`;
|
|
853
|
+
}
|
|
854
|
+
return `${base} [data-theme],${base}[data-theme]`;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function rebuildArtboardThemeStyle(): void {
|
|
858
|
+
if (typeof document === 'undefined') return;
|
|
859
|
+
let el = document.getElementById('dc-artboard-theme-css') as HTMLStyleElement | null;
|
|
860
|
+
if (!el) {
|
|
861
|
+
el = document.createElement('style');
|
|
862
|
+
el.id = 'dc-artboard-theme-css';
|
|
863
|
+
document.head.appendChild(el);
|
|
864
|
+
}
|
|
865
|
+
let css = '';
|
|
866
|
+
for (const [screenId, theme] of _artboardThemes) {
|
|
867
|
+
const decls = collectThemeDeclarations(theme);
|
|
868
|
+
if (decls) css += `${artboardScopeSelector(screenId)}{${decls}}\n`;
|
|
869
|
+
}
|
|
870
|
+
el.textContent = css;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Current canvas-shell chrome theme (mirrors `data-maude-theme` on <html>). */
|
|
874
|
+
function currentChromeTheme(): 'light' | 'dark' {
|
|
875
|
+
if (typeof document === 'undefined') return 'dark';
|
|
876
|
+
return document.documentElement.dataset.maudeTheme === 'light' ? 'light' : 'dark';
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Set (or clear) a single artboard's theme override, keyed by its stable
|
|
881
|
+
* `data-dc-screen` id. `theme === null` → remove the override (DS default).
|
|
882
|
+
* `follow === true` → mirror the live chrome theme and keep tracking toggles.
|
|
883
|
+
*/
|
|
884
|
+
function setArtboardTheme(screenId: string, theme: 'light' | 'dark' | null, follow = false): void {
|
|
885
|
+
_artboardFollowers.delete(screenId);
|
|
886
|
+
if (follow) {
|
|
887
|
+
_artboardFollowers.add(screenId);
|
|
888
|
+
_artboardThemes.set(screenId, currentChromeTheme());
|
|
889
|
+
} else if (theme === null) {
|
|
890
|
+
_artboardThemes.delete(screenId);
|
|
891
|
+
} else {
|
|
892
|
+
_artboardThemes.set(screenId, theme);
|
|
893
|
+
}
|
|
894
|
+
rebuildArtboardThemeStyle();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Re-point every "Follow chrome" artboard at the current chrome theme. */
|
|
898
|
+
function applyArtboardFollowers(): void {
|
|
899
|
+
if (_artboardFollowers.size === 0) return;
|
|
900
|
+
const t = currentChromeTheme();
|
|
901
|
+
for (const id of _artboardFollowers) _artboardThemes.set(id, t);
|
|
902
|
+
rebuildArtboardThemeStyle();
|
|
903
|
+
}
|
|
904
|
+
|
|
662
905
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
663
906
|
// Registry builder — closes over controller + clear callback.
|
|
664
907
|
|
|
@@ -743,6 +986,56 @@ function buildRegistry(deps: {
|
|
|
743
986
|
},
|
|
744
987
|
});
|
|
745
988
|
|
|
989
|
+
// D9 — per-artboard theme override, keyed by the stable `data-dc-screen` id
|
|
990
|
+
// (target.artboardId) via an injected stylesheet (see setArtboardTheme). The
|
|
991
|
+
// DS-supports-both probe gates the explicit Light/Dark entries; "DS default"
|
|
992
|
+
// + "Follow chrome" are always available.
|
|
993
|
+
const themeSupport = detectDsThemeSupport();
|
|
994
|
+
const themeHint = 'This design system defines only one theme';
|
|
995
|
+
const themeItem: MenuItem = {
|
|
996
|
+
id: 'theme',
|
|
997
|
+
label: 'Theme',
|
|
998
|
+
onSelect: () => {
|
|
999
|
+
/* parent of a submenu — never invoked directly */
|
|
1000
|
+
},
|
|
1001
|
+
submenu: [
|
|
1002
|
+
{
|
|
1003
|
+
id: 'theme-ds-default',
|
|
1004
|
+
label: 'DS default',
|
|
1005
|
+
onSelect: (target) => {
|
|
1006
|
+
if (target.artboardId) setArtboardTheme(target.artboardId, null);
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
id: 'theme-light',
|
|
1011
|
+
label: 'Light',
|
|
1012
|
+
disabled: !themeSupport.supported,
|
|
1013
|
+
disabledHint: themeHint,
|
|
1014
|
+
onSelect: (target) => {
|
|
1015
|
+
if (target.artboardId) setArtboardTheme(target.artboardId, 'light');
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
id: 'theme-dark',
|
|
1020
|
+
label: 'Dark',
|
|
1021
|
+
disabled: !themeSupport.supported,
|
|
1022
|
+
disabledHint: themeHint,
|
|
1023
|
+
onSelect: (target) => {
|
|
1024
|
+
if (target.artboardId) setArtboardTheme(target.artboardId, 'dark');
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: 'theme-follow',
|
|
1029
|
+
label: 'Follow chrome',
|
|
1030
|
+
disabled: !themeSupport.supported,
|
|
1031
|
+
disabledHint: themeHint,
|
|
1032
|
+
onSelect: (target) => {
|
|
1033
|
+
if (target.artboardId) setArtboardTheme(target.artboardId, null, true);
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
};
|
|
1038
|
+
|
|
746
1039
|
return {
|
|
747
1040
|
element: [
|
|
748
1041
|
[
|
|
@@ -868,6 +1161,7 @@ function buildRegistry(deps: {
|
|
|
868
1161
|
onSelect: () => distributeArtboards('y'),
|
|
869
1162
|
},
|
|
870
1163
|
],
|
|
1164
|
+
[themeItem],
|
|
871
1165
|
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
872
1166
|
],
|
|
873
1167
|
world: [
|
|
@@ -944,6 +1238,13 @@ function CanvasRouter({
|
|
|
944
1238
|
// user closes the composer.
|
|
945
1239
|
useEffect(() => {
|
|
946
1240
|
if (typeof window === 'undefined') return;
|
|
1241
|
+
// Default the canvas-shell chrome theme to dark (matches the dev-server's
|
|
1242
|
+
// own default + the bare `:root` --maude-chrome-* set) so a canvas that
|
|
1243
|
+
// never receives a `dgn:'theme'` message still renders coherent-dark and
|
|
1244
|
+
// the follow-chrome observer (Task 5) has a concrete value to mirror.
|
|
1245
|
+
if (!document.documentElement.dataset.maudeTheme) {
|
|
1246
|
+
document.documentElement.dataset.maudeTheme = 'dark';
|
|
1247
|
+
}
|
|
947
1248
|
const onMessage = (e: MessageEvent) => {
|
|
948
1249
|
const m = e.data as { dgn?: string } | null;
|
|
949
1250
|
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
@@ -958,6 +1259,19 @@ function CanvasRouter({
|
|
|
958
1259
|
if (typeof t === 'string') setTool(t as never);
|
|
959
1260
|
return;
|
|
960
1261
|
}
|
|
1262
|
+
// D9 — canvas-shell chrome follows the Maude chrome theme. The chrome's
|
|
1263
|
+
// `--maude-chrome-*` token family is keyed by `data-maude-theme` on the
|
|
1264
|
+
// iframe documentElement (see HUD_TOKENS_CSS). This attribute is
|
|
1265
|
+
// DELIBERATELY separate from the DS `data-theme`: it only re-themes the
|
|
1266
|
+
// floating chrome, never the artboard palettes. Followers (per-artboard
|
|
1267
|
+
// "Follow chrome") restamp via the MutationObserver in CanvasShell.
|
|
1268
|
+
if (m.dgn === 'theme') {
|
|
1269
|
+
const t = (m as { theme?: string }).theme;
|
|
1270
|
+
if (t === 'light' || t === 'dark') {
|
|
1271
|
+
document.documentElement.dataset.maudeTheme = t;
|
|
1272
|
+
}
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
961
1275
|
};
|
|
962
1276
|
window.addEventListener('message', onMessage);
|
|
963
1277
|
return () => window.removeEventListener('message', onMessage);
|
|
@@ -1085,14 +1399,14 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1085
1399
|
align-items: stretch;
|
|
1086
1400
|
gap: 2px;
|
|
1087
1401
|
padding: 4px;
|
|
1088
|
-
background: var(--
|
|
1089
|
-
border: 1px solid var(--
|
|
1402
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
1403
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
1090
1404
|
border-radius: 8px;
|
|
1091
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
1092
|
-
font-family: var(--
|
|
1405
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
1406
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1093
1407
|
font-size: 11px;
|
|
1094
1408
|
letter-spacing: 0.02em;
|
|
1095
|
-
color: var(--
|
|
1409
|
+
color: var(--maude-chrome-fg-0, #1a1a1a);
|
|
1096
1410
|
user-select: none;
|
|
1097
1411
|
}
|
|
1098
1412
|
.dc-multi-artboard-tb button {
|
|
@@ -1118,8 +1432,8 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1118
1432
|
}
|
|
1119
1433
|
.dc-multi-artboard-tb .dc-mab-count {
|
|
1120
1434
|
padding: 4px 8px 4px 10px;
|
|
1121
|
-
color: var(--fg-1, rgba(40,30,20,0.7));
|
|
1122
|
-
border-right: 1px solid var(--
|
|
1435
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.7));
|
|
1436
|
+
border-right: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
1123
1437
|
margin-right: 2px;
|
|
1124
1438
|
font-variant-numeric: tabular-nums;
|
|
1125
1439
|
}
|
|
@@ -1131,7 +1445,7 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1131
1445
|
.dc-multi-artboard-tb .dc-mab-divider {
|
|
1132
1446
|
width: 1px;
|
|
1133
1447
|
align-self: stretch;
|
|
1134
|
-
background: var(--
|
|
1448
|
+
background: var(--maude-chrome-border, rgba(0,0,0,0.10));
|
|
1135
1449
|
margin: 0 4px;
|
|
1136
1450
|
}
|
|
1137
1451
|
@media (prefers-reduced-motion: reduce) {
|