@1agh/maude 0.15.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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/cli/bin/maude.exe +15 -0
  4. package/cli/bin/maude.mjs +45 -0
  5. package/cli/bin/mdcc.exe +10 -0
  6. package/cli/bin/mdcc.mjs +7 -0
  7. package/cli/cli-wrapper.cjs +67 -0
  8. package/cli/commands/config.mjs +94 -0
  9. package/cli/commands/design.mjs +386 -0
  10. package/cli/commands/help.mjs +57 -0
  11. package/cli/commands/init.mjs +178 -0
  12. package/cli/commands/version.mjs +7 -0
  13. package/cli/install.cjs +113 -0
  14. package/cli/lib/argv.mjs +37 -0
  15. package/cli/lib/argv.test.mjs +46 -0
  16. package/cli/lib/copy-tree.mjs +78 -0
  17. package/package.json +94 -0
  18. package/plugins/design/dev-server/annotations-context-toolbar.tsx +397 -0
  19. package/plugins/design/dev-server/annotations-layer.tsx +1717 -0
  20. package/plugins/design/dev-server/api.ts +674 -0
  21. package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +50 -0
  22. package/plugins/design/dev-server/bin/bootstrap-check.sh +83 -0
  23. package/plugins/design/dev-server/bin/canvas-edit.sh +48 -0
  24. package/plugins/design/dev-server/bin/handoff.sh +27 -0
  25. package/plugins/design/dev-server/bin/screenshot.sh +232 -0
  26. package/plugins/design/dev-server/bin/server-up.sh +135 -0
  27. package/plugins/design/dev-server/bin/slug.sh +22 -0
  28. package/plugins/design/dev-server/bin/smoke.sh +272 -0
  29. package/plugins/design/dev-server/build.ts +267 -0
  30. package/plugins/design/dev-server/canvas-build.ts +219 -0
  31. package/plugins/design/dev-server/canvas-edit.ts +388 -0
  32. package/plugins/design/dev-server/canvas-header.ts +165 -0
  33. package/plugins/design/dev-server/canvas-icons.tsx +131 -0
  34. package/plugins/design/dev-server/canvas-lib-inline.ts +260 -0
  35. package/plugins/design/dev-server/canvas-lib-resolver.ts +85 -0
  36. package/plugins/design/dev-server/canvas-lib.tsx +1995 -0
  37. package/plugins/design/dev-server/canvas-meta.schema.json +181 -0
  38. package/plugins/design/dev-server/canvas-pipeline.ts +270 -0
  39. package/plugins/design/dev-server/canvas-shell.tsx +813 -0
  40. package/plugins/design/dev-server/client/app.jsx +2027 -0
  41. package/plugins/design/dev-server/client/hmr.mjs +85 -0
  42. package/plugins/design/dev-server/client/iframe-lazy.mjs +121 -0
  43. package/plugins/design/dev-server/client/index.html +15 -0
  44. package/plugins/design/dev-server/client/styles/0-reset.css +18 -0
  45. package/plugins/design/dev-server/client/styles/1-tokens.css +297 -0
  46. package/plugins/design/dev-server/client/styles/2-layout.css +35 -0
  47. package/plugins/design/dev-server/client/styles/3-shell.css +906 -0
  48. package/plugins/design/dev-server/client/styles/4-components.css +1268 -0
  49. package/plugins/design/dev-server/client/styles/5-utilities.css +4 -0
  50. package/plugins/design/dev-server/client/styles/_index.css +24 -0
  51. package/plugins/design/dev-server/client/styles.css +1419 -0
  52. package/plugins/design/dev-server/config.schema.json +147 -0
  53. package/plugins/design/dev-server/context-menu.tsx +343 -0
  54. package/plugins/design/dev-server/context.ts +173 -0
  55. package/plugins/design/dev-server/dist/client.bundle.js +20323 -0
  56. package/plugins/design/dev-server/dist/styles.css +2875 -0
  57. package/plugins/design/dev-server/examples/README.md +9 -0
  58. package/plugins/design/dev-server/examples/perf-100-artboards.tsx +113 -0
  59. package/plugins/design/dev-server/fs-watch.ts +63 -0
  60. package/plugins/design/dev-server/handoff.ts +721 -0
  61. package/plugins/design/dev-server/history.ts +125 -0
  62. package/plugins/design/dev-server/hmr-broadcast.ts +114 -0
  63. package/plugins/design/dev-server/http.ts +413 -0
  64. package/plugins/design/dev-server/input-router.tsx +485 -0
  65. package/plugins/design/dev-server/inspect.ts +365 -0
  66. package/plugins/design/dev-server/locator.ts +159 -0
  67. package/plugins/design/dev-server/mem.ts +97 -0
  68. package/plugins/design/dev-server/runtime-bundle.ts +235 -0
  69. package/plugins/design/dev-server/server.mjs +1246 -0
  70. package/plugins/design/dev-server/server.ts +131 -0
  71. package/plugins/design/dev-server/test/_helpers.ts +81 -0
  72. package/plugins/design/dev-server/test/active-state.test.ts +145 -0
  73. package/plugins/design/dev-server/test/annotations-api.test.ts +146 -0
  74. package/plugins/design/dev-server/test/annotations-layer.test.ts +419 -0
  75. package/plugins/design/dev-server/test/binary-smoke.test.ts +47 -0
  76. package/plugins/design/dev-server/test/bundle-smoke.test.ts +29 -0
  77. package/plugins/design/dev-server/test/canvas-build.test.ts +78 -0
  78. package/plugins/design/dev-server/test/canvas-edit.test.ts +139 -0
  79. package/plugins/design/dev-server/test/canvas-header.test.ts +127 -0
  80. package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +146 -0
  81. package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +112 -0
  82. package/plugins/design/dev-server/test/canvas-meta-api.test.ts +236 -0
  83. package/plugins/design/dev-server/test/canvas-pipeline.test.ts +180 -0
  84. package/plugins/design/dev-server/test/canvas-route.test.ts +176 -0
  85. package/plugins/design/dev-server/test/fs-watch.test.ts +41 -0
  86. package/plugins/design/dev-server/test/handoff-static-frames.test.ts +215 -0
  87. package/plugins/design/dev-server/test/handoff.test.ts +281 -0
  88. package/plugins/design/dev-server/test/history-rollback.test.ts +62 -0
  89. package/plugins/design/dev-server/test/hmr-broadcast.test.ts +108 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +316 -0
  91. package/plugins/design/dev-server/test/locator.test.ts +214 -0
  92. package/plugins/design/dev-server/test/perf-harness.ts +193 -0
  93. package/plugins/design/dev-server/test/phase-3.6-smoke.test.ts +77 -0
  94. package/plugins/design/dev-server/test/runtime-bundle.test.ts +69 -0
  95. package/plugins/design/dev-server/test/server-lifecycle.test.ts +28 -0
  96. package/plugins/design/dev-server/test/tool-palette.test.tsx +55 -0
  97. package/plugins/design/dev-server/test/use-annotation-selection.test.tsx +77 -0
  98. package/plugins/design/dev-server/test/use-artboard-drag.test.ts +325 -0
  99. package/plugins/design/dev-server/test/use-selection-set.test.tsx +166 -0
  100. package/plugins/design/dev-server/test/use-snap-guides.test.ts +190 -0
  101. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +93 -0
  102. package/plugins/design/dev-server/test/ws-handshake.test.ts +33 -0
  103. package/plugins/design/dev-server/tool-palette.tsx +278 -0
  104. package/plugins/design/dev-server/tsconfig.json +26 -0
  105. package/plugins/design/dev-server/use-annotation-selection.tsx +92 -0
  106. package/plugins/design/dev-server/use-annotations-visibility.tsx +43 -0
  107. package/plugins/design/dev-server/use-artboard-drag.tsx +445 -0
  108. package/plugins/design/dev-server/use-selection-set.tsx +224 -0
  109. package/plugins/design/dev-server/use-snap-guides.tsx +215 -0
  110. package/plugins/design/dev-server/use-tool-mode.tsx +114 -0
  111. package/plugins/design/dev-server/ws.ts +90 -0
  112. package/plugins/design/templates/_shell.html +177 -0
  113. package/plugins/design/templates/canvas.tsx.template +54 -0
  114. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +277 -0
  115. package/plugins/design/templates/design-system-inspiration/_README.md +71 -0
  116. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-banner.html +68 -0
  117. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-empty-state-generous.html +39 -0
  118. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-feature-grid.html +62 -0
  119. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-marketing-card.html +63 -0
  120. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-testimonial.html +80 -0
  121. package/plugins/design/templates/design-system-inspiration/audience-developer/components-code-block.html +71 -0
  122. package/plugins/design/templates/design-system-inspiration/audience-developer/components-diff-view.html +65 -0
  123. package/plugins/design/templates/design-system-inspiration/audience-developer/components-log-stream.html +62 -0
  124. package/plugins/design/templates/design-system-inspiration/audience-developer/components-monospace-table.html +57 -0
  125. package/plugins/design/templates/design-system-inspiration/audience-developer/components-terminal-pane.html +67 -0
  126. package/plugins/design/templates/design-system-inspiration/audience-developer/type-mono.html +57 -0
  127. package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +74 -0
  128. package/plugins/design/templates/design-system-inspiration/audience-pro/components-command-palette.html +90 -0
  129. package/plugins/design/templates/design-system-inspiration/audience-pro/components-keyboard.html +51 -0
  130. package/plugins/design/templates/design-system-inspiration/audience-pro/components-list.html +89 -0
  131. package/plugins/design/templates/design-system-inspiration/audience-pro/components-shortcuts-overlay.html +85 -0
  132. package/plugins/design/templates/design-system-inspiration/audience-pro/components-toast-menu.html +80 -0
  133. package/plugins/design/templates/design-system-inspiration/core/INDEX.md.tpl +25 -0
  134. package/plugins/design/templates/design-system-inspiration/core/README.orchestration.md.tpl +71 -0
  135. package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +77 -0
  136. package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +50 -0
  137. package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +111 -0
  138. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +28 -0
  139. package/plugins/design/templates/design-system-inspiration/core/preview/_layout.css +113 -0
  140. package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +48 -0
  141. package/plugins/design/templates/design-system-inspiration/core/preview/colors-surfaces.html +49 -0
  142. package/plugins/design/templates/design-system-inspiration/core/preview/colors-text.html +52 -0
  143. package/plugins/design/templates/design-system-inspiration/core/preview/components-buttons.html +83 -0
  144. package/plugins/design/templates/design-system-inspiration/core/preview/components-cards.html +79 -0
  145. package/plugins/design/templates/design-system-inspiration/core/preview/components-inputs.html +82 -0
  146. package/plugins/design/templates/design-system-inspiration/core/preview/motion.html +66 -0
  147. package/plugins/design/templates/design-system-inspiration/core/preview/spacing-scale.html +53 -0
  148. package/plugins/design/templates/design-system-inspiration/core/preview/type-scale.html +62 -0
  149. package/plugins/design/templates/design-system-inspiration/foundations/borders.html +39 -0
  150. package/plugins/design/templates/design-system-inspiration/foundations/elevation.html +46 -0
  151. package/plugins/design/templates/design-system-inspiration/foundations/focus.html +48 -0
  152. package/plugins/design/templates/design-system-inspiration/foundations/grid.html +51 -0
  153. package/plugins/design/templates/design-system-inspiration/foundations/iconography.html +73 -0
  154. package/plugins/design/templates/design-system-inspiration/foundations/opacity.html +45 -0
  155. package/plugins/design/templates/design-system-inspiration/foundations/radii.html +40 -0
  156. package/plugins/design/templates/design-system-inspiration/foundations/selection.html +45 -0
  157. package/plugins/design/templates/design-system-inspiration/meta/accessibility.html +62 -0
  158. package/plugins/design/templates/design-system-inspiration/meta/i18n.html +73 -0
  159. package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +71 -0
  160. package/plugins/design/templates/design-system-inspiration/meta/tokens-index.html +80 -0
  161. package/plugins/design/templates/design-system-inspiration/patterns/patterns-auth.html +78 -0
  162. package/plugins/design/templates/design-system-inspiration/patterns/patterns-data-density.html +61 -0
  163. package/plugins/design/templates/design-system-inspiration/patterns/patterns-error-pages.html +70 -0
  164. package/plugins/design/templates/design-system-inspiration/patterns/patterns-form-layouts.html +70 -0
  165. package/plugins/design/templates/design-system-inspiration/patterns/patterns-onboarding.html +71 -0
  166. package/plugins/design/templates/design-system-inspiration/patterns/patterns-pricing.html +83 -0
  167. package/plugins/design/templates/design-system-inspiration/platform-desktop/components-resize-panels.html +63 -0
  168. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-index.html +55 -0
  169. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +302 -0
  170. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-bottom-sheet.html +63 -0
  171. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-pull-to-refresh.html +74 -0
  172. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-segmented-control.html +51 -0
  173. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-tab-bar.html +57 -0
  174. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-index.html +58 -0
  175. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +237 -0
  176. package/plugins/design/templates/design-system-inspiration/status/colors-status.html +49 -0
  177. package/plugins/design/templates/design-system-inspiration/status/components-status.html +63 -0
  178. package/plugins/design/templates/design-system-inspiration/status/skeletons.html +74 -0
  179. package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +59 -0
  180. package/plugins/design/templates/design-system-inspiration/universal/components-callout.html +74 -0
  181. package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +81 -0
  182. package/plugins/design/templates/design-system-inspiration/universal/components-tables.html +101 -0
  183. package/plugins/design/templates/design-system-inspiration/universal/components-toggles.html +74 -0
  184. package/plugins/design/templates/design-system-inspiration/universal/components-tooltips.html +74 -0
  185. package/plugins/design/templates/design-system-inspiration/universal/empty-state.html.tpl +34 -0
  186. package/plugins/design/templates/design-system-inspiration/universal/logo.html +42 -0
  187. package/plugins/design/templates/ds-specimen.tsx.template +59 -0
  188. package/plugins/flow/.claude-plugin/config.schema.json +398 -0
  189. package/plugins/flow/README.md +45 -0
  190. package/plugins/flow/templates/ai-skeleton/INDEX.md +50 -0
  191. package/plugins/flow/templates/ai-skeleton/README.md +46 -0
  192. package/plugins/flow/templates/ai-skeleton/browser/har/.gitkeep +0 -0
  193. package/plugins/flow/templates/ai-skeleton/browser/snapshots/.gitkeep +0 -0
  194. package/plugins/flow/templates/ai-skeleton/business/README.md +12 -0
  195. package/plugins/flow/templates/ai-skeleton/context/README.md +12 -0
  196. package/plugins/flow/templates/ai-skeleton/decisions/README.md +20 -0
  197. package/plugins/flow/templates/ai-skeleton/design-import/.gitkeep +0 -0
  198. package/plugins/flow/templates/ai-skeleton/dev-logs/.gitkeep +0 -0
  199. package/plugins/flow/templates/ai-skeleton/device/.gitkeep +0 -0
  200. package/plugins/flow/templates/ai-skeleton/docs/README.md +5 -0
  201. package/plugins/flow/templates/ai-skeleton/logs/.gitkeep +0 -0
  202. package/plugins/flow/templates/ai-skeleton/logs/README.md +13 -0
  203. package/plugins/flow/templates/ai-skeleton/plans/README.md +16 -0
  204. package/plugins/flow/templates/ai-skeleton/plans/archive/.gitkeep +0 -0
  205. package/plugins/flow/templates/ai-skeleton/release-guide.md +35 -0
  206. package/plugins/flow/templates/ai-skeleton/reviews/README.md +15 -0
  207. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +26 -0
  208. package/plugins/flow/templates/ai-skeleton/scenarios/_lib/.gitkeep +0 -0
  209. package/plugins/flow/templates/ai-skeleton/state/STATE.md +25 -0
  210. package/plugins/flow/templates/ai-skeleton/templates/HANDOFF.md +32 -0
  211. package/plugins/flow/templates/ai-skeleton/workflows.config.json +74 -0
@@ -0,0 +1,813 @@
1
+ /**
2
+ * @file canvas-shell.tsx — universal input-grammar wrapper for TSX canvases
3
+ * @scope plugins/design/dev-server/canvas-shell.tsx
4
+ * @purpose Mounted by `DesignCanvas` for every canvas. Stacks
5
+ * SelectionSetProvider + ContextMenuProvider, wires the input
6
+ * router to provider actions, and renders the floating chrome
7
+ * (ToolPalette, hover halo, selection halos, group bbox).
8
+ *
9
+ * Input grammar (V0.16+ universal — there's no opt-out flag):
10
+ *
11
+ * Move tool (V)
12
+ * bare hover / click → passes through (native interactions work)
13
+ * Cmd + hover → preview halo on deepest element under cursor
14
+ * Cmd + click → replace selection with deepest element
15
+ * Cmd + Shift + click → add deepest to selection (multi)
16
+ * right-click → context menu
17
+ *
18
+ * Hand tool (H) pan-on-drag, no Space required; no selection
19
+ * Comment tool (C) hover paints halo, click drops comment pin;
20
+ * native interactions on artboard children fully
21
+ * suppressed via capture-phase preventDefault
22
+ *
23
+ * keydown V / H / C / Esc → tool switch (Esc also clears selection + menu)
24
+ *
25
+ * Wheel / pinch / space-pan / Cmd+0/1/+/- stay with `useViewportController`
26
+ * (canvas-lib.tsx). The router consumes a strict non-overlapping subset.
27
+ */
28
+
29
+ import {
30
+ type ReactNode,
31
+ type RefObject,
32
+ useCallback,
33
+ useEffect,
34
+ useMemo,
35
+ useRef,
36
+ useState,
37
+ } from 'react';
38
+
39
+ import { AnnotationsLayer } from './annotations-layer.tsx';
40
+ import {
41
+ SnapGuideOverlay,
42
+ type ViewportControllerHandle,
43
+ useViewportControllerContext,
44
+ } from './canvas-lib.tsx';
45
+ import {
46
+ ContextMenuProvider,
47
+ type ContextRegistry,
48
+ type ContextTarget,
49
+ type ContextTargetKind,
50
+ type MenuItem,
51
+ useContextMenu,
52
+ } from './context-menu.tsx';
53
+ import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
54
+ import { ToolPalette } from './tool-palette.tsx';
55
+ import {
56
+ AnnotationSelectionProvider,
57
+ useAnnotationSelection,
58
+ } from './use-annotation-selection.tsx';
59
+ import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
60
+ import { type Selection, SelectionSetProvider, useSelectionSet } from './use-selection-set.tsx';
61
+ import { useToolMode } from './use-tool-mode.tsx';
62
+
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ // Styles — halos render as `position: fixed` siblings of the canvas. Reading
65
+ // element bounds via getBoundingClientRect (screen coords) keeps the 2 px
66
+ // border thickness consistent across zoom levels — CSS `zoom` on the world
67
+ // plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
68
+ // = invisible). No per-element class stamping is used.
69
+
70
+ const HALO_CSS = `
71
+ .dc-cv-halo {
72
+ position: fixed;
73
+ pointer-events: none;
74
+ z-index: 5;
75
+ border: 2px solid var(--accent, #d63b1f);
76
+ box-sizing: border-box;
77
+ border-radius: 2px;
78
+ transition: opacity 60ms linear;
79
+ }
80
+ .dc-cv-halo--hover {
81
+ border-width: 2px;
82
+ opacity: 0.85;
83
+ }
84
+ .dc-cv-halo--selected {
85
+ border-width: 2px;
86
+ box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent, #d63b1f) 18%, transparent);
87
+ }
88
+ .dc-cv-group-bbox {
89
+ position: fixed;
90
+ pointer-events: none;
91
+ z-index: 5;
92
+ border: 1px dashed var(--accent, #d63b1f);
93
+ border-radius: 2px;
94
+ opacity: 0.85;
95
+ }
96
+ /*
97
+ * Active-artboard indicator — the artboard whose center sits closest to the
98
+ * viewport midpoint after pan settles is "active" (DesignCanvas tracks this
99
+ * for keyboard jumps + the /design:edit context anchor). Phase 4 shipped a
100
+ * 2 px accent ring there; that's too loud next to selection halos. Keep it
101
+ * subtle: 1 px outline at low opacity. The drop-shadow stays untouched.
102
+ */
103
+ .dc-canvas .dc-artboard[aria-current="true"] {
104
+ box-shadow:
105
+ 6px 6px 0 var(--fg-0, #2a2520),
106
+ 0 0 0 1px color-mix(in oklab, var(--accent, #d63b1f) 40%, transparent);
107
+ }
108
+ /*
109
+ * Force tool cursor across the canvas tree in comment / hand modes. Without
110
+ * !important on every descendant, buttons and links with their own cursor
111
+ * declaration would flip the cursor away from the tool affordance the moment
112
+ * the user hovers an interactive element — wrong signal when native
113
+ * interactions are suppressed by the router anyway.
114
+ */
115
+ .dc-canvas[data-active-tool="comment"],
116
+ .dc-canvas[data-active-tool="comment"] *,
117
+ .dc-canvas[data-active-tool="pen"],
118
+ .dc-canvas[data-active-tool="pen"] *,
119
+ .dc-canvas[data-active-tool="rect"],
120
+ .dc-canvas[data-active-tool="rect"] *,
121
+ .dc-canvas[data-active-tool="ellipse"],
122
+ .dc-canvas[data-active-tool="ellipse"] *,
123
+ .dc-canvas[data-active-tool="arrow"],
124
+ .dc-canvas[data-active-tool="arrow"] * {
125
+ cursor: crosshair !important;
126
+ }
127
+ .dc-canvas[data-active-tool="hand"],
128
+ .dc-canvas[data-active-tool="hand"] * {
129
+ cursor: grab !important;
130
+ }
131
+ .dc-canvas[data-active-tool="eraser"],
132
+ .dc-canvas[data-active-tool="eraser"] * {
133
+ cursor: cell !important;
134
+ }
135
+ `.trim();
136
+
137
+ function ensureHaloStyles(): void {
138
+ if (typeof document === 'undefined') return;
139
+ if (document.getElementById('dc-cv-halo-css')) return;
140
+ const s = document.createElement('style');
141
+ s.id = 'dc-cv-halo-css';
142
+ s.textContent = HALO_CSS;
143
+ document.head.appendChild(s);
144
+ }
145
+
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+ // Shell
148
+
149
+ export function CanvasShell({
150
+ hostRef,
151
+ children,
152
+ }: {
153
+ hostRef: RefObject<HTMLDivElement | null>;
154
+ children: ReactNode;
155
+ }) {
156
+ ensureHaloStyles();
157
+ // ToolProvider is mounted by DesignCanvas one level up (so the viewport
158
+ // controller's `isPanDragActive` predicate can read the live tool state).
159
+ return (
160
+ <SelectionSetProvider>
161
+ <AnnotationSelectionProvider>
162
+ <AnnotationsVisibilityProvider>
163
+ <CanvasCore hostRef={hostRef}>{children}</CanvasCore>
164
+ </AnnotationsVisibilityProvider>
165
+ </AnnotationSelectionProvider>
166
+ </SelectionSetProvider>
167
+ );
168
+ }
169
+
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+ // CanvasCore — sits inside SelectionSetProvider, builds the menu registry
172
+ // against the live viewport controller + selection set, then mounts
173
+ // ContextMenuProvider + CanvasRouter.
174
+
175
+ function CanvasCore({
176
+ hostRef,
177
+ children,
178
+ }: {
179
+ hostRef: RefObject<HTMLDivElement | null>;
180
+ children: ReactNode;
181
+ }) {
182
+ const controller = useViewportControllerContext();
183
+ const selSet = useSelectionSet();
184
+ const { tool } = useToolMode();
185
+
186
+ // Project active tool to `.dc-canvas[data-active-tool]` so the cursor
187
+ // override CSS rules (HALO_CSS) can force the tool cursor across every
188
+ // descendant — buttons / links with their own cursor declaration get
189
+ // overridden when in comment / hand modes.
190
+ useEffect(() => {
191
+ const host = hostRef.current;
192
+ if (!host) return;
193
+ host.setAttribute('data-active-tool', tool);
194
+ return () => {
195
+ host.removeAttribute('data-active-tool');
196
+ };
197
+ }, [hostRef, tool]);
198
+
199
+ const registry = useMemo<ContextRegistry>(
200
+ () => buildRegistry({ controller, clearSelection: selSet.clear }),
201
+ [controller, selSet.clear]
202
+ );
203
+
204
+ return (
205
+ <ContextMenuProvider registry={registry}>
206
+ <CanvasRouter hostRef={hostRef}>{children}</CanvasRouter>
207
+ </ContextMenuProvider>
208
+ );
209
+ }
210
+
211
+ // ─────────────────────────────────────────────────────────────────────────────
212
+ // Registry builder — closes over controller + clear callback.
213
+
214
+ function buildRegistry(deps: {
215
+ controller: ViewportControllerHandle | null;
216
+ clearSelection: () => void;
217
+ }): ContextRegistry {
218
+ const { controller, clearSelection } = deps;
219
+
220
+ const copy = (text: string): void => {
221
+ if (typeof navigator === 'undefined' || !navigator.clipboard) return;
222
+ void navigator.clipboard.writeText(text).catch(() => {
223
+ /* clipboard blocked */
224
+ });
225
+ };
226
+
227
+ const postComposeForTarget = (target: ContextTarget): void => {
228
+ if (typeof window === 'undefined') return;
229
+ const sel: Selection | null = target.el
230
+ ? {
231
+ file: deriveFile(),
232
+ id: target.cdId ?? undefined,
233
+ selector: target.cdId ? `[data-cd-id="${target.cdId}"]` : cssPath(target.el),
234
+ artboardId: target.artboardId,
235
+ tag: target.el.tagName.toLowerCase(),
236
+ classes: realClasses(target.el),
237
+ text: shortText(target.el, 240),
238
+ dom_path: domPath(target.el),
239
+ bounds: (target.el as HTMLElement).getBoundingClientRect
240
+ ? boundsOf(target.el as HTMLElement)
241
+ : null,
242
+ html: (target.el.outerHTML ?? '').slice(0, 4000),
243
+ }
244
+ : null;
245
+ try {
246
+ window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
247
+ } catch {
248
+ /* ignore */
249
+ }
250
+ };
251
+
252
+ const fitItem: MenuItem = {
253
+ id: 'fit-view',
254
+ label: 'Fit to view',
255
+ shortcut: '1',
256
+ onSelect: () => controller?.fit(),
257
+ };
258
+ const resetItem: MenuItem = {
259
+ id: 'reset-view',
260
+ label: 'Reset view',
261
+ shortcut: '⌘0',
262
+ onSelect: () => controller?.reset(),
263
+ };
264
+
265
+ return {
266
+ element: [
267
+ [
268
+ {
269
+ id: 'add-comment',
270
+ label: 'Add comment',
271
+ shortcut: 'C',
272
+ onSelect: postComposeForTarget,
273
+ },
274
+ {
275
+ id: 'copy-css',
276
+ label: 'Copy CSS',
277
+ shortcut: '⌘⇧C',
278
+ onSelect: (target) => {
279
+ if (!target.el) return;
280
+ copy(cssPath(target.el));
281
+ },
282
+ },
283
+ {
284
+ id: 'copy-id',
285
+ label: 'Copy data-cd-id',
286
+ onSelect: (target) => {
287
+ if (target.cdId) copy(target.cdId);
288
+ },
289
+ },
290
+ {
291
+ id: 'inspect',
292
+ label: 'Inspect',
293
+ shortcut: '⌥I',
294
+ disabled: true,
295
+ onSelect: () => {
296
+ console.warn('[context-menu] TODO: tweaks panel for TSX canvases');
297
+ },
298
+ },
299
+ ],
300
+ [
301
+ {
302
+ id: 'hide',
303
+ label: 'Hide',
304
+ shortcut: '⌘⇧H',
305
+ onSelect: (target) => {
306
+ if (target.el) (target.el as HTMLElement).style.visibility = 'hidden';
307
+ },
308
+ },
309
+ {
310
+ id: 'deselect',
311
+ label: 'Deselect',
312
+ shortcut: 'Esc',
313
+ onSelect: () => clearSelection(),
314
+ },
315
+ ],
316
+ ],
317
+ 'artboard-chrome': [
318
+ [
319
+ {
320
+ id: 'fit-one',
321
+ label: 'Fit just this artboard',
322
+ onSelect: (target) => {
323
+ if (!controller || !target.artboardId) return;
324
+ // controller exposes jumpTo(rect) — DCArtboard.onFocus uses the
325
+ // same pattern. The artboard label button already wires to
326
+ // onFocus; dispatch a synthetic click as the simplest bridge.
327
+ const btn = (target.el ?? document)
328
+ .closest?.('[data-dc-screen]')
329
+ ?.querySelector('button.dc-artboard-label');
330
+ (btn as HTMLButtonElement | null)?.click();
331
+ },
332
+ },
333
+ fitItem,
334
+ resetItem,
335
+ ],
336
+ ],
337
+ world: [[fitItem, resetItem]],
338
+ overlay: [],
339
+ };
340
+ }
341
+
342
+ function boundsOf(el: HTMLElement) {
343
+ const r = el.getBoundingClientRect();
344
+ return {
345
+ x: Math.round(r.left),
346
+ y: Math.round(r.top),
347
+ w: Math.round(r.width),
348
+ h: Math.round(r.height),
349
+ };
350
+ }
351
+
352
+ // ─────────────────────────────────────────────────────────────────────────────
353
+ // Router wire-up
354
+
355
+ function CanvasRouter({
356
+ hostRef,
357
+ children,
358
+ }: {
359
+ hostRef: RefObject<HTMLDivElement | null>;
360
+ children: ReactNode;
361
+ }) {
362
+ const { tool, setTool } = useToolMode();
363
+ const selSet = useSelectionSet();
364
+ const annotSel = useAnnotationSelection();
365
+ const ctxMenu = useContextMenu();
366
+
367
+ // Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
368
+ // itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
369
+ const [hoverEl, setHoverEl] = useState<Element | null>(null);
370
+
371
+ // rAF-coalesced hover dispatcher. `pointermove` fires hundreds of times/sec
372
+ // under trackpad input — collapse to one elementFromPoint per frame.
373
+ const pendingHoverRef = useRef<{ deep: boolean; x: number; y: number } | null>(null);
374
+ const hoverRafRef = useRef<number | null>(null);
375
+
376
+ const getActiveTool = useCallback(() => tool, [tool]);
377
+
378
+ const applyHover = useCallback(() => {
379
+ hoverRafRef.current = null;
380
+ const pending = pendingHoverRef.current;
381
+ pendingHoverRef.current = null;
382
+ if (!pending) return;
383
+ const target = resolveHoverTarget(document, pending.x, pending.y, {
384
+ deep: pending.deep,
385
+ });
386
+ const nextEl = target?.el ?? null;
387
+ setHoverEl((prev) => (prev === nextEl ? prev : nextEl));
388
+ }, []);
389
+
390
+ // Clear hover when switching to hand mode mid-stream.
391
+ useEffect(() => {
392
+ if (tool === 'hand') setHoverEl(null);
393
+ }, [tool]);
394
+
395
+ // Listen for `dgn: 'force-clear'` from the shell — the comment composer
396
+ // posts it on submit / cancel / Esc so the selection halo clears when the
397
+ // user closes the composer.
398
+ useEffect(() => {
399
+ if (typeof window === 'undefined') return;
400
+ const onMessage = (e: MessageEvent) => {
401
+ const m = e.data as { dgn?: string } | null;
402
+ if (!m || typeof m !== 'object' || !m.dgn) return;
403
+ if (m.dgn === 'force-clear' || m.dgn === 'select-clear' || m.dgn === 'selection-clear') {
404
+ selSet.clear();
405
+ annotSel.clear();
406
+ setHoverEl(null);
407
+ return;
408
+ }
409
+ if (m.dgn === 'tool-set') {
410
+ const t = (m as { tool?: string }).tool;
411
+ if (typeof t === 'string') setTool(t as never);
412
+ return;
413
+ }
414
+ };
415
+ window.addEventListener('message', onMessage);
416
+ return () => window.removeEventListener('message', onMessage);
417
+ }, [selSet, annotSel, setTool]);
418
+
419
+ // Cleanup any pending rAF on unmount.
420
+ useEffect(
421
+ () => () => {
422
+ if (hoverRafRef.current != null && typeof cancelAnimationFrame !== 'undefined') {
423
+ cancelAnimationFrame(hoverRafRef.current);
424
+ }
425
+ },
426
+ []
427
+ );
428
+
429
+ useInputRouter({
430
+ hostRef,
431
+ getActiveTool,
432
+ callbacks: {
433
+ onHover: ({ deep, clientX, clientY }) => {
434
+ pendingHoverRef.current = { deep, x: clientX, y: clientY };
435
+ if (hoverRafRef.current == null && typeof requestAnimationFrame !== 'undefined') {
436
+ hoverRafRef.current = requestAnimationFrame(applyHover);
437
+ }
438
+ },
439
+ onSelect: ({ mode, deep, clientX, clientY }) => {
440
+ const target = resolveHoverTarget(document, clientX, clientY, { deep });
441
+ if (!target) {
442
+ // Cmd-click on dead space (canvas chrome, label strip, empty world):
443
+ // clear for `replace`, leave the set alone for `add`.
444
+ if (mode === 'replace') selSet.clear();
445
+ return;
446
+ }
447
+ const sel = hoverTargetToSelection(target);
448
+ if (mode === 'replace') selSet.replace(sel);
449
+ else selSet.add(sel);
450
+ },
451
+ onContextMenu: ({ clientX, clientY }) => {
452
+ const target = resolveHoverTarget(document, clientX, clientY, { deep: true });
453
+ const kind = classifyContextKind(target);
454
+ const ctxTarget: ContextTarget = {
455
+ kind,
456
+ el: target?.el ?? null,
457
+ cdId: target?.cdId ?? null,
458
+ artboardId: target?.artboardId ?? null,
459
+ clientX,
460
+ clientY,
461
+ };
462
+ ctxMenu.open(ctxTarget);
463
+ },
464
+ onTool: ({ tool: t }) => setTool(t),
465
+ onEscape: () => {
466
+ ctxMenu.close();
467
+ selSet.clear();
468
+ annotSel.clear();
469
+ setHoverEl(null);
470
+ },
471
+ onDropComment: ({ clientX, clientY }) => {
472
+ const target = resolveHoverTarget(document, clientX, clientY, { deep: true });
473
+ if (!target) return;
474
+ const sel = hoverTargetToSelection(target);
475
+ // Commit the target to the selection set so the halo persists while
476
+ // the shell-side composer is open. The user clears by:
477
+ // - submit / cancel on the composer (shell posts `force-clear`)
478
+ // - pressing Esc inside the canvas (router's onEscape → clear)
479
+ // - clicking another element in comment mode (this handler runs
480
+ // again and replaces)
481
+ selSet.replace(sel);
482
+ if (typeof window === 'undefined') return;
483
+ try {
484
+ window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
485
+ } catch {
486
+ /* parent detached */
487
+ }
488
+ },
489
+ },
490
+ });
491
+
492
+ return (
493
+ <>
494
+ {children}
495
+ <AnnotationsLayer />
496
+ <ToolPalette />
497
+ <HoverHalo el={hoverEl} />
498
+ <SelectionHalos />
499
+ <GroupBbox />
500
+ <SnapGuideOverlay />
501
+ </>
502
+ );
503
+ }
504
+
505
+ // ─────────────────────────────────────────────────────────────────────────────
506
+ // HoverHalo — single floating overlay tracking the hovered element's screen
507
+ // bounds. Updates on every animation frame while mounted. Position: fixed so
508
+ // CSS `zoom` on the world plane never affects the 2 px border thickness.
509
+
510
+ function HoverHalo({ el }: { el: Element | null }) {
511
+ const ref = useRef<HTMLDivElement | null>(null);
512
+ const rafRef = useRef<number | null>(null);
513
+ const targetRef = useRef<Element | null>(el);
514
+ targetRef.current = el;
515
+
516
+ useEffect(() => {
517
+ if (!el) {
518
+ if (ref.current) ref.current.style.display = 'none';
519
+ return;
520
+ }
521
+ const tick = () => {
522
+ rafRef.current = null;
523
+ const div = ref.current;
524
+ const t = targetRef.current;
525
+ if (!div || !t || !t.isConnected) {
526
+ if (div) div.style.display = 'none';
527
+ return;
528
+ }
529
+ const r = (t as HTMLElement).getBoundingClientRect();
530
+ if (r.width === 0 && r.height === 0) {
531
+ div.style.display = 'none';
532
+ } else {
533
+ div.style.display = 'block';
534
+ div.style.left = `${Math.round(r.left)}px`;
535
+ div.style.top = `${Math.round(r.top)}px`;
536
+ div.style.width = `${Math.round(r.width)}px`;
537
+ div.style.height = `${Math.round(r.height)}px`;
538
+ }
539
+ rafRef.current = requestAnimationFrame(tick);
540
+ };
541
+ rafRef.current = requestAnimationFrame(tick);
542
+ return () => {
543
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
544
+ };
545
+ }, [el]);
546
+
547
+ if (!el) return null;
548
+ return <div ref={ref} className="dc-cv-halo dc-cv-halo--hover" aria-hidden="true" />;
549
+ }
550
+
551
+ // ─────────────────────────────────────────────────────────────────────────────
552
+ // SelectionHalos — N floating overlays, one per selected element. Resolves
553
+ // elements by `data-cd-id` when present, falling back to the selector path.
554
+
555
+ function SelectionHalos() {
556
+ const { selected } = useSelectionSet();
557
+ const containerRef = useRef<HTMLDivElement | null>(null);
558
+ const rafRef = useRef<number | null>(null);
559
+
560
+ useEffect(() => {
561
+ if (selected.length === 0) {
562
+ const c = containerRef.current;
563
+ if (c) c.innerHTML = '';
564
+ return;
565
+ }
566
+ const tick = () => {
567
+ rafRef.current = null;
568
+ const c = containerRef.current;
569
+ if (!c) return;
570
+ // Match the rendered halo count to selected.length; reuse DOM nodes.
571
+ while (c.children.length < selected.length) {
572
+ const child = document.createElement('div');
573
+ child.className = 'dc-cv-halo dc-cv-halo--selected';
574
+ child.setAttribute('aria-hidden', 'true');
575
+ c.appendChild(child);
576
+ }
577
+ while (c.children.length > selected.length) {
578
+ c.removeChild(c.lastChild as Node);
579
+ }
580
+ for (let i = 0; i < selected.length; i++) {
581
+ const sel = selected[i];
582
+ const child = c.children[i] as HTMLDivElement;
583
+ const el = sel?.id
584
+ ? document.querySelector(`[data-cd-id="${cssEscape(sel.id)}"]`)
585
+ : sel
586
+ ? safeQuery(sel.selector)
587
+ : null;
588
+ if (!el) {
589
+ child.style.display = 'none';
590
+ continue;
591
+ }
592
+ const r = (el as HTMLElement).getBoundingClientRect();
593
+ if (r.width === 0 && r.height === 0) {
594
+ child.style.display = 'none';
595
+ } else {
596
+ child.style.display = 'block';
597
+ child.style.left = `${Math.round(r.left)}px`;
598
+ child.style.top = `${Math.round(r.top)}px`;
599
+ child.style.width = `${Math.round(r.width)}px`;
600
+ child.style.height = `${Math.round(r.height)}px`;
601
+ }
602
+ }
603
+ rafRef.current = requestAnimationFrame(tick);
604
+ };
605
+ rafRef.current = requestAnimationFrame(tick);
606
+ return () => {
607
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
608
+ };
609
+ }, [selected]);
610
+
611
+ return <div ref={containerRef} aria-hidden="true" />;
612
+ }
613
+
614
+ // ─────────────────────────────────────────────────────────────────────────────
615
+ // GroupBbox — dashed outline around the union of selected elements when N > 1.
616
+
617
+ function GroupBbox() {
618
+ const { selected } = useSelectionSet();
619
+ const ref = useRef<HTMLDivElement | null>(null);
620
+ const rafRef = useRef<number | null>(null);
621
+
622
+ useEffect(() => {
623
+ if (selected.length < 2) {
624
+ if (ref.current) ref.current.style.display = 'none';
625
+ return;
626
+ }
627
+ const tick = () => {
628
+ rafRef.current = null;
629
+ const div = ref.current;
630
+ if (!div) return;
631
+ let xMin = Number.POSITIVE_INFINITY;
632
+ let yMin = Number.POSITIVE_INFINITY;
633
+ let xMax = Number.NEGATIVE_INFINITY;
634
+ let yMax = Number.NEGATIVE_INFINITY;
635
+ let anyHit = false;
636
+ for (const sel of selected) {
637
+ const el = sel.id
638
+ ? document.querySelector(`[data-cd-id="${cssEscape(sel.id)}"]`)
639
+ : safeQuery(sel.selector);
640
+ if (!el) continue;
641
+ const r = (el as HTMLElement).getBoundingClientRect();
642
+ if (r.width === 0 && r.height === 0) continue;
643
+ anyHit = true;
644
+ if (r.left < xMin) xMin = r.left;
645
+ if (r.top < yMin) yMin = r.top;
646
+ if (r.right > xMax) xMax = r.right;
647
+ if (r.bottom > yMax) yMax = r.bottom;
648
+ }
649
+ if (!anyHit) {
650
+ div.style.display = 'none';
651
+ return;
652
+ }
653
+ const pad = 4;
654
+ div.style.display = 'block';
655
+ div.style.left = `${Math.round(xMin - pad)}px`;
656
+ div.style.top = `${Math.round(yMin - pad)}px`;
657
+ div.style.width = `${Math.round(xMax - xMin + pad * 2)}px`;
658
+ div.style.height = `${Math.round(yMax - yMin + pad * 2)}px`;
659
+ rafRef.current = requestAnimationFrame(tick);
660
+ };
661
+ rafRef.current = requestAnimationFrame(tick);
662
+ return () => {
663
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
664
+ };
665
+ }, [selected]);
666
+
667
+ if (selected.length < 2) return null;
668
+ return <div ref={ref} className="dc-cv-group-bbox" aria-hidden="true" />;
669
+ }
670
+
671
+ // ─────────────────────────────────────────────────────────────────────────────
672
+ // Helpers
673
+
674
+ function classifyContextKind(target: HoverTarget | null): ContextTargetKind {
675
+ if (!target) return 'world';
676
+ const el = target.el;
677
+ if (!el) return 'world';
678
+ if (el.closest?.('.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu')) {
679
+ return 'overlay';
680
+ }
681
+ if (target.cdId) return 'element';
682
+ if (target.artboardId) return 'artboard-chrome';
683
+ return 'world';
684
+ }
685
+
686
+ function hoverTargetToSelection(target: HoverTarget): Selection {
687
+ const el = target.el;
688
+ const rect =
689
+ el && (el as HTMLElement).getBoundingClientRect
690
+ ? (el as HTMLElement).getBoundingClientRect()
691
+ : null;
692
+ // `cdId` is the hit element's OWN data-cd-id (deep mode); resolver never
693
+ // climbs to an ancestor. Falls back to cssPath of the hit when no stable
694
+ // anchor exists.
695
+ const cdId = target.cdId;
696
+ return {
697
+ file: typeof window !== 'undefined' ? deriveFile() : undefined,
698
+ id: cdId ?? undefined,
699
+ selector: cdId ? `[data-cd-id="${cdId}"]` : cssPath(el),
700
+ artboardId: target.artboardId,
701
+ tag: el?.tagName.toLowerCase() ?? '',
702
+ classes: realClasses(el),
703
+ text: shortText(el, 240),
704
+ dom_path: domPath(el),
705
+ bounds: rect
706
+ ? {
707
+ x: Math.round(rect.left),
708
+ y: Math.round(rect.top),
709
+ w: Math.round(rect.width),
710
+ h: Math.round(rect.height),
711
+ }
712
+ : null,
713
+ html: el ? (el.outerHTML ?? '').slice(0, 4000) : '',
714
+ };
715
+ }
716
+
717
+ function deriveFile(): string | undefined {
718
+ try {
719
+ const p = window.location.pathname;
720
+ if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
721
+ const qs = new URLSearchParams(window.location.search);
722
+ const canvas = qs.get('canvas') ?? '';
723
+ const designRel = (qs.get('designRel') ?? '.design').replace(/^\/+|\/+$/g, '');
724
+ return `${designRel}/${canvas}`;
725
+ }
726
+ return decodeURIComponent(p).replace(/^\//, '');
727
+ } catch {
728
+ return undefined;
729
+ }
730
+ }
731
+
732
+ function realClasses(el: Element | null): string {
733
+ if (!el) return '';
734
+ return (el.getAttribute('class') ?? '')
735
+ .trim()
736
+ .split(/\s+/)
737
+ .filter((c) => c && !c.startsWith('dgn-') && !c.startsWith('dc-cv-'))
738
+ .join(' ');
739
+ }
740
+
741
+ function shortText(el: Element | null, max: number): string {
742
+ if (!el) return '';
743
+ const t = ((el as HTMLElement).innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
744
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
745
+ }
746
+
747
+ function cssPath(el: Element | null): string {
748
+ if (!el) return '';
749
+ const path: string[] = [];
750
+ let cur: Element | null = el;
751
+ while (cur && cur.nodeType === 1 && path.length < 8) {
752
+ const dscEl = cur.getAttribute?.('data-dc-element');
753
+ if (dscEl) {
754
+ path.unshift(`[data-dc-element="${dscEl}"]`);
755
+ break;
756
+ }
757
+ const dscSc = cur.getAttribute?.('data-dc-screen');
758
+ if (dscSc) {
759
+ path.unshift(`[data-dc-screen="${dscSc}"]`);
760
+ break;
761
+ }
762
+ let sel = cur.nodeName.toLowerCase();
763
+ if (cur.id) {
764
+ sel = `#${cur.id}`;
765
+ path.unshift(sel);
766
+ break;
767
+ }
768
+ const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
769
+ if (cls.length) sel += `.${cls.join('.')}`;
770
+ let sib = 1;
771
+ let n: Element | null = cur.previousElementSibling;
772
+ while (n) {
773
+ sib++;
774
+ n = n.previousElementSibling;
775
+ }
776
+ sel += `:nth-child(${sib})`;
777
+ path.unshift(sel);
778
+ cur = cur.parentElement;
779
+ }
780
+ return path.join(' > ');
781
+ }
782
+
783
+ function domPath(el: Element | null): string[] {
784
+ const hops: string[] = [];
785
+ let cur = el;
786
+ while (cur && cur.nodeType === 1 && hops.length < 8) {
787
+ let label = cur.nodeName.toLowerCase();
788
+ const dEl = cur.getAttribute?.('data-dc-element');
789
+ const dSc = cur.getAttribute?.('data-dc-screen');
790
+ if (dEl) label += `[data-dc-element="${dEl}"]`;
791
+ else if (dSc) label += `[data-dc-screen="${dSc}"]`;
792
+ else if (cur.id) label += `#${cur.id}`;
793
+ const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
794
+ if (cls.length && !dEl && !dSc) label += `.${cls.join('.')}`;
795
+ hops.unshift(label);
796
+ cur = cur.parentElement;
797
+ }
798
+ return hops;
799
+ }
800
+
801
+ function cssEscape(s: string): string {
802
+ // Minimal CSS.escape polyfill — only handles chars actually present in
803
+ // pipeline-stamped IDs (alphanumerics + `-` + `_`).
804
+ return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
805
+ }
806
+
807
+ function safeQuery(selector: string): Element | null {
808
+ try {
809
+ return document.querySelector(selector);
810
+ } catch {
811
+ return null;
812
+ }
813
+ }