@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,419 @@
1
+ // annotations-layer — Phase 5 pure helpers (no DOM render).
2
+ //
3
+ // Covers: SVG round-trip (strokesToSvg + svgToStrokes), pen path serialization,
4
+ // arrow-head geometry, eraser hit-test across all three shapes.
5
+ //
6
+ // `svgToStrokes` reads via DOMParser; bun:test exposes one through happy-dom
7
+ // when imported transitively. We dodge that here by parsing the regex-friendly
8
+ // shapes directly and only exercising the parser in the integration test.
9
+
10
+ import { describe, expect, test } from 'bun:test';
11
+
12
+ import {
13
+ type ArrowStroke,
14
+ type EllipseStroke,
15
+ type PenStroke,
16
+ type RectStroke,
17
+ type Stroke,
18
+ type TextStroke,
19
+ arrowHeadPoints,
20
+ penPathD,
21
+ rid,
22
+ strokeHitTest,
23
+ strokesToSvg,
24
+ } from '../annotations-layer.tsx';
25
+
26
+ describe('annotations-layer / penPathD', () => {
27
+ test('empty points → empty string', () => {
28
+ expect(penPathD([])).toBe('');
29
+ });
30
+
31
+ test('single point → M only', () => {
32
+ expect(penPathD([[10, 20]])).toBe('M10 20');
33
+ });
34
+
35
+ test('multiple points → M then L per next', () => {
36
+ expect(
37
+ penPathD([
38
+ [0, 0],
39
+ [10, 10],
40
+ [20, 5],
41
+ ])
42
+ ).toBe('M0 0 L10 10 L20 5');
43
+ });
44
+ });
45
+
46
+ describe('annotations-layer / arrowHeadPoints', () => {
47
+ test('horizontal arrow head is symmetric around the tip', () => {
48
+ const pts = arrowHeadPoints(0, 0, 100, 0, 2);
49
+ // "ax,ay x2,y2 bx,by" — y of the two wings symmetric across y=0
50
+ const tokens = pts.split(' ');
51
+ expect(tokens).toHaveLength(3);
52
+ const [a, tip, b] = tokens.map((t) => t.split(',').map(Number));
53
+ expect(tip).toEqual([100, 0]);
54
+ expect(a?.[1]).toBeCloseTo(-(b?.[1] ?? 0), 4);
55
+ // The wings sit BEFORE the tip on x.
56
+ expect(a?.[0]).toBeLessThan(100);
57
+ expect(b?.[0]).toBeLessThan(100);
58
+ });
59
+
60
+ test('zero-length arrow degenerates safely (no NaN)', () => {
61
+ const pts = arrowHeadPoints(50, 50, 50, 50, 2);
62
+ for (const t of pts.split(/[ ,]/)) {
63
+ expect(Number.isFinite(Number(t))).toBe(true);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('annotations-layer / strokesToSvg', () => {
69
+ test('empty list → self-closing svg shell', () => {
70
+ expect(strokesToSvg([])).toBe(
71
+ '<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1"></svg>'
72
+ );
73
+ });
74
+
75
+ test('pen stroke → <path data-tool="pen" d=...>', () => {
76
+ const pen: PenStroke = {
77
+ id: 'p1',
78
+ tool: 'pen',
79
+ color: '#d63b1f',
80
+ width: 2,
81
+ points: [
82
+ [0, 0],
83
+ [10, 10],
84
+ ],
85
+ };
86
+ const svg = strokesToSvg([pen]);
87
+ expect(svg).toContain('data-id="p1"');
88
+ expect(svg).toContain('data-tool="pen"');
89
+ expect(svg).toContain('d="M0 0 L10 10"');
90
+ expect(svg).toContain('stroke="#d63b1f"');
91
+ expect(svg).toContain('vector-effect="non-scaling-stroke"');
92
+ });
93
+
94
+ test('rect with negative w/h is clamped to 0 in serialization', () => {
95
+ const rect: RectStroke = {
96
+ id: 'r1',
97
+ tool: 'rect',
98
+ color: '#1d6cf0',
99
+ width: 2,
100
+ x: 10,
101
+ y: 10,
102
+ w: -5,
103
+ h: -8,
104
+ };
105
+ const svg = strokesToSvg([rect]);
106
+ expect(svg).toContain('width="0"');
107
+ expect(svg).toContain('height="0"');
108
+ });
109
+
110
+ test('arrow renders as <g> with <line> + <polyline> head', () => {
111
+ const arrow: ArrowStroke = {
112
+ id: 'a1',
113
+ tool: 'arrow',
114
+ color: '#1a8f3e',
115
+ width: 2,
116
+ x1: 0,
117
+ y1: 0,
118
+ x2: 100,
119
+ y2: 0,
120
+ };
121
+ const svg = strokesToSvg([arrow]);
122
+ expect(svg).toContain('data-tool="arrow"');
123
+ expect(svg).toContain('<line x1="0" y1="0" x2="100" y2="0"');
124
+ expect(svg).toContain('<polyline points=');
125
+ });
126
+
127
+ test('color value is HTML-escaped on output (no tag injection)', () => {
128
+ const pen: PenStroke = {
129
+ id: 'p2',
130
+ tool: 'pen',
131
+ color: '"><script>',
132
+ width: 2,
133
+ points: [
134
+ [0, 0],
135
+ [1, 1],
136
+ ],
137
+ };
138
+ const svg = strokesToSvg([pen]);
139
+ // The `<` of <script> and the `"` that would close the attribute are
140
+ // both escaped — those are the two chars that could break out of the
141
+ // stroke="..." attribute. `>` is harmless inside an attribute value, so
142
+ // we don't waste bytes encoding it.
143
+ expect(svg).toContain('stroke="&quot;>&lt;script>"');
144
+ // Belt-and-suspenders: no live <script tag survives anywhere in output.
145
+ expect(svg).not.toMatch(/<script[\s>]/);
146
+ });
147
+ });
148
+
149
+ describe('annotations-layer / strokeHitTest', () => {
150
+ const pen: PenStroke = {
151
+ id: 'p',
152
+ tool: 'pen',
153
+ color: '#000',
154
+ width: 2,
155
+ points: [
156
+ [0, 0],
157
+ [100, 0],
158
+ [100, 100],
159
+ ],
160
+ };
161
+
162
+ test('pen hit near the first segment', () => {
163
+ expect(strokeHitTest(pen, 50, 2, 4)).toBe(true);
164
+ });
165
+
166
+ test('pen miss far from any segment', () => {
167
+ expect(strokeHitTest(pen, 50, 50, 4)).toBe(false);
168
+ });
169
+
170
+ test('pen hit near the corner (transition between segments)', () => {
171
+ expect(strokeHitTest(pen, 101, 50, 4)).toBe(true);
172
+ });
173
+
174
+ test('single-point pen stroke is hit-testable as a disk', () => {
175
+ const dot: PenStroke = {
176
+ id: 'd',
177
+ tool: 'pen',
178
+ color: '#000',
179
+ width: 2,
180
+ points: [[10, 10]],
181
+ };
182
+ expect(strokeHitTest(dot, 11, 11, 4)).toBe(true);
183
+ expect(strokeHitTest(dot, 50, 50, 4)).toBe(false);
184
+ });
185
+
186
+ test('rect: hit on edge, miss in interior', () => {
187
+ const r: RectStroke = {
188
+ id: 'r',
189
+ tool: 'rect',
190
+ color: '#000',
191
+ width: 2,
192
+ x: 0,
193
+ y: 0,
194
+ w: 100,
195
+ h: 100,
196
+ };
197
+ expect(strokeHitTest(r, 0, 50, 4)).toBe(true); // left edge
198
+ expect(strokeHitTest(r, 100, 50, 4)).toBe(true); // right edge
199
+ expect(strokeHitTest(r, 50, 100, 4)).toBe(true); // bottom edge
200
+ expect(strokeHitTest(r, 50, 50, 4)).toBe(false); // interior
201
+ expect(strokeHitTest(r, 500, 500, 4)).toBe(false); // far outside
202
+ });
203
+
204
+ test('arrow: hit near shaft, miss otherwise', () => {
205
+ const a: ArrowStroke = {
206
+ id: 'a',
207
+ tool: 'arrow',
208
+ color: '#000',
209
+ width: 2,
210
+ x1: 0,
211
+ y1: 0,
212
+ x2: 100,
213
+ y2: 0,
214
+ };
215
+ expect(strokeHitTest(a, 50, 1, 4)).toBe(true);
216
+ expect(strokeHitTest(a, 50, 50, 4)).toBe(false);
217
+ });
218
+
219
+ test('tolerance widens with stroke width', () => {
220
+ const wide: PenStroke = {
221
+ id: 'w',
222
+ tool: 'pen',
223
+ color: '#000',
224
+ width: 20,
225
+ points: [
226
+ [0, 0],
227
+ [100, 0],
228
+ ],
229
+ };
230
+ // 8 world units away vertically, with tol=4 → would miss a thin stroke,
231
+ // but here width 20 raises the floor.
232
+ expect(strokeHitTest(wide, 50, 8, 4)).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('annotations-layer / rid', () => {
237
+ test('generates a string with the `s_` prefix', () => {
238
+ const id = rid();
239
+ expect(id.startsWith('s_')).toBe(true);
240
+ expect(id.length).toBeGreaterThan(2);
241
+ });
242
+
243
+ test('subsequent calls are distinct', () => {
244
+ const seen = new Set<string>();
245
+ for (let i = 0; i < 50; i++) seen.add(rid());
246
+ expect(seen.size).toBe(50);
247
+ });
248
+ });
249
+
250
+ describe('annotations-layer / Phase 5.1 ellipse + fill + text + thickness', () => {
251
+ test('ellipse stroke → <ellipse data-tool="ellipse" cx= cy= rx= ry=>', () => {
252
+ const e: EllipseStroke = {
253
+ id: 'e1',
254
+ tool: 'ellipse',
255
+ color: '#7a4ad3',
256
+ width: 2,
257
+ cx: 50,
258
+ cy: 60,
259
+ rx: 30,
260
+ ry: 20,
261
+ fill: null,
262
+ };
263
+ const svg = strokesToSvg([e]);
264
+ expect(svg).toContain('data-id="e1"');
265
+ expect(svg).toContain('data-tool="ellipse"');
266
+ expect(svg).toContain('cx="50"');
267
+ expect(svg).toContain('cy="60"');
268
+ expect(svg).toContain('rx="30"');
269
+ expect(svg).toContain('ry="20"');
270
+ expect(svg).toContain('fill="none"');
271
+ });
272
+
273
+ test('rect with fill is serialized with fill="..." (not none)', () => {
274
+ const r: RectStroke = {
275
+ id: 'r-fill',
276
+ tool: 'rect',
277
+ color: '#1d6cf0',
278
+ width: 2,
279
+ x: 0,
280
+ y: 0,
281
+ w: 50,
282
+ h: 50,
283
+ fill: '#fff4d6',
284
+ };
285
+ const svg = strokesToSvg([r]);
286
+ expect(svg).toContain('fill="#fff4d6"');
287
+ expect(svg).not.toContain('fill="none"');
288
+ });
289
+
290
+ test('ellipse with explicit fill survives serialization', () => {
291
+ const e: EllipseStroke = {
292
+ id: 'e-fill',
293
+ tool: 'ellipse',
294
+ color: '#1a8f3e',
295
+ width: 2,
296
+ cx: 100,
297
+ cy: 100,
298
+ rx: 40,
299
+ ry: 25,
300
+ fill: '#e6f4ea',
301
+ };
302
+ const svg = strokesToSvg([e]);
303
+ expect(svg).toContain('fill="#e6f4ea"');
304
+ });
305
+
306
+ test('text stroke → <text data-tool="text" data-anchor-id= data-font-size=>', () => {
307
+ const t: TextStroke = {
308
+ id: 't1',
309
+ tool: 'text',
310
+ color: '#1a1a1a',
311
+ fontSize: 14,
312
+ text: 'needs padding',
313
+ anchorId: 'r-host',
314
+ };
315
+ const svg = strokesToSvg([t]);
316
+ expect(svg).toContain('data-tool="text"');
317
+ expect(svg).toContain('data-anchor-id="r-host"');
318
+ expect(svg).toContain('data-font-size="14"');
319
+ expect(svg).toContain('>needs padding</text>');
320
+ });
321
+
322
+ test('text content is HTML-escaped (no tag injection)', () => {
323
+ const t: TextStroke = {
324
+ id: 't2',
325
+ tool: 'text',
326
+ color: '#000',
327
+ fontSize: 14,
328
+ text: '<script>alert(1)</script>',
329
+ anchorId: 'host',
330
+ };
331
+ const svg = strokesToSvg([t]);
332
+ expect(svg).toContain('&lt;script>alert(1)&lt;/script>');
333
+ expect(svg).not.toMatch(/<script[\s>]/);
334
+ });
335
+
336
+ test('pen thickness round-trips via stroke-width (thin=2 / thick=6)', () => {
337
+ const thick: PenStroke = {
338
+ id: 'pT',
339
+ tool: 'pen',
340
+ color: '#000',
341
+ width: 6,
342
+ points: [
343
+ [0, 0],
344
+ [10, 10],
345
+ ],
346
+ };
347
+ const svg = strokesToSvg([thick]);
348
+ expect(svg).toContain('stroke-width="6"');
349
+ });
350
+
351
+ test('ellipse hit-test: stroke band detection (no fill)', () => {
352
+ const e: EllipseStroke = {
353
+ id: 'e',
354
+ tool: 'ellipse',
355
+ color: '#000',
356
+ width: 2,
357
+ cx: 100,
358
+ cy: 100,
359
+ rx: 50,
360
+ ry: 50,
361
+ fill: null,
362
+ };
363
+ // On the perimeter
364
+ expect(strokeHitTest(e, 150, 100, 4)).toBe(true);
365
+ // Inside (no fill) → miss
366
+ expect(strokeHitTest(e, 100, 100, 4)).toBe(false);
367
+ // Far outside → miss
368
+ expect(strokeHitTest(e, 300, 300, 4)).toBe(false);
369
+ });
370
+
371
+ test('ellipse hit-test: filled ellipse hits inside', () => {
372
+ const e: EllipseStroke = {
373
+ id: 'e-fill',
374
+ tool: 'ellipse',
375
+ color: '#000',
376
+ width: 2,
377
+ cx: 100,
378
+ cy: 100,
379
+ rx: 50,
380
+ ry: 50,
381
+ fill: '#fff',
382
+ };
383
+ expect(strokeHitTest(e, 100, 100, 4)).toBe(true);
384
+ expect(strokeHitTest(e, 300, 300, 4)).toBe(false);
385
+ });
386
+ });
387
+
388
+ describe('annotations-layer / strokes round-trip is stable for arrays', () => {
389
+ // We don't run svgToStrokes here (DOMParser is environment-bound under
390
+ // bun:test). Instead we assert serialization is deterministic + stable so
391
+ // the wire format doesn't drift across edits.
392
+ const sample: Stroke[] = [
393
+ {
394
+ id: 'p1',
395
+ tool: 'pen',
396
+ color: '#000',
397
+ width: 2,
398
+ points: [
399
+ [1, 2],
400
+ [3, 4],
401
+ [5, 6],
402
+ ],
403
+ },
404
+ {
405
+ id: 'r1',
406
+ tool: 'rect',
407
+ color: '#111',
408
+ width: 2,
409
+ x: 10,
410
+ y: 20,
411
+ w: 30,
412
+ h: 40,
413
+ },
414
+ ];
415
+
416
+ test('identical input yields identical output (no randomness)', () => {
417
+ expect(strokesToSvg(sample)).toBe(strokesToSvg(sample));
418
+ });
419
+ });
@@ -0,0 +1,47 @@
1
+ // Smoke: dist/maude-<platform> compiles + spawns + exits cleanly.
2
+ //
3
+ // We run this only when the compiled binary is already present. The full
4
+ // `bun build --compile` step costs ~150 ms; CI matrix already proves it on
5
+ // every release tag (Task 13). Local devs see this skip silently unless they
6
+ // have already run `bun run build.ts --release`.
7
+
8
+ import { describe, expect, test } from 'bun:test';
9
+ import { join } from 'node:path';
10
+
11
+ const PLATFORM_SLUG = (() => {
12
+ const p = process.platform;
13
+ const a = process.arch;
14
+ if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
15
+ if (p === 'linux') return a === 'arm64' ? 'linux-arm64' : 'linux-x64';
16
+ if (p === 'win32') return 'win32-x64';
17
+ return null;
18
+ })();
19
+
20
+ describe('binary smoke', () => {
21
+ test('compiled binary boots and exits 0 on --help / handles unknown args gracefully', async () => {
22
+ if (!PLATFORM_SLUG) {
23
+ console.log('binary-smoke: unsupported host platform; skipping');
24
+ return;
25
+ }
26
+ const binPath = join(import.meta.dir, '..', 'dist', `maude-${PLATFORM_SLUG}`);
27
+ const exists = await Bun.file(binPath).exists();
28
+ if (!exists) {
29
+ console.log(
30
+ `binary-smoke: ${binPath} not built; skipping (run \`bun run build.ts --release\`)`
31
+ );
32
+ return;
33
+ }
34
+
35
+ // The compiled binary is the dev-server entry — invoking it with --root
36
+ // pointing at a temp dir without .design/ causes a fail-loud exit (1).
37
+ // That's enough to prove the binary is a valid executable.
38
+ const proc = Bun.spawn({
39
+ cmd: [binPath, '--root', '/tmp/nonexistent-maude-test'],
40
+ stdout: 'pipe',
41
+ stderr: 'pipe',
42
+ env: { ...process.env, NO_OPEN: '1' },
43
+ });
44
+ const code = await proc.exited;
45
+ expect(code === 0 || code === 1).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,29 @@
1
+ // Smoke: dist/client.bundle.js exists, parses, and references React 19.
2
+
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { join } from 'node:path';
5
+ import { spawnSync } from 'bun';
6
+
7
+ describe('client bundle', () => {
8
+ test('build.ts produces a parsable IIFE bundle that ships React', async () => {
9
+ const here = join(import.meta.dir, '..');
10
+ const result = spawnSync({
11
+ cmd: ['bun', 'run', join(here, 'build.ts')],
12
+ cwd: here,
13
+ stdout: 'pipe',
14
+ stderr: 'pipe',
15
+ env: { ...process.env, NODE_ENV: 'development' },
16
+ });
17
+ expect(result.exitCode).toBe(0);
18
+
19
+ const bundle = Bun.file(join(here, 'dist', 'client.bundle.js'));
20
+ expect(await bundle.exists()).toBe(true);
21
+ expect(bundle.size).toBeGreaterThan(50_000);
22
+
23
+ const src = await bundle.text();
24
+ // React 19 internal marker — exact constants change across minor versions,
25
+ // so we look for a stable substring on the IIFE shell + a known React API.
26
+ expect(src.includes('createRoot') || src.includes('react-dom')).toBe(true);
27
+ expect(src.includes('useState') || src.includes('react')).toBe(true);
28
+ });
29
+ });
@@ -0,0 +1,78 @@
1
+ // canvas-build.ts — Phase 3.6 Task 6. Verify the per-canvas Bun.build wrap
2
+ // produces browser-loadable ES modules that:
3
+ // 1. preserve the data-cd-id attributes injected by canvas-pipeline pass 1,
4
+ // 2. import "react" / "react/jsx-dev-runtime" as standard ESM specifiers
5
+ // (browser resolves via importmap to /_canvas-runtime/*.js),
6
+ // 3. expose the default export so _shell.html can mount it.
7
+
8
+ import { describe, expect, test } from 'bun:test';
9
+
10
+ import { buildCanvasModule } from '../canvas-build.ts';
11
+
12
+ // Bun.build resolves the entrypoint against the filesystem (its virtual-loader
13
+ // plugin only kicks in for onLoad, after onResolve has confirmed the path
14
+ // exists). Each test writes its source to a real temp file and points
15
+ // buildCanvasModule at it; the canvas-virtual-source plugin then intercepts
16
+ // the onLoad and feeds the post-pass-1 TSX.
17
+ async function writeTmp(name: string, source: string): Promise<string> {
18
+ const abs = `/tmp/canvas-build-${name}-${Math.random().toString(36).slice(2, 8)}.tsx`;
19
+ await Bun.write(abs, source);
20
+ return abs;
21
+ }
22
+
23
+ describe('canvas-build / buildCanvasModule', () => {
24
+ test('preserves data-cd-id from pass 1', async () => {
25
+ const src =
26
+ `import { useState } from "react";\n` +
27
+ 'export default function Demo() {\n' +
28
+ ' const [n] = useState(0);\n' +
29
+ ` return <button className="btn">{n}</button>;\n` +
30
+ '}\n';
31
+ const abs = await writeTmp('cd-id', src);
32
+ const r = await buildCanvasModule(abs, src);
33
+ expect(r.js).toContain('data-cd-id');
34
+ expect(r.js).toContain('className: "btn"');
35
+ });
36
+
37
+ test('emits standard react JSX runtime import (no Bun-internal symbols)', async () => {
38
+ const src = 'export default function X() { return <div />; }\n';
39
+ const abs = await writeTmp('std-jsx', src);
40
+ const r = await buildCanvasModule(abs, src);
41
+ // Production-mode build uses react/jsx-runtime (the dev variant trips a
42
+ // Bun.build rename collision with React's CJS `var React` hoist; see
43
+ // runtime-bundle.ts comment). Accept either runtime name.
44
+ expect(r.js).toMatch(/react\/jsx(-dev)?-runtime/);
45
+ // The pre-3.6 Bun.Transpiler-only path emitted `jsxDEV_<hash>` /
46
+ // `jsx_<hash>`; Bun.build uses the standard name.
47
+ expect(r.js).not.toMatch(/jsxDEV?_[0-9a-z]{6,}/);
48
+ });
49
+
50
+ test('locator + etag mirror the canvas-pipeline result', async () => {
51
+ const src =
52
+ 'export default function Y() {\n' + ' return <section><h1>hi</h1></section>;\n' + '}\n';
53
+ const abs = await writeTmp('locator', src);
54
+ const r = await buildCanvasModule(abs, src);
55
+ // section + h1 = 2 JSX elements → 2 locator entries.
56
+ expect(Object.keys(r.locator).length).toBe(2);
57
+ expect(r.etag).toMatch(/^[0-9a-f]+$/);
58
+ });
59
+
60
+ test('externalises react + reactDOM (no inlined copies)', async () => {
61
+ const src = 'export default function Z() { return <span />; }\n';
62
+ const abs = await writeTmp('external', src);
63
+ const r = await buildCanvasModule(abs, src);
64
+ // ReactDOM's package signature; the canvas bundle must NOT contain it,
65
+ // or the runtime singleton invariant breaks (two React copies).
66
+ expect(r.js).not.toContain('ReactCurrentOwner');
67
+ // jsx-runtime stays external too — the canvas should import jsxDEV via
68
+ // a normal specifier, not redefine it.
69
+ expect(r.js).not.toContain('function jsxDEV(');
70
+ });
71
+
72
+ test('exposes a default export', async () => {
73
+ const src = 'export default function Q() { return <i />; }\n';
74
+ const abs = await writeTmp('default', src);
75
+ const r = await buildCanvasModule(abs, src);
76
+ expect(r.js).toMatch(/export\s*\{[\s\S]*default[\s\S]*\}/);
77
+ });
78
+ });