@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,215 @@
1
+ /**
2
+ * @file use-snap-guides.tsx — Phase 4.2 snap math + guide-line shapes
3
+ * @scope plugins/design/dev-server/use-snap-guides.tsx
4
+ * @purpose Pure function `computeSnap` that returns a snapped (x, y) for a
5
+ * proposed artboard rect plus the guide lines that should render
6
+ * as visual feedback. No React state; the drag controller calls
7
+ * this on every pointermove tick.
8
+ *
9
+ * Two snap kinds:
10
+ *
11
+ * Grid — every `gridSize` world-units. Applied to top-left corner of
12
+ * `proposed` (left edge for X, top edge for Y).
13
+ * Sibling — alignment with other artboard rects: left↔left, right↔right,
14
+ * left↔right, right↔left, centerX↔centerX on the X-axis (and
15
+ * the matching top/bottom/centerY set on the Y-axis).
16
+ *
17
+ * Tolerance is in world units, not screen pixels (DDR-028) — snap feel stays
18
+ * consistent across zoom levels.
19
+ *
20
+ * The X-axis and Y-axis are independent: a proposed rect can snap X to a
21
+ * sibling edge and Y to a grid line simultaneously. Both guides render.
22
+ *
23
+ * Closest candidate wins per axis (smallest |delta|). When multiple sibling
24
+ * candidates land on the exact same `pos` (e.g. two artboards' right edges
25
+ * stacked at the same x), we render one merged guide whose from/to spans the
26
+ * union of every aligned rect's perpendicular extent.
27
+ *
28
+ * `disabled: true` (Alt held) short-circuits with `{ x, y, guides: [] }`.
29
+ */
30
+
31
+ export interface Rect {
32
+ /** Optional rect id — used by the drag controller to filter self / followers
33
+ * out of the snap candidate set. `computeSnap` itself ignores it. */
34
+ id?: string;
35
+ x: number;
36
+ y: number;
37
+ w: number;
38
+ h: number;
39
+ }
40
+
41
+ export type SnapAxis = 'x' | 'y';
42
+
43
+ export interface SnapGuide {
44
+ /** `"x"` → vertical line (snapping X coord). Line sits at `pos` on X,
45
+ * spans `from..to` on Y. `"y"` is the dual: horizontal line at `pos` on Y,
46
+ * spans `from..to` on X. */
47
+ axis: SnapAxis;
48
+ pos: number;
49
+ from: number;
50
+ to: number;
51
+ }
52
+
53
+ export interface SnapResult {
54
+ /** Possibly-snapped top-left X of the proposed rect (in world coords). */
55
+ x: number;
56
+ /** Possibly-snapped top-left Y. */
57
+ y: number;
58
+ /** Guides to render. 0..2 entries in the common case (one per axis). */
59
+ guides: SnapGuide[];
60
+ }
61
+
62
+ export interface SnapOptions {
63
+ /** World-units between grid lines. Default 40. */
64
+ gridSize: number;
65
+ /** Max world-unit distance at which a candidate is considered "close". */
66
+ tolerance: number;
67
+ /** Skip all snap math + return proposed unchanged (Alt-held bypass). */
68
+ disabled: boolean;
69
+ }
70
+
71
+ interface AxisCandidate {
72
+ /** Shift to apply to proposed on this axis. */
73
+ delta: number;
74
+ /** Position of the guide line in world coords (on the snapped axis). */
75
+ pos: number;
76
+ /** Perpendicular extent — `from..to` of the would-be guide. */
77
+ from: number;
78
+ to: number;
79
+ }
80
+
81
+ function nearestGridDelta(coord: number, gridSize: number, tolerance: number): number | null {
82
+ if (!Number.isFinite(coord) || gridSize <= 0) return null;
83
+ const nearest = Math.round(coord / gridSize) * gridSize;
84
+ const delta = nearest - coord;
85
+ return Math.abs(delta) <= tolerance ? delta : null;
86
+ }
87
+
88
+ function pickClosest(cands: AxisCandidate[]): AxisCandidate | null {
89
+ if (cands.length === 0) return null;
90
+ let best = cands[0] as AxisCandidate;
91
+ let bestAbs = Math.abs(best.delta);
92
+ for (let i = 1; i < cands.length; i++) {
93
+ const c = cands[i] as AxisCandidate;
94
+ const a = Math.abs(c.delta);
95
+ if (a < bestAbs) {
96
+ best = c;
97
+ bestAbs = a;
98
+ }
99
+ }
100
+ return best;
101
+ }
102
+
103
+ /**
104
+ * Among `cands`, find every entry whose `pos` matches `winner.pos` (within a
105
+ * tiny epsilon to absorb floating-point noise) and union their from/to into
106
+ * one guide. Returns the merged guide.
107
+ */
108
+ function mergeAtPos(axis: SnapAxis, winner: AxisCandidate, cands: AxisCandidate[]): SnapGuide {
109
+ let from = winner.from;
110
+ let to = winner.to;
111
+ for (const c of cands) {
112
+ if (Math.abs(c.pos - winner.pos) > 0.001) continue;
113
+ if (c.from < from) from = c.from;
114
+ if (c.to > to) to = c.to;
115
+ }
116
+ return { axis, pos: winner.pos, from, to };
117
+ }
118
+
119
+ export function computeSnap(proposed: Rect, others: Rect[], opts: SnapOptions): SnapResult {
120
+ if (opts.disabled) {
121
+ return { x: proposed.x, y: proposed.y, guides: [] };
122
+ }
123
+ const { gridSize, tolerance } = opts;
124
+ const propLeft = proposed.x;
125
+ const propRight = proposed.x + proposed.w;
126
+ const propCenterX = proposed.x + proposed.w / 2;
127
+ const propTop = proposed.y;
128
+ const propBottom = proposed.y + proposed.h;
129
+ const propCenterY = proposed.y + proposed.h / 2;
130
+
131
+ const xCands: AxisCandidate[] = [];
132
+ const yCands: AxisCandidate[] = [];
133
+
134
+ // Grid candidates — left edge for X, top edge for Y. Perpendicular extent
135
+ // is just the proposed rect's own range (no sibling involved).
136
+ const gridX = nearestGridDelta(propLeft, gridSize, tolerance);
137
+ if (gridX !== null) {
138
+ xCands.push({
139
+ delta: gridX,
140
+ pos: propLeft + gridX,
141
+ from: propTop,
142
+ to: propBottom,
143
+ });
144
+ }
145
+ const gridY = nearestGridDelta(propTop, gridSize, tolerance);
146
+ if (gridY !== null) {
147
+ yCands.push({
148
+ delta: gridY,
149
+ pos: propTop + gridY,
150
+ from: propLeft,
151
+ to: propRight,
152
+ });
153
+ }
154
+
155
+ // Sibling candidates.
156
+ for (const other of others) {
157
+ const oLeft = other.x;
158
+ const oRight = other.x + other.w;
159
+ const oCenterX = other.x + other.w / 2;
160
+ const oTop = other.y;
161
+ const oBottom = other.y + other.h;
162
+ const oCenterY = other.y + other.h / 2;
163
+
164
+ // X-axis pairs: (propCoord, otherCoord).
165
+ const xPairs: Array<[number, number]> = [
166
+ [propLeft, oLeft],
167
+ [propRight, oRight],
168
+ [propLeft, oRight],
169
+ [propRight, oLeft],
170
+ [propCenterX, oCenterX],
171
+ ];
172
+ for (const [propCoord, otherCoord] of xPairs) {
173
+ const delta = otherCoord - propCoord;
174
+ if (Math.abs(delta) > tolerance) continue;
175
+ xCands.push({
176
+ delta,
177
+ pos: otherCoord,
178
+ from: Math.min(propTop, oTop),
179
+ to: Math.max(propBottom, oBottom),
180
+ });
181
+ }
182
+
183
+ // Y-axis pairs.
184
+ const yPairs: Array<[number, number]> = [
185
+ [propTop, oTop],
186
+ [propBottom, oBottom],
187
+ [propTop, oBottom],
188
+ [propBottom, oTop],
189
+ [propCenterY, oCenterY],
190
+ ];
191
+ for (const [propCoord, otherCoord] of yPairs) {
192
+ const delta = otherCoord - propCoord;
193
+ if (Math.abs(delta) > tolerance) continue;
194
+ yCands.push({
195
+ delta,
196
+ pos: otherCoord,
197
+ from: Math.min(propLeft, oLeft),
198
+ to: Math.max(propRight, oRight),
199
+ });
200
+ }
201
+ }
202
+
203
+ const winX = pickClosest(xCands);
204
+ const winY = pickClosest(yCands);
205
+
206
+ const guides: SnapGuide[] = [];
207
+ if (winX) guides.push(mergeAtPos('x', winX, xCands));
208
+ if (winY) guides.push(mergeAtPos('y', winY, yCands));
209
+
210
+ return {
211
+ x: proposed.x + (winX ? winX.delta : 0),
212
+ y: proposed.y + (winY ? winY.delta : 0),
213
+ guides,
214
+ };
215
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @file use-tool-mode.tsx — Phase 4.1 tool-mode store
3
+ * @scope plugins/design/dev-server/use-tool-mode.tsx
4
+ * @purpose Context + hook for the active canvas tool. Wired into
5
+ * DesignCanvas. Phase 5 will
6
+ * register additional tools (pen, circle, arrow, eraser) via
7
+ * the same provider — the API is intentionally open.
8
+ *
9
+ * The router's `onTool` callback (input-router.tsx) writes into this store.
10
+ * The ToolPalette + cursor sync read from it. Selecting a tool also mutates
11
+ * `document.body.style.cursor` so the affordance matches across the iframe.
12
+ */
13
+
14
+ import {
15
+ type ReactNode,
16
+ createContext,
17
+ useCallback,
18
+ useContext,
19
+ useEffect,
20
+ useMemo,
21
+ useState,
22
+ } from 'react';
23
+
24
+ import type { Tool } from './input-router.tsx';
25
+
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ // Types
28
+
29
+ export interface ToolDescriptor {
30
+ id: Tool;
31
+ label: string;
32
+ /** Letter-key shortcut shown in the palette tooltip. */
33
+ shortcut: string;
34
+ /** CSS cursor value applied to <body> when this tool is active. */
35
+ cursor: string;
36
+ }
37
+
38
+ export const DEFAULT_TOOLS: readonly ToolDescriptor[] = Object.freeze([
39
+ { id: 'move', label: 'Move', shortcut: 'V', cursor: 'default' },
40
+ { id: 'hand', label: 'Hand', shortcut: 'H', cursor: 'grab' },
41
+ { id: 'comment', label: 'Comment', shortcut: 'C', cursor: 'crosshair' },
42
+ // Phase 5 — draw / annotation tools. Cursors stay as `crosshair` for pen /
43
+ // rect / arrow (the pen-tip glyph is reserved for the system text caret).
44
+ // Eraser uses `cell` as the closest cross-browser substitute for a rubber
45
+ // affordance (no native rubber cursor exists).
46
+ { id: 'pen', label: 'Pen', shortcut: 'B', cursor: 'crosshair' },
47
+ { id: 'rect', label: 'Rect', shortcut: 'R', cursor: 'crosshair' },
48
+ { id: 'ellipse', label: 'Ellipse', shortcut: 'O', cursor: 'crosshair' },
49
+ { id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: 'crosshair' },
50
+ { id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: 'cell' },
51
+ ]);
52
+
53
+ interface ToolContextValue {
54
+ tool: Tool;
55
+ setTool: (t: Tool) => void;
56
+ tools: readonly ToolDescriptor[];
57
+ }
58
+
59
+ const ToolContext = createContext<ToolContextValue | null>(null);
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // Provider
63
+
64
+ export function ToolProvider({
65
+ children,
66
+ tools = DEFAULT_TOOLS,
67
+ initial = 'move',
68
+ }: {
69
+ children: ReactNode;
70
+ tools?: readonly ToolDescriptor[];
71
+ initial?: Tool;
72
+ }) {
73
+ const [tool, setToolState] = useState<Tool>(initial);
74
+ const setTool = useCallback((t: Tool) => setToolState(t), []);
75
+
76
+ // Body cursor sync — applied to the canvas iframe's body (this hook runs
77
+ // inside the iframe context). The viewport-controller still owns the
78
+ // grabbing/grab cursor swap during space-pan; this only sets the resting
79
+ // cursor for the active tool.
80
+ useEffect(() => {
81
+ if (typeof document === 'undefined') return;
82
+ const desc = tools.find((t) => t.id === tool);
83
+ if (!desc) return;
84
+ const prev = document.body.style.cursor;
85
+ document.body.style.cursor = desc.cursor;
86
+ return () => {
87
+ document.body.style.cursor = prev;
88
+ };
89
+ }, [tool, tools]);
90
+
91
+ const value = useMemo<ToolContextValue>(() => ({ tool, setTool, tools }), [tool, setTool, tools]);
92
+
93
+ return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
94
+ }
95
+
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+ // Hook
98
+
99
+ export function useToolMode(): ToolContextValue {
100
+ const ctx = useContext(ToolContext);
101
+ if (!ctx) {
102
+ throw new Error('useToolMode must be used inside <ToolProvider>');
103
+ }
104
+ return ctx;
105
+ }
106
+
107
+ /**
108
+ * Read-only variant — returns `null` when no provider mounted. Used by
109
+ * components that can render outside a ToolProvider tree (the input
110
+ * router's optional path).
111
+ */
112
+ export function useToolModeOptional(): ToolContextValue | null {
113
+ return useContext(ToolContext);
114
+ }
@@ -0,0 +1,90 @@
1
+ // Bun.serve native WebSocket handlers — replaces server.mjs's hand-rolled
2
+ // RFC-6455 upgrade. Per-connection state lives on ws.data (Bun's typed slot).
3
+
4
+ import type { ServerWebSocket, WebSocketHandler } from 'bun';
5
+
6
+ import type { Api } from './api.ts';
7
+ import type { Context } from './context.ts';
8
+ import { createHmrBroadcaster } from './hmr-broadcast.ts';
9
+ import type { Inspect } from './inspect.ts';
10
+
11
+ export interface WsData {
12
+ id: string;
13
+ remote: string;
14
+ }
15
+
16
+ export interface Ws {
17
+ handler: WebSocketHandler<WsData>;
18
+ broadcast(payload: unknown): void;
19
+ clientCount(): number;
20
+ }
21
+
22
+ export function createWs(ctx: Context, api: Api, inspect: Inspect): Ws {
23
+ const clients = new Set<ServerWebSocket<WsData>>();
24
+
25
+ function send(ws: ServerWebSocket<WsData>, payload: unknown) {
26
+ try {
27
+ ws.send(typeof payload === 'string' ? payload : JSON.stringify(payload));
28
+ } catch {
29
+ /* dead socket — close handler will clean up */
30
+ }
31
+ }
32
+
33
+ function broadcast(payload: unknown) {
34
+ const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
35
+ for (const ws of clients) send(ws, msg);
36
+ }
37
+
38
+ // Wire bus -> WS broadcasts. inspect.ts emits 'selected' / 'active' after every
39
+ // state write; fs-watch.ts emits 'fs:*' on every save.
40
+ ctx.bus.on('selected', (sel) => broadcast({ type: 'selected', selected: sel }));
41
+ ctx.bus.on('active', (file) => broadcast({ type: 'active', file }));
42
+ ctx.bus.on('fs:html', (file) => broadcast({ type: 'fs:html', file }));
43
+ ctx.bus.on('fs:css', (file) => broadcast({ type: 'fs:css', file }));
44
+ ctx.bus.on('fs:json', (file) => broadcast({ type: 'fs:json', file }));
45
+ ctx.bus.on('comments', ({ file, comments }: { file: string; comments: unknown[] }) =>
46
+ broadcast({ type: 'comments', file, comments })
47
+ );
48
+
49
+ // HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
50
+ // The iframe-side client (in _shell.html) decides reload strategy from `mode`.
51
+ createHmrBroadcaster(ctx, (msg) => broadcast(msg));
52
+
53
+ const handler: WebSocketHandler<WsData> = {
54
+ open(ws) {
55
+ clients.add(ws);
56
+ send(ws, { type: 'snapshot', state: inspect.state });
57
+ },
58
+ close(ws) {
59
+ clients.delete(ws);
60
+ },
61
+ async message(ws, raw) {
62
+ // biome-ignore lint/suspicious/noExplicitAny: JSON.parse result; narrowed by runtime discriminator checks below.
63
+ let msg: any;
64
+ try {
65
+ msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw));
66
+ } catch {
67
+ return;
68
+ }
69
+ if (!msg || typeof msg !== 'object') return;
70
+ try {
71
+ if (msg.type === 'active' && typeof msg.file === 'string') inspect.setActive(msg.file);
72
+ else if (msg.type === 'tabs' && Array.isArray(msg.tabs)) inspect.setOpenTabs(msg.tabs);
73
+ else if (msg.type === 'select' && msg.selection) inspect.setSelected(msg.selection);
74
+ else if (msg.type === 'clear-select') inspect.setSelected(null);
75
+ else if (msg.type === 'comments-add' && msg.payload) await api.commentsAdd(msg.payload);
76
+ else if (msg.type === 'comments-patch' && msg.id)
77
+ await api.commentsPatch(msg.id, msg.patch || {});
78
+ else if (msg.type === 'comments-delete' && msg.id) await api.commentsDelete(msg.id);
79
+ else if (msg.type === 'comments-request' && typeof msg.file === 'string') {
80
+ const comments = await api.loadCommentsForFile(msg.file);
81
+ send(ws, { type: 'comments', file: msg.file, comments });
82
+ }
83
+ } catch (err) {
84
+ console.error('[ws] message handler threw:', err);
85
+ }
86
+ },
87
+ };
88
+
89
+ return { handler, broadcast, clientCount: () => clients.size };
90
+ }
@@ -0,0 +1,177 @@
1
+ <!doctype html>
2
+ <!--
3
+ _shell.html — shared canvas mount harness (DDR-019, Phase 3.6).
4
+
5
+ How it works:
6
+ 1. The dev-server's TSX-canvas iframe loads this file with
7
+ ?canvas=<path-relative-to-designRoot>
8
+ (e.g. ?canvas=ui/Docs%20Site.tsx).
9
+ 2. The importmap below resolves "react", "react-dom/client",
10
+ "react/jsx-runtime", "react/jsx-dev-runtime" against the four
11
+ /_canvas-runtime/*.js bundles served by the dev-server. The browser
12
+ only fetches each bundle once — every canvas in the session shares them.
13
+ 3. The canvas TSX module is fetched from the same path under designRoot.
14
+ The dev-server's /<designRel>/ui/<slug>.tsx route runs canvas-pipeline
15
+ (data-cd-id injection) then Bun.build (browser-loadable ESM) and serves
16
+ the result with Content-Type application/javascript.
17
+ 4. We grab the default export, wrap it in React's createRoot, and render
18
+ into <div id="canvas-root">. The token + component CSS files load
19
+ cooperatively — same href as for legacy .html canvases, so the visual
20
+ contract is identical.
21
+
22
+ This file is part of the plugin distribution; the dev-server serves it
23
+ directly from plugins/design/templates/. It is intentionally NOT copied
24
+ into <designRoot> — the dev-server is its single source of truth.
25
+ -->
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8" />
29
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
30
+ <title>Canvas</title>
31
+ <style>
32
+ :root {
33
+ color-scheme: light dark;
34
+ }
35
+ html, body, #canvas-root { height: 100%; margin: 0; }
36
+ body { background: var(--u-bg-0, #fff); }
37
+ #canvas-mount-error {
38
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
39
+ padding: 16px 20px;
40
+ color: #b91c1c;
41
+ background: #fff5f5;
42
+ white-space: pre-wrap;
43
+ }
44
+ #canvas-mount-error:empty { display: none; }
45
+ </style>
46
+ <!-- Tokens + DS component CSS — loaded by the host app via the canvas's
47
+ .meta.json (set later via window.parent postMessage, OR by the dev-
48
+ server when it knows the active DS). For now we load both common
49
+ paths; one will 404 silently per DS depending on layout. -->
50
+ <link id="canvas-tokens" rel="stylesheet" href="" data-canvas-css="tokens" />
51
+ <link id="canvas-components" rel="stylesheet" href="" data-canvas-css="components" />
52
+ <script type="importmap">
53
+ {
54
+ "imports": {
55
+ "react": "/_canvas-runtime/react.js",
56
+ "react-dom": "/_canvas-runtime/react-dom.js",
57
+ "react-dom/client": "/_canvas-runtime/react-dom_client.js",
58
+ "react/jsx-runtime": "/_canvas-runtime/react_jsx-runtime.js",
59
+ "react/jsx-dev-runtime": "/_canvas-runtime/react_jsx-dev-runtime.js",
60
+ "pixi.js": "/_canvas-runtime/pixi-js.js"
61
+ }
62
+ }
63
+ </script>
64
+ </head>
65
+ <body>
66
+ <div id="canvas-root"></div>
67
+ <pre id="canvas-mount-error"></pre>
68
+
69
+ <script type="module">
70
+ const params = new URLSearchParams(location.search);
71
+ const canvasRel = params.get('canvas');
72
+ const designRel = params.get('designRel') || '.design';
73
+ const tokensRel = params.get('tokens') || '';
74
+ const componentsRel = params.get('components') || '';
75
+ const layoutRel = params.get('layout') || '';
76
+
77
+ function showError(msg) {
78
+ const el = document.getElementById('canvas-mount-error');
79
+ if (el) el.textContent = msg;
80
+ console.error('[canvas-shell]', msg);
81
+ }
82
+
83
+ // ---------------------------------------------------------------------
84
+ // HMR client — listens for `canvas-hmr` messages on /_ws. Three modes:
85
+ // - css: swap <link> href with ?v=<version> cache bust.
86
+ // - module: location.reload() — the canvas module re-fetches.
87
+ // - hard: location.reload() — _lib/* changed, everything re-bundles.
88
+ // The WS connection retries on close (dev-server restart resilience).
89
+ function connectHmr() {
90
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
91
+ const ws = new WebSocket(proto + '://' + location.host + '/_ws');
92
+ ws.addEventListener('message', (ev) => {
93
+ try {
94
+ const msg = JSON.parse(ev.data);
95
+ if (!msg || msg.type !== 'canvas-hmr') return;
96
+ if (msg.mode === 'css') {
97
+ const v = msg.version || Date.now();
98
+ // Match by exact filename when we have one; otherwise refresh all.
99
+ const targetFile = (msg.file || '').split('/').pop();
100
+ for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
101
+ const href = link.getAttribute('href') || '';
102
+ if (!targetFile || href.includes(targetFile)) {
103
+ const base = href.split('?')[0];
104
+ link.setAttribute('href', base + '?v=' + v);
105
+ }
106
+ }
107
+ } else if (msg.mode === 'module' || msg.mode === 'hard') {
108
+ // Only reload when the change touches *this* canvas (or `_lib`).
109
+ if (msg.mode === 'hard' || !msg.file || (canvasRel && msg.file === canvasRel)) {
110
+ location.reload();
111
+ }
112
+ }
113
+ } catch (e) {
114
+ console.warn('[canvas-shell] HMR message parse error', e);
115
+ }
116
+ });
117
+ ws.addEventListener('close', () => {
118
+ // Reconnect after a beat — dev-server may have restarted.
119
+ setTimeout(connectHmr, 750);
120
+ });
121
+ }
122
+ connectHmr();
123
+
124
+ if (!canvasRel) {
125
+ showError('Missing ?canvas= query parameter. Add ?canvas=ui/<slug>.tsx.');
126
+ } else {
127
+ if (tokensRel) document.getElementById('canvas-tokens').href = '/' + designRel + '/' + tokensRel;
128
+ if (componentsRel) document.getElementById('canvas-components').href = '/' + designRel + '/' + componentsRel;
129
+ if (layoutRel) {
130
+ // Inject a third <link> for the DS layout chrome (specimens load this).
131
+ const layoutLink = document.createElement('link');
132
+ layoutLink.rel = 'stylesheet';
133
+ layoutLink.href = '/' + designRel + '/' + layoutRel;
134
+ layoutLink.dataset.canvasCss = 'layout';
135
+ document.head.appendChild(layoutLink);
136
+ }
137
+
138
+ const canvasUrl = '/' + designRel + '/' + canvasRel;
139
+ // Phase 4 T5 — read the canvas's sibling .meta.json and stash it on
140
+ // window so DesignCanvas can seed `layout` + `viewport` synchronously.
141
+ // The fetch runs in parallel with imports; failure is non-fatal (the
142
+ // canvas falls through to default grid + fit-to-screen).
143
+ const metaFileParam = encodeURIComponent(designRel + '/' + canvasRel);
144
+ const metaFetch = fetch('/_api/canvas-meta?file=' + metaFileParam, {
145
+ headers: { 'Cache-Control': 'no-store' },
146
+ })
147
+ .then((r) => (r.ok ? r.json() : null))
148
+ .then((j) => {
149
+ window.__canvas_meta__ = (j && typeof j === 'object') ? j : {};
150
+ window.__canvas_meta_file__ = designRel + '/' + canvasRel;
151
+ })
152
+ .catch(() => {
153
+ window.__canvas_meta__ = {};
154
+ window.__canvas_meta_file__ = designRel + '/' + canvasRel;
155
+ });
156
+ try {
157
+ const [{ createRoot }, mod, react] = await Promise.all([
158
+ import('react-dom/client'),
159
+ import(canvasUrl),
160
+ import('react'),
161
+ metaFetch,
162
+ ]);
163
+ const Canvas = mod.default;
164
+ if (typeof Canvas !== 'function') {
165
+ throw new Error(
166
+ 'Canvas module at ' + canvasUrl + ' has no default export (or it is not a component).'
167
+ );
168
+ }
169
+ const root = createRoot(document.getElementById('canvas-root'));
170
+ root.render(react.createElement(Canvas));
171
+ } catch (err) {
172
+ showError((err && err.stack) || String(err));
173
+ }
174
+ }
175
+ </script>
176
+ </body>
177
+ </html>
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @canvas {{NAME}} — {{SUBTITLE}}
3
+ * @ds {{DS_NAME}}
4
+ * @platform {{PLATFORM}}
5
+ * @opt_out {{OPT_OUT_SCOPE}}
6
+ * @artboards {{ARTBOARDS}}
7
+ * @brief {{BRIEF}}
8
+ * @stack React 19 · TSX · Bun.build · css_mode={{CSS_MODE}}
9
+ * @history {{HISTORY_DIR}}
10
+ * @handoff bunx shadcn add file://./{{NAME}}.registry.json
11
+ *
12
+ * Authored under {{DS_NAME}}. Tokens + shared component classes load via the
13
+ * dev-server's _shell.html harness — link tags arrive automatically from the
14
+ * iframe's ?tokens= / ?components= query. Class names match the DS's
15
+ * `_components.css` (`.btn`, `.tile`, `.sku`, `.seg`, ...). Bespoke classes go
16
+ * into a sibling .module.css iff `css_mode === "modules"`; for `inline` (the
17
+ * MDCC-DSN/01 default) prefer existing classes + `style={{}}` for arbitrary
18
+ * one-off values.
19
+ *
20
+ * The envelope primitives (`DesignCanvas`, `DCSection`, `DCArtboard`) and any
21
+ * specimen helpers come from the dev-server-bundled canvas library via the
22
+ * virtual specifier — the dev-server resolves it to
23
+ * `plugins/design/dev-server/canvas-lib.tsx` (single source, ships with the
24
+ * dev-server install per DDR-025), and `/design:handoff` AST-inlines the used
25
+ * exports so the registry-item drop is self-contained. Read that file for the
26
+ * full export surface (helpers + hooks) before reaching for new components.
27
+ *
28
+ * `data-cd-id="<8 hex>"` attributes are injected at request time by the dev
29
+ * server (canvas-pipeline.ts pass 1). Don't author them by hand — they'd be
30
+ * stripped + replaced. `/design:handoff` strips them again before the registry
31
+ * sidecar leaves dev (production has no business with dev-time scaffolding).
32
+ */
33
+
34
+ import { useState } from "react";
35
+
36
+ import { DesignCanvas, DCSection, DCArtboard } from "@maude/canvas-lib";
37
+
38
+ export default function {{COMPONENT_NAME}}() {
39
+ return (
40
+ <DesignCanvas>
41
+ <DCSection id="overview" title="{{SUBTITLE}}">
42
+ <DCArtboard id="primary" label="A · primary" width={1280} height={800}>
43
+ <div className="mdcc" data-theme="{{THEME_DEFAULT}}">
44
+ {/* Replace this scaffold with the actual canvas content. The
45
+ envelope this template was rendered against is in
46
+ {{HISTORY_DIR}}/000-envelope.md. */}
47
+ <h1 className="sku">{{NAME}}</h1>
48
+ <p>{{BRIEF}}</p>
49
+ </div>
50
+ </DCArtboard>
51
+ </DCSection>
52
+ </DesignCanvas>
53
+ );
54
+ }