@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,485 @@
1
+ /**
2
+ * @file input-router.tsx — canvas pointer/keyboard classifier + hook
3
+ * @scope plugins/design/dev-server/input-router.tsx
4
+ * @purpose Owned by canvas-lib's DesignCanvas. Classifies the NON-WHEEL
5
+ * subset of pointer + key events into discrete router actions.
6
+ * `useViewportController` keeps owning wheel + middle-mouse +
7
+ * space-pan + Cmd+0/1/+/- — the two stacks coexist without a
8
+ * listener race (DDR-026).
9
+ *
10
+ * Event ownership (read this before adding handlers):
11
+ *
12
+ * ┌──────────────────────────────────┬─────────────────────────┐
13
+ * │ Event │ Owner │
14
+ * ├──────────────────────────────────┼─────────────────────────┤
15
+ * │ wheel / shift-wheel / cmd-wheel │ useViewportController │
16
+ * │ pointerdown btn=1 / space-held │ useViewportController │
17
+ * │ keydown Space / Cmd+0/1/+/- │ useViewportController │
18
+ * │ pointermove (hover) │ input-router │
19
+ * │ pointerdown btn=0 (select) │ input-router │
20
+ * │ pointerdown btn=2 (right-click) │ input-router │
21
+ * │ keydown V / H / C / Esc │ input-router │
22
+ * └──────────────────────────────────┴─────────────────────────┘
23
+ *
24
+ * The router does no DOM work itself — `classify()` is pure (testable without
25
+ * a DOM) and `useInputRouter()` attaches listeners that dispatch through the
26
+ * caller-supplied callbacks. Hover-target resolution + selection persistence
27
+ * live in the consumer (DesignCanvas).
28
+ */
29
+
30
+ import { type RefObject, useEffect } from 'react';
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Types
34
+
35
+ /**
36
+ * Tool union. Phase 4.1 shipped V/H/C; Phase 5 adds the draw set
37
+ * (pen / rect / arrow / eraser). Draw-tool pointer events are owned by
38
+ * `AnnotationsLayer` — the router classifies their letter shortcuts but
39
+ * returns `no-op` for the corresponding pointer events so the SVG overlay
40
+ * can grab them natively.
41
+ */
42
+ export type Tool = 'move' | 'hand' | 'comment' | 'pen' | 'rect' | 'ellipse' | 'arrow' | 'eraser';
43
+
44
+ const ANNOTATION_TOOLS = new Set<Tool>(['pen', 'rect', 'ellipse', 'arrow', 'eraser']);
45
+
46
+ export function isAnnotationTool(t: Tool): boolean {
47
+ return ANNOTATION_TOOLS.has(t);
48
+ }
49
+
50
+ export type RouterAction =
51
+ | { kind: 'no-op' }
52
+ | { kind: 'hover'; deep: boolean; clientX: number; clientY: number }
53
+ | {
54
+ kind: 'select';
55
+ /** `replace` swaps the selection set, `add` merges into it. */
56
+ mode: 'replace' | 'add';
57
+ /**
58
+ * `true` resolves to the deepest descendant under the cursor (Cmd-held
59
+ * mode). `false` resolves to the topmost interesting ancestor (top mode).
60
+ * Phase 4.1 Move-tool selection always uses deep=true — bare clicks
61
+ * are passthrough (no select), and the only entry points are Cmd
62
+ * (replace deep) and Cmd+Shift (add deep).
63
+ */
64
+ deep: boolean;
65
+ clientX: number;
66
+ clientY: number;
67
+ }
68
+ | { kind: 'drop-comment'; clientX: number; clientY: number }
69
+ | { kind: 'context-menu'; clientX: number; clientY: number }
70
+ | { kind: 'tool'; tool: Tool }
71
+ | { kind: 'escape' };
72
+
73
+ export interface ClassifyInput {
74
+ type: 'pointermove' | 'pointerdown' | 'contextmenu' | 'keydown';
75
+ /** PointerEvent.button: 0 = left, 1 = middle, 2 = right. */
76
+ button?: number;
77
+ metaKey?: boolean;
78
+ ctrlKey?: boolean;
79
+ shiftKey?: boolean;
80
+ altKey?: boolean;
81
+ key?: string;
82
+ clientX?: number;
83
+ clientY?: number;
84
+ /** Spacebar held — shared signal with `useViewportController`'s pan-drag. */
85
+ spaceHeld?: boolean;
86
+ /** Event target is editable (input/textarea/contentEditable) — caller computes. */
87
+ isEditable?: boolean;
88
+ activeTool: Tool;
89
+ }
90
+
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ // classify — pure function. All branching lives here so unit tests cover every
93
+ // row of the dispatch table without spinning up a DOM.
94
+
95
+ const metaOrCtrl = (i: ClassifyInput): boolean => !!(i.metaKey || i.ctrlKey);
96
+
97
+ export function classify(input: ClassifyInput): RouterAction {
98
+ if (input.type === 'keydown') {
99
+ if (input.isEditable) return { kind: 'no-op' };
100
+ // Tool letters are bare keys — Cmd/Ctrl/Alt+letter belongs to shell / browser.
101
+ if (input.metaKey || input.ctrlKey || input.altKey) {
102
+ // Esc with modifiers still dismisses.
103
+ if (input.key === 'Escape') return { kind: 'escape' };
104
+ return { kind: 'no-op' };
105
+ }
106
+ const k = (input.key || '').toLowerCase();
107
+ if (k === 'v') return { kind: 'tool', tool: 'move' };
108
+ if (k === 'h') return { kind: 'tool', tool: 'hand' };
109
+ if (k === 'c') return { kind: 'tool', tool: 'comment' };
110
+ if (k === 'b') return { kind: 'tool', tool: 'pen' };
111
+ if (k === 'r') return { kind: 'tool', tool: 'rect' };
112
+ if (k === 'o') return { kind: 'tool', tool: 'ellipse' };
113
+ if (k === 'a') return { kind: 'tool', tool: 'arrow' };
114
+ if (k === 'e') return { kind: 'tool', tool: 'eraser' };
115
+ if (input.key === 'Escape') return { kind: 'escape' };
116
+ return { kind: 'no-op' };
117
+ }
118
+
119
+ if (input.type === 'contextmenu') {
120
+ return {
121
+ kind: 'context-menu',
122
+ clientX: input.clientX ?? 0,
123
+ clientY: input.clientY ?? 0,
124
+ };
125
+ }
126
+
127
+ if (input.type === 'pointermove') {
128
+ // Phase 5 draw tools: pen / rect / arrow / eraser own all their pointer
129
+ // events through `AnnotationsLayer`. The router never paints a hover halo
130
+ // while drawing — that affordance is reserved for select / comment.
131
+ if (isAnnotationTool(input.activeTool)) return { kind: 'no-op' };
132
+ // Hand tool: drag pan is owned by useViewportController; no hover paint.
133
+ if (input.activeTool === 'hand') return { kind: 'no-op' };
134
+ // Comment tool: always paint a preview halo on the deepest element under
135
+ // cursor — that's the element the user is about to comment on. Comment
136
+ // pin attachment is to the same element they were hovering.
137
+ if (input.activeTool === 'comment') {
138
+ return {
139
+ kind: 'hover',
140
+ deep: true,
141
+ clientX: input.clientX ?? 0,
142
+ clientY: input.clientY ?? 0,
143
+ };
144
+ }
145
+ // Move tool: bare hover does nothing (native interactions pass through);
146
+ // Cmd-held hover paints a halo on the deepest element (preview).
147
+ if (!metaOrCtrl(input)) return { kind: 'no-op' };
148
+ return {
149
+ kind: 'hover',
150
+ deep: true,
151
+ clientX: input.clientX ?? 0,
152
+ clientY: input.clientY ?? 0,
153
+ };
154
+ }
155
+
156
+ if (input.type === 'pointerdown') {
157
+ if (input.button === 2) {
158
+ return {
159
+ kind: 'context-menu',
160
+ clientX: input.clientX ?? 0,
161
+ clientY: input.clientY ?? 0,
162
+ };
163
+ }
164
+ if (input.button === 1 || input.spaceHeld) return { kind: 'no-op' };
165
+ if (input.button !== 0) return { kind: 'no-op' };
166
+
167
+ // Phase 5 draw tools own bare left-clicks; the router returns no-op so
168
+ // the SVG layer's own listeners (no preventDefault) fire normally. Cmd-
169
+ // modified clicks still flow into the move-tool select path below — that
170
+ // stays available as an escape hatch even while a draw tool is active.
171
+ if (isAnnotationTool(input.activeTool) && !metaOrCtrl(input)) {
172
+ return { kind: 'no-op' };
173
+ }
174
+
175
+ if (input.activeTool === 'comment') {
176
+ // Comment tool: bare click drops a pin. Cmd / Shift modifiers reserved
177
+ // for future "scope comment to deepest" variants — for now they fall
178
+ // through to the same drop.
179
+ return {
180
+ kind: 'drop-comment',
181
+ clientX: input.clientX ?? 0,
182
+ clientY: input.clientY ?? 0,
183
+ };
184
+ }
185
+
186
+ // Hand tool: pan is owned by useViewportController via `isPanDragActive`.
187
+ // Router returns no-op so it doesn't preventDefault or stopPropagation —
188
+ // the controller's pointerdown listener on the same host claims the drag.
189
+ if (input.activeTool === 'hand') return { kind: 'no-op' };
190
+
191
+ // Move tool. Selection ONLY fires with Cmd / Cmd+Shift. Bare clicks and
192
+ // Shift-without-Cmd pass through so native canvas interactions (button
193
+ // presses, link clicks, input focus) still work — exactly the same as
194
+ // pre-Phase-4.1 behavior for everything except Cmd-modified gestures.
195
+ const cmd = metaOrCtrl(input);
196
+ if (!cmd) return { kind: 'no-op' };
197
+ const shift = !!input.shiftKey;
198
+ return {
199
+ kind: 'select',
200
+ mode: shift ? 'add' : 'replace',
201
+ deep: true,
202
+ clientX: input.clientX ?? 0,
203
+ clientY: input.clientY ?? 0,
204
+ };
205
+ }
206
+
207
+ return { kind: 'no-op' };
208
+ }
209
+
210
+ // ─────────────────────────────────────────────────────────────────────────────
211
+ // useInputRouter — attach listeners scoped to `hostRef.current`. Dispatches
212
+ // through `callbacks`. Returns nothing; cleans up on unmount.
213
+
214
+ export interface RouterCallbacks {
215
+ onHover?: (a: Extract<RouterAction, { kind: 'hover' }>) => void;
216
+ onSelect?: (a: Extract<RouterAction, { kind: 'select' }>) => void;
217
+ onDropComment?: (a: Extract<RouterAction, { kind: 'drop-comment' }>) => void;
218
+ onContextMenu?: (a: Extract<RouterAction, { kind: 'context-menu' }>) => void;
219
+ onTool?: (a: Extract<RouterAction, { kind: 'tool' }>) => void;
220
+ onEscape?: () => void;
221
+ }
222
+
223
+ export interface UseInputRouterOptions {
224
+ hostRef: RefObject<HTMLElement | null>;
225
+ /** Latest active tool — read at event time, not captured. */
226
+ getActiveTool: () => Tool;
227
+ /** Optional spacebar-held signal shared with useViewportController. */
228
+ isSpaceHeld?: () => boolean;
229
+ callbacks: RouterCallbacks;
230
+ /** When false, listeners are not attached. Defaults to true. */
231
+ enabled?: boolean;
232
+ }
233
+
234
+ export function isEditableTarget(t: EventTarget | null): boolean {
235
+ if (!t || !(t as HTMLElement).tagName) return false;
236
+ const el = t as HTMLElement;
237
+ const tag = el.tagName;
238
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
239
+ if (el.isContentEditable) return true;
240
+ return false;
241
+ }
242
+
243
+ export function useInputRouter(opts: UseInputRouterOptions): void {
244
+ const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true } = opts;
245
+
246
+ useEffect(() => {
247
+ if (!enabled) return;
248
+ const host = hostRef.current;
249
+ if (!host) return;
250
+
251
+ const dispatch = (action: RouterAction): void => {
252
+ switch (action.kind) {
253
+ case 'hover':
254
+ callbacks.onHover?.(action);
255
+ break;
256
+ case 'select':
257
+ callbacks.onSelect?.(action);
258
+ break;
259
+ case 'drop-comment':
260
+ callbacks.onDropComment?.(action);
261
+ break;
262
+ case 'context-menu':
263
+ callbacks.onContextMenu?.(action);
264
+ break;
265
+ case 'tool':
266
+ callbacks.onTool?.(action);
267
+ break;
268
+ case 'escape':
269
+ callbacks.onEscape?.();
270
+ break;
271
+ case 'no-op':
272
+ break;
273
+ }
274
+ };
275
+
276
+ const onPointerMove = (e: PointerEvent): void => {
277
+ const action = classify({
278
+ type: 'pointermove',
279
+ button: e.button,
280
+ metaKey: e.metaKey,
281
+ ctrlKey: e.ctrlKey,
282
+ shiftKey: e.shiftKey,
283
+ altKey: e.altKey,
284
+ clientX: e.clientX,
285
+ clientY: e.clientY,
286
+ spaceHeld: isSpaceHeld?.() ?? false,
287
+ activeTool: getActiveTool(),
288
+ });
289
+ dispatch(action);
290
+ };
291
+
292
+ const onPointerDown = (e: PointerEvent): void => {
293
+ const action = classify({
294
+ type: 'pointerdown',
295
+ button: e.button,
296
+ metaKey: e.metaKey,
297
+ ctrlKey: e.ctrlKey,
298
+ shiftKey: e.shiftKey,
299
+ altKey: e.altKey,
300
+ clientX: e.clientX,
301
+ clientY: e.clientY,
302
+ spaceHeld: isSpaceHeld?.() ?? false,
303
+ activeTool: getActiveTool(),
304
+ });
305
+ if (action.kind !== 'no-op') {
306
+ // Suppress native behavior on every event the router claims —
307
+ // button presses don't fire, inputs don't focus, the canvas
308
+ // content's own click handlers don't run. The router lives in
309
+ // capture phase so this fires before descendants.
310
+ e.preventDefault();
311
+ e.stopImmediatePropagation();
312
+ }
313
+ dispatch(action);
314
+ };
315
+
316
+ /**
317
+ * Paired mousedown listener — preventDefault on pointerdown does NOT
318
+ * suppress the mousedown event that browsers fire alongside, and
319
+ * `<input>` / `<button>` focus is driven by mousedown's default behavior.
320
+ * We mirror the same gate as pointerdown so suppressed pointerdowns also
321
+ * stop their twin mousedown.
322
+ */
323
+ const onMouseDown = (e: MouseEvent): void => {
324
+ const action = classify({
325
+ type: 'pointerdown',
326
+ button: e.button,
327
+ metaKey: e.metaKey,
328
+ ctrlKey: e.ctrlKey,
329
+ shiftKey: e.shiftKey,
330
+ altKey: e.altKey,
331
+ clientX: e.clientX,
332
+ clientY: e.clientY,
333
+ spaceHeld: isSpaceHeld?.() ?? false,
334
+ activeTool: getActiveTool(),
335
+ });
336
+ if (action.kind !== 'no-op') {
337
+ e.preventDefault();
338
+ e.stopImmediatePropagation();
339
+ }
340
+ };
341
+
342
+ /**
343
+ * Click listener — fires AFTER pointerdown+pointerup. Even with
344
+ * preventDefault on mousedown, the click event still synthesizes for
345
+ * non-form elements. We suppress it whenever the router claimed the
346
+ * matching pointerdown (re-classify with the same modifiers).
347
+ */
348
+ const onClick = (e: MouseEvent): void => {
349
+ const tool = getActiveTool();
350
+ const mod = e.metaKey || e.ctrlKey;
351
+ const wouldRoute =
352
+ tool === 'comment' || (tool === 'move' && mod && e.button === 0) || e.button === 2;
353
+ if (wouldRoute) {
354
+ e.preventDefault();
355
+ e.stopImmediatePropagation();
356
+ }
357
+ };
358
+
359
+ const onContextMenu = (e: MouseEvent): void => {
360
+ e.preventDefault();
361
+ e.stopImmediatePropagation();
362
+ const action = classify({
363
+ type: 'contextmenu',
364
+ clientX: e.clientX,
365
+ clientY: e.clientY,
366
+ metaKey: e.metaKey,
367
+ ctrlKey: e.ctrlKey,
368
+ shiftKey: e.shiftKey,
369
+ altKey: e.altKey,
370
+ activeTool: getActiveTool(),
371
+ });
372
+ dispatch(action);
373
+ };
374
+
375
+ const onKeyDown = (e: KeyboardEvent): void => {
376
+ const action = classify({
377
+ type: 'keydown',
378
+ key: e.key,
379
+ metaKey: e.metaKey,
380
+ ctrlKey: e.ctrlKey,
381
+ shiftKey: e.shiftKey,
382
+ altKey: e.altKey,
383
+ isEditable: isEditableTarget(e.target),
384
+ activeTool: getActiveTool(),
385
+ });
386
+ if (action.kind === 'tool' || action.kind === 'escape') {
387
+ e.preventDefault();
388
+ }
389
+ dispatch(action);
390
+ };
391
+
392
+ // Capture phase for pointer/mouse/click events — router runs BEFORE
393
+ // descendants (buttons, inputs, canvas content listeners). For events the
394
+ // classifier claims, we preventDefault + stopImmediatePropagation so the
395
+ // descendants never see them.
396
+ host.addEventListener('pointermove', onPointerMove, { passive: true });
397
+ host.addEventListener('pointerdown', onPointerDown, { capture: true });
398
+ host.addEventListener('mousedown', onMouseDown, { capture: true });
399
+ host.addEventListener('click', onClick, { capture: true });
400
+ host.addEventListener('contextmenu', onContextMenu, { capture: true });
401
+ // Key events: attach on document so focus inside any descendant is OK;
402
+ // the editable-target gate handles the "user is typing" case.
403
+ const doc = host.ownerDocument ?? document;
404
+ doc.addEventListener('keydown', onKeyDown, true);
405
+
406
+ return () => {
407
+ host.removeEventListener('pointermove', onPointerMove);
408
+ host.removeEventListener('pointerdown', onPointerDown, {
409
+ capture: true,
410
+ } as EventListenerOptions);
411
+ host.removeEventListener('mousedown', onMouseDown, { capture: true } as EventListenerOptions);
412
+ host.removeEventListener('click', onClick, { capture: true } as EventListenerOptions);
413
+ host.removeEventListener('contextmenu', onContextMenu, {
414
+ capture: true,
415
+ } as EventListenerOptions);
416
+ doc.removeEventListener('keydown', onKeyDown, true);
417
+ };
418
+ }, [enabled, hostRef, getActiveTool, isSpaceHeld, callbacks]);
419
+ }
420
+
421
+ // ─────────────────────────────────────────────────────────────────────────────
422
+ // resolveHoverTarget — walks from a clientX/clientY pair to the canvas element
423
+ // of interest. Default = topmost `[data-cd-id]` ancestor (the stable
424
+ // pipeline-stamped anchor). `deep = true` returns the deepest descendant
425
+ // (Cmd-hover behavior).
426
+
427
+ export interface HoverTarget {
428
+ el: Element;
429
+ cdId: string | null;
430
+ artboardId: string | null;
431
+ }
432
+
433
+ export function resolveHoverTarget(
434
+ doc: Document,
435
+ clientX: number,
436
+ clientY: number,
437
+ opts: { deep: boolean }
438
+ ): HoverTarget | null {
439
+ const hit = doc.elementFromPoint(clientX, clientY);
440
+ if (!hit) return null;
441
+ // Skip the floating chrome (MiniMap / ZoomToolbar / ToolPalette / ContextMenu)
442
+ // AND the canvas/world frame itself — the user is never asking to "select
443
+ // the entire canvas viewport," that's a UI accident from climbing too high.
444
+ if (hit.closest?.('.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-group-bbox')) {
445
+ return null;
446
+ }
447
+
448
+ const artboardEl = hit.closest?.('[data-dc-screen]') ?? null;
449
+ const artboardId = artboardEl?.getAttribute('data-dc-screen') ?? null;
450
+
451
+ // Hover-target hard ceiling = `.dc-artboard-body`. The artboard chrome
452
+ // (article root + label button + body wrapper) is never a selection
453
+ // candidate — those carry `data-cd-id` from the pipeline pass but visually
454
+ // selecting them makes the WHOLE artboard outline, which looks like the
455
+ // canvas viewport is selected. If the user landed on chrome (the label
456
+ // button or empty body padding), bail.
457
+ const bodyEl = hit.closest?.('.dc-artboard-body') ?? null;
458
+ if (!bodyEl) return null;
459
+ if (hit === bodyEl) {
460
+ // Clicked exactly on the body wrapper, no inner element under cursor.
461
+ return null;
462
+ }
463
+
464
+ if (opts.deep) {
465
+ // Deepest mode — the hit element IS the target. Use its OWN data-cd-id
466
+ // when present; never climb to an ancestor's id (climbing was the cause
467
+ // of "Cmd-click on a deep span selects the whole artboard root"). When
468
+ // the hit lacks a stamped id, consumers fall back to a CSS-path selector.
469
+ const cdId = hit.getAttribute?.('data-cd-id') ?? null;
470
+ return { el: hit, cdId, artboardId };
471
+ }
472
+
473
+ // Top mode — climb to the topmost descendant of the artboard body that
474
+ // still carries a data-cd-id. Hard ceiling is bodyEl itself (never select
475
+ // the body wrapper or higher).
476
+ let cur: Element | null = hit;
477
+ let topCdEl: Element | null = null;
478
+ while (cur && cur !== bodyEl) {
479
+ if (cur.hasAttribute?.('data-cd-id')) topCdEl = cur;
480
+ cur = cur.parentElement;
481
+ }
482
+ const el = topCdEl ?? hit;
483
+ const cdId = el.getAttribute?.('data-cd-id') ?? null;
484
+ return { el, cdId, artboardId };
485
+ }