@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,316 @@
1
+ // input-router — Phase 4.1 Task 1. Pure classifier table tests.
2
+
3
+ import { describe, expect, test } from 'bun:test';
4
+
5
+ import { type ClassifyInput, type Tool, classify, isEditableTarget } from '../input-router.tsx';
6
+
7
+ const base = (over: Partial<ClassifyInput>): ClassifyInput => ({
8
+ type: 'pointermove',
9
+ activeTool: 'move' as Tool,
10
+ ...over,
11
+ });
12
+
13
+ describe('input-router / pointermove — move tool', () => {
14
+ test('bare hover → no-op (native interactions pass through)', () => {
15
+ const action = classify(base({ type: 'pointermove', clientX: 10, clientY: 20 }));
16
+ expect(action.kind).toBe('no-op');
17
+ });
18
+
19
+ test('cmd-hover → hover preview with deep=true', () => {
20
+ const action = classify(base({ type: 'pointermove', metaKey: true, clientX: 5, clientY: 6 }));
21
+ expect(action).toMatchObject({ kind: 'hover', deep: true });
22
+ });
23
+
24
+ test('ctrl-hover (windows/linux) → hover with deep=true', () => {
25
+ const action = classify(base({ type: 'pointermove', ctrlKey: true }));
26
+ expect(action).toMatchObject({ kind: 'hover', deep: true });
27
+ });
28
+
29
+ test('hover in hand tool → no-op', () => {
30
+ const action = classify(base({ type: 'pointermove', activeTool: 'hand' }));
31
+ expect(action.kind).toBe('no-op');
32
+ });
33
+
34
+ test('hover in comment tool → hover with deep=true (preview deepest)', () => {
35
+ const action = classify(
36
+ base({ type: 'pointermove', activeTool: 'comment', clientX: 1, clientY: 2 })
37
+ );
38
+ expect(action).toEqual({ kind: 'hover', deep: true, clientX: 1, clientY: 2 });
39
+ });
40
+ });
41
+
42
+ describe('input-router / pointerdown — move tool select', () => {
43
+ test('bare left-click → no-op (native interactions pass through)', () => {
44
+ const action = classify(base({ type: 'pointerdown', button: 0, clientX: 100, clientY: 200 }));
45
+ expect(action.kind).toBe('no-op');
46
+ });
47
+
48
+ test('shift+left-click (no cmd) → no-op (no select without Cmd)', () => {
49
+ const action = classify(base({ type: 'pointerdown', button: 0, shiftKey: true }));
50
+ expect(action.kind).toBe('no-op');
51
+ });
52
+
53
+ test('cmd+left-click → select replace, deep=true (nested single)', () => {
54
+ const action = classify(
55
+ base({ type: 'pointerdown', button: 0, metaKey: true, clientX: 11, clientY: 22 })
56
+ );
57
+ expect(action).toEqual({
58
+ kind: 'select',
59
+ mode: 'replace',
60
+ deep: true,
61
+ clientX: 11,
62
+ clientY: 22,
63
+ });
64
+ });
65
+
66
+ test('cmd+shift+left-click → select add, deep=true (multi nested)', () => {
67
+ const action = classify(
68
+ base({ type: 'pointerdown', button: 0, metaKey: true, shiftKey: true })
69
+ );
70
+ expect(action).toMatchObject({ kind: 'select', mode: 'add', deep: true });
71
+ });
72
+
73
+ test('ctrl+left-click (linux/windows cmd-equivalent) → select replace deep', () => {
74
+ const action = classify(base({ type: 'pointerdown', button: 0, ctrlKey: true }));
75
+ expect(action).toMatchObject({ kind: 'select', mode: 'replace', deep: true });
76
+ });
77
+ });
78
+
79
+ describe('input-router / pointerdown — non-left buttons', () => {
80
+ test('middle-button → no-op (viewport-controller owns it)', () => {
81
+ const action = classify(base({ type: 'pointerdown', button: 1 }));
82
+ expect(action.kind).toBe('no-op');
83
+ });
84
+
85
+ test('right-button → context-menu', () => {
86
+ const action = classify(base({ type: 'pointerdown', button: 2, clientX: 50, clientY: 60 }));
87
+ expect(action).toEqual({ kind: 'context-menu', clientX: 50, clientY: 60 });
88
+ });
89
+
90
+ test('space-held + left-click → no-op (viewport-controller pans)', () => {
91
+ const action = classify(base({ type: 'pointerdown', button: 0, spaceHeld: true }));
92
+ expect(action.kind).toBe('no-op');
93
+ });
94
+ });
95
+
96
+ describe('input-router / pointerdown — tool-aware', () => {
97
+ test('comment tool + bare click → drop-comment', () => {
98
+ const action = classify(
99
+ base({ type: 'pointerdown', button: 0, activeTool: 'comment', clientX: 12, clientY: 34 })
100
+ );
101
+ expect(action).toEqual({ kind: 'drop-comment', clientX: 12, clientY: 34 });
102
+ });
103
+
104
+ test('comment tool + shift+click → drop-comment (modifier ignored for now)', () => {
105
+ const action = classify(
106
+ base({ type: 'pointerdown', button: 0, activeTool: 'comment', shiftKey: true })
107
+ );
108
+ expect(action.kind).toBe('drop-comment');
109
+ });
110
+
111
+ test('hand tool + bare click → no-op (viewport-controller claims the drag)', () => {
112
+ const action = classify(base({ type: 'pointerdown', button: 0, activeTool: 'hand' }));
113
+ expect(action.kind).toBe('no-op');
114
+ });
115
+
116
+ test('hand tool + cmd+click → still no-op (no select in hand mode)', () => {
117
+ const action = classify(
118
+ base({ type: 'pointerdown', button: 0, activeTool: 'hand', metaKey: true })
119
+ );
120
+ expect(action.kind).toBe('no-op');
121
+ });
122
+ });
123
+
124
+ describe('input-router / contextmenu event', () => {
125
+ test('always opens menu + carries cursor coords', () => {
126
+ const action = classify(base({ type: 'contextmenu', clientX: 77, clientY: 88 }));
127
+ expect(action).toEqual({ kind: 'context-menu', clientX: 77, clientY: 88 });
128
+ });
129
+ });
130
+
131
+ describe('input-router / keydown — tool letters', () => {
132
+ test('V → tool move', () => {
133
+ const action = classify(base({ type: 'keydown', key: 'v' }));
134
+ expect(action).toEqual({ kind: 'tool', tool: 'move' });
135
+ });
136
+
137
+ test('uppercase V (Shift held during letter)', () => {
138
+ const action = classify(base({ type: 'keydown', key: 'V' }));
139
+ expect(action).toEqual({ kind: 'tool', tool: 'move' });
140
+ });
141
+
142
+ test('H → tool hand', () => {
143
+ expect(classify(base({ type: 'keydown', key: 'h' }))).toEqual({
144
+ kind: 'tool',
145
+ tool: 'hand',
146
+ });
147
+ });
148
+
149
+ test('C → tool comment', () => {
150
+ expect(classify(base({ type: 'keydown', key: 'c' }))).toEqual({
151
+ kind: 'tool',
152
+ tool: 'comment',
153
+ });
154
+ });
155
+
156
+ test('Escape → escape action', () => {
157
+ expect(classify(base({ type: 'keydown', key: 'Escape' }))).toEqual({
158
+ kind: 'escape',
159
+ });
160
+ });
161
+
162
+ test('Cmd+C with modifier → no-op (browser copy / viewport-controller)', () => {
163
+ const action = classify(base({ type: 'keydown', key: 'c', metaKey: true }));
164
+ expect(action.kind).toBe('no-op');
165
+ });
166
+
167
+ test('Cmd+V with modifier → no-op', () => {
168
+ const action = classify(base({ type: 'keydown', key: 'v', metaKey: true }));
169
+ expect(action.kind).toBe('no-op');
170
+ });
171
+
172
+ test('Cmd+Escape → escape (cancels regardless of modifiers)', () => {
173
+ const action = classify(base({ type: 'keydown', key: 'Escape', metaKey: true }));
174
+ expect(action.kind).toBe('escape');
175
+ });
176
+
177
+ test('V in input field → no-op', () => {
178
+ const action = classify(base({ type: 'keydown', key: 'v', isEditable: true }));
179
+ expect(action.kind).toBe('no-op');
180
+ });
181
+
182
+ test('Other letters → no-op', () => {
183
+ expect(classify(base({ type: 'keydown', key: 'x' })).kind).toBe('no-op');
184
+ expect(classify(base({ type: 'keydown', key: 'Tab' })).kind).toBe('no-op');
185
+ expect(classify(base({ type: 'keydown', key: 'Enter' })).kind).toBe('no-op');
186
+ });
187
+ });
188
+
189
+ describe('input-router / keydown — Phase 5 draw tools', () => {
190
+ test('B → tool pen', () => {
191
+ expect(classify(base({ type: 'keydown', key: 'b' }))).toEqual({
192
+ kind: 'tool',
193
+ tool: 'pen',
194
+ });
195
+ });
196
+
197
+ test('R → tool rect', () => {
198
+ expect(classify(base({ type: 'keydown', key: 'r' }))).toEqual({
199
+ kind: 'tool',
200
+ tool: 'rect',
201
+ });
202
+ });
203
+
204
+ test('A → tool arrow', () => {
205
+ expect(classify(base({ type: 'keydown', key: 'a' }))).toEqual({
206
+ kind: 'tool',
207
+ tool: 'arrow',
208
+ });
209
+ });
210
+
211
+ test('E → tool eraser', () => {
212
+ expect(classify(base({ type: 'keydown', key: 'e' }))).toEqual({
213
+ kind: 'tool',
214
+ tool: 'eraser',
215
+ });
216
+ });
217
+
218
+ test('O → tool ellipse (Phase 5.1)', () => {
219
+ expect(classify(base({ type: 'keydown', key: 'o' }))).toEqual({
220
+ kind: 'tool',
221
+ tool: 'ellipse',
222
+ });
223
+ });
224
+
225
+ test('uppercase B (shift held) — still maps to pen (lowercased)', () => {
226
+ expect(classify(base({ type: 'keydown', key: 'B', shiftKey: true }))).toEqual({
227
+ kind: 'tool',
228
+ tool: 'pen',
229
+ });
230
+ });
231
+
232
+ test('Cmd+B (modifier-held) → no-op so the browser keeps it', () => {
233
+ expect(classify(base({ type: 'keydown', key: 'b', metaKey: true })).kind).toBe('no-op');
234
+ });
235
+ });
236
+
237
+ describe('input-router / pointer events — Phase 5 annotation tools', () => {
238
+ test('pointermove in pen tool → no-op (SVG overlay owns it)', () => {
239
+ expect(
240
+ classify(base({ type: 'pointermove', activeTool: 'pen', clientX: 1, clientY: 2 })).kind
241
+ ).toBe('no-op');
242
+ });
243
+
244
+ test('pointermove in eraser tool → no-op', () => {
245
+ expect(classify(base({ type: 'pointermove', activeTool: 'eraser' })).kind).toBe('no-op');
246
+ });
247
+
248
+ test('bare left-click in rect tool → no-op (SVG overlay claims)', () => {
249
+ expect(classify(base({ type: 'pointerdown', activeTool: 'rect', button: 0 })).kind).toBe(
250
+ 'no-op'
251
+ );
252
+ });
253
+
254
+ test('bare left-click in ellipse tool → no-op (Phase 5.1)', () => {
255
+ expect(classify(base({ type: 'pointerdown', activeTool: 'ellipse', button: 0 })).kind).toBe(
256
+ 'no-op'
257
+ );
258
+ });
259
+
260
+ test('pointermove in ellipse tool → no-op (SVG overlay owns it)', () => {
261
+ expect(classify(base({ type: 'pointermove', activeTool: 'ellipse' })).kind).toBe('no-op');
262
+ });
263
+
264
+ test('cmd+left-click in arrow tool → select replace (escape hatch to move)', () => {
265
+ expect(
266
+ classify(
267
+ base({
268
+ type: 'pointerdown',
269
+ activeTool: 'arrow',
270
+ button: 0,
271
+ metaKey: true,
272
+ clientX: 4,
273
+ clientY: 5,
274
+ })
275
+ )
276
+ ).toEqual({
277
+ kind: 'select',
278
+ mode: 'replace',
279
+ deep: true,
280
+ clientX: 4,
281
+ clientY: 5,
282
+ });
283
+ });
284
+
285
+ test('right-click in pen tool → context-menu (unchanged)', () => {
286
+ expect(classify(base({ type: 'pointerdown', activeTool: 'pen', button: 2 })).kind).toBe(
287
+ 'context-menu'
288
+ );
289
+ });
290
+ });
291
+
292
+ describe('input-router / isEditableTarget', () => {
293
+ test('null target → false', () => {
294
+ expect(isEditableTarget(null)).toBe(false);
295
+ });
296
+
297
+ test('plain div → false', () => {
298
+ const el = { tagName: 'DIV', isContentEditable: false } as HTMLElement;
299
+ expect(isEditableTarget(el)).toBe(false);
300
+ });
301
+
302
+ test('INPUT → true', () => {
303
+ const el = { tagName: 'INPUT', isContentEditable: false } as HTMLElement;
304
+ expect(isEditableTarget(el)).toBe(true);
305
+ });
306
+
307
+ test('TEXTAREA → true', () => {
308
+ const el = { tagName: 'TEXTAREA', isContentEditable: false } as HTMLElement;
309
+ expect(isEditableTarget(el)).toBe(true);
310
+ });
311
+
312
+ test('contentEditable=true → true', () => {
313
+ const el = { tagName: 'DIV', isContentEditable: true } as HTMLElement;
314
+ expect(isEditableTarget(el)).toBe(true);
315
+ });
316
+ });
@@ -0,0 +1,214 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ type LocatorMap,
8
+ canvasSlug,
9
+ clearLocatorSlug,
10
+ readLocator,
11
+ readLocatorFile,
12
+ writeLocator,
13
+ } from '../locator.ts';
14
+
15
+ function tmp(): { dir: string; locFile: string; cleanup: () => void } {
16
+ const dir = mkdtempSync(path.join(tmpdir(), 'mdcc-locator-'));
17
+ const locFile = path.join(dir, '_locator.json');
18
+ return { dir, locFile, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
19
+ }
20
+
21
+ const entry = (canvas: string, line: number): LocatorMap[string] => ({
22
+ canvas,
23
+ line,
24
+ col: 0,
25
+ jsxPath: ['section'],
26
+ componentName: 'X',
27
+ });
28
+
29
+ describe('canvasSlug', () => {
30
+ test('strips designRoot prefix and file extension', () => {
31
+ expect(canvasSlug('/proj/.design/ui/Docs Site.tsx', '/proj/.design')).toBe('ui/Docs Site');
32
+ });
33
+
34
+ test('handles nested paths', () => {
35
+ expect(canvasSlug('/proj/.design/system/project/preview/btn.tsx', '/proj/.design')).toBe(
36
+ 'system/project/preview/btn'
37
+ );
38
+ });
39
+
40
+ test('files without extension are returned as-is', () => {
41
+ expect(canvasSlug('/proj/.design/ui/no-ext', '/proj/.design')).toBe('ui/no-ext');
42
+ });
43
+ });
44
+
45
+ describe('readLocatorFile / readLocator', () => {
46
+ test('returns empty object when the file does not exist', async () => {
47
+ const t = tmp();
48
+ try {
49
+ expect(await readLocatorFile(t.locFile)).toEqual({});
50
+ expect(await readLocator(t.locFile, 'ui/anything')).toBeNull();
51
+ } finally {
52
+ t.cleanup();
53
+ }
54
+ });
55
+
56
+ test('treats malformed JSON as empty without throwing', async () => {
57
+ const t = tmp();
58
+ try {
59
+ await Bun.write(t.locFile, '{ not valid');
60
+ expect(await readLocatorFile(t.locFile)).toEqual({});
61
+ } finally {
62
+ t.cleanup();
63
+ }
64
+ });
65
+
66
+ test('treats a top-level array as empty (defensive against bad shape)', async () => {
67
+ const t = tmp();
68
+ try {
69
+ await Bun.write(t.locFile, '[]');
70
+ expect(await readLocatorFile(t.locFile)).toEqual({});
71
+ } finally {
72
+ t.cleanup();
73
+ }
74
+ });
75
+ });
76
+
77
+ describe('writeLocator — roundtrip + atomicity', () => {
78
+ test('round-trip a single slug', async () => {
79
+ const t = tmp();
80
+ try {
81
+ const map: LocatorMap = { aaaaaaaa: entry('/c/X.tsx', 4) };
82
+ await writeLocator(t.locFile, 'ui/X', map);
83
+ const back = await readLocator(t.locFile, 'ui/X');
84
+ expect(back).toEqual({ aaaaaaaa: entry('/c/X.tsx', 4) });
85
+ } finally {
86
+ t.cleanup();
87
+ }
88
+ });
89
+
90
+ test('multiple slugs share the same _locator.json without trampling', async () => {
91
+ const t = tmp();
92
+ try {
93
+ await writeLocator(t.locFile, 'ui/A', { aaaaaaaa: entry('/c/A.tsx', 1) });
94
+ await writeLocator(t.locFile, 'ui/B', { bbbbbbbb: entry('/c/B.tsx', 2) });
95
+ const file = await readLocatorFile(t.locFile);
96
+ expect(Object.keys(file).sort()).toEqual(['ui/A', 'ui/B']);
97
+ expect(file['ui/A']).toEqual({ aaaaaaaa: entry('/c/A.tsx', 1) });
98
+ expect(file['ui/B']).toEqual({ bbbbbbbb: entry('/c/B.tsx', 2) });
99
+ } finally {
100
+ t.cleanup();
101
+ }
102
+ });
103
+
104
+ test('updating an existing slug overwrites its map but leaves siblings intact', async () => {
105
+ const t = tmp();
106
+ try {
107
+ await writeLocator(t.locFile, 'ui/A', { aaaaaaaa: entry('/c/A.tsx', 1) });
108
+ await writeLocator(t.locFile, 'ui/B', { bbbbbbbb: entry('/c/B.tsx', 2) });
109
+ await writeLocator(t.locFile, 'ui/A', { cccccccc: entry('/c/A.tsx', 9) });
110
+ const file = await readLocatorFile(t.locFile);
111
+ expect(file['ui/A']).toEqual({ cccccccc: entry('/c/A.tsx', 9) });
112
+ expect(file['ui/B']).toEqual({ bbbbbbbb: entry('/c/B.tsx', 2) });
113
+ } finally {
114
+ t.cleanup();
115
+ }
116
+ });
117
+
118
+ test('on-disk JSON is deterministic (sorted keys, trailing newline)', async () => {
119
+ const t = tmp();
120
+ try {
121
+ const big: LocatorMap = {
122
+ zzzzzzzz: entry('/c/X.tsx', 9),
123
+ aaaaaaaa: entry('/c/X.tsx', 1),
124
+ mmmmmmmm: entry('/c/X.tsx', 5),
125
+ };
126
+ await writeLocator(t.locFile, 'ui/X', big);
127
+ const raw = await Bun.file(t.locFile).text();
128
+ // top-level + nested keys should be sorted
129
+ const aIdx = raw.indexOf('aaaaaaaa');
130
+ const mIdx = raw.indexOf('mmmmmmmm');
131
+ const zIdx = raw.indexOf('zzzzzzzz');
132
+ expect(aIdx).toBeGreaterThan(-1);
133
+ expect(aIdx).toBeLessThan(mIdx);
134
+ expect(mIdx).toBeLessThan(zIdx);
135
+ expect(raw.endsWith('\n')).toBe(true);
136
+ } finally {
137
+ t.cleanup();
138
+ }
139
+ });
140
+
141
+ test('concurrent writes against the same file do not corrupt JSON', async () => {
142
+ const t = tmp();
143
+ try {
144
+ // Fire 20 writes in parallel, each against a distinct slug. The
145
+ // per-path mutex must serialise them; the final file must contain all 20.
146
+ const writes: Promise<void>[] = [];
147
+ for (let i = 0; i < 20; i++) {
148
+ const slug = `ui/slug-${String(i).padStart(2, '0')}`;
149
+ const map: LocatorMap = {
150
+ [`id${String(i).padStart(5, '0')}`]: entry(`/c/${slug}.tsx`, i + 1),
151
+ };
152
+ writes.push(writeLocator(t.locFile, slug, map));
153
+ }
154
+ await Promise.all(writes);
155
+ const file = await readLocatorFile(t.locFile);
156
+ expect(Object.keys(file).length).toBe(20);
157
+ for (let i = 0; i < 20; i++) {
158
+ const slug = `ui/slug-${String(i).padStart(2, '0')}`;
159
+ expect(file[slug]).toBeDefined();
160
+ }
161
+ } finally {
162
+ t.cleanup();
163
+ }
164
+ });
165
+
166
+ test('concurrent writes against the SAME slug end up with the last writer winning, no partial state', async () => {
167
+ const t = tmp();
168
+ try {
169
+ const writes: Promise<void>[] = [];
170
+ // 10 writes to the same slug, each with a different singleton map.
171
+ for (let i = 0; i < 10; i++) {
172
+ const map: LocatorMap = { [`id-${i}`]: entry('/c/X.tsx', i) };
173
+ writes.push(writeLocator(t.locFile, 'ui/X', map));
174
+ }
175
+ await Promise.all(writes);
176
+ const back = await readLocator(t.locFile, 'ui/X');
177
+ expect(back).not.toBeNull();
178
+ // Each writer fully replaces the slug map, so the final state has exactly
179
+ // one entry. Which entry won is implementation-defined (FIFO via mutex),
180
+ // but the file shape is well-formed.
181
+ expect(Object.keys(back as LocatorMap).length).toBe(1);
182
+ } finally {
183
+ t.cleanup();
184
+ }
185
+ });
186
+ });
187
+
188
+ describe('clearLocatorSlug', () => {
189
+ test('removes one slug and leaves siblings', async () => {
190
+ const t = tmp();
191
+ try {
192
+ await writeLocator(t.locFile, 'ui/A', { aaaaaaaa: entry('/c/A.tsx', 1) });
193
+ await writeLocator(t.locFile, 'ui/B', { bbbbbbbb: entry('/c/B.tsx', 2) });
194
+ await clearLocatorSlug(t.locFile, 'ui/A');
195
+ const file = await readLocatorFile(t.locFile);
196
+ expect(file['ui/A']).toBeUndefined();
197
+ expect(file['ui/B']).toBeDefined();
198
+ } finally {
199
+ t.cleanup();
200
+ }
201
+ });
202
+
203
+ test('is a no-op when slug missing', async () => {
204
+ const t = tmp();
205
+ try {
206
+ await writeLocator(t.locFile, 'ui/A', { aaaaaaaa: entry('/c/A.tsx', 1) });
207
+ await clearLocatorSlug(t.locFile, 'ui/Nope');
208
+ const file = await readLocatorFile(t.locFile);
209
+ expect(file['ui/A']).toBeDefined();
210
+ } finally {
211
+ t.cleanup();
212
+ }
213
+ });
214
+ });