@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,365 @@
1
+ // Active-canvas state, selected-element tracking, and HTML injection
2
+ // (inspector overlay + canvas runtime). See plan Task 7 + DDR-007.
3
+
4
+ import path from 'node:path';
5
+ import type { Context } from './context.ts';
6
+
7
+ export interface SelectedElement {
8
+ file: string;
9
+ /** CSS-selector path (v1 anchor). Always present for backwards-compat + legacy HTML canvases. */
10
+ selector: string;
11
+ tag: string;
12
+ classes: string;
13
+ text: string;
14
+ dom_path: string[];
15
+ bounds: { x: number; y: number; w: number; h: number } | null;
16
+ html: string;
17
+ ts: string;
18
+ /**
19
+ * Schema version. v2 = TSX canvas with a `data-cd-id` anchor at click target
20
+ * (or any ancestor — script walks via `closest()`); v1 = no `data-cd-id`
21
+ * anywhere (legacy `.html` canvases, or click on shell chrome of a TSX
22
+ * canvas). Readers must accept both during the grace window.
23
+ */
24
+ v: 1 | 2;
25
+ /** Stable per-element id from canvas-pipeline two-pass transform. Present only when v === 2. */
26
+ id?: string;
27
+ /**
28
+ * Canvas slug — POSIX, extension-less, relative to designRoot. Matches
29
+ * `_locator.json` top-level keys. Present only when v === 2. The inspector
30
+ * derives it server-side from `file` (stripping `<designRoot>/` prefix + `.tsx`).
31
+ */
32
+ canvas?: string;
33
+ }
34
+
35
+ /**
36
+ * Phase 4.1: `selected` widens from `SelectedElement | null` to
37
+ * `SelectedElement | SelectedElement[] | null` for multi-select via canvas-shell
38
+ * input router. Readers must accept all three shapes. Writer below emits a
39
+ * single object when cardinality is 1 (back-compat with `/design:edit` +
40
+ * downstream tools that read the legacy shape) and an array for N > 1.
41
+ */
42
+ export type SelectedValue = SelectedElement | SelectedElement[] | null;
43
+
44
+ export interface ActiveState {
45
+ active: string | null;
46
+ open_tabs: string[];
47
+ selected: SelectedValue;
48
+ last_change: string | null;
49
+ session_started: string;
50
+ active_comments?: unknown[];
51
+ }
52
+
53
+ type SetSelectedInput =
54
+ | Omit<SelectedElement, 'ts' | 'v' | 'canvas'>
55
+ | Array<Omit<SelectedElement, 'ts' | 'v' | 'canvas'>>
56
+ | null;
57
+
58
+ export interface Inspect {
59
+ state: ActiveState;
60
+ load(): Promise<void>;
61
+ setActive(file: string): void;
62
+ setOpenTabs(tabs: string[]): void;
63
+ setSelected(sel: SetSelectedInput): void;
64
+ save(): Promise<void>;
65
+ injectInspector(html: string): string;
66
+ }
67
+
68
+ const NEW = (): ActiveState => ({
69
+ active: null,
70
+ open_tabs: [],
71
+ selected: null,
72
+ last_change: null,
73
+ session_started: new Date().toISOString(),
74
+ });
75
+
76
+ export function createInspect(
77
+ ctx: Context,
78
+ loadActiveComments: (file: string) => Promise<unknown[]>
79
+ ): Inspect {
80
+ const state: ActiveState = NEW();
81
+ let saveQueued = false;
82
+
83
+ async function save() {
84
+ saveQueued = false;
85
+ try {
86
+ // Bun.write creates parent dirs automatically — no .keep poke needed.
87
+ let active_comments: unknown[] = [];
88
+ if (state.active) {
89
+ try {
90
+ active_comments = await loadActiveComments(state.active);
91
+ } catch {
92
+ /* ignore */
93
+ }
94
+ }
95
+ const enriched = { ...state, active_comments };
96
+ await Bun.write(ctx.paths.activeFile, JSON.stringify(enriched, null, 2));
97
+ } catch (e) {
98
+ const msg = e instanceof Error ? e.message : String(e);
99
+ console.error(' warn: failed to save _active.json:', msg);
100
+ }
101
+ }
102
+
103
+ function scheduleSave() {
104
+ if (saveQueued) return;
105
+ saveQueued = true;
106
+ queueMicrotask(save);
107
+ }
108
+
109
+ async function load() {
110
+ try {
111
+ const raw = await Bun.file(ctx.paths.activeFile).text();
112
+ const prev = JSON.parse(raw);
113
+ Object.assign(state, prev, { session_started: new Date().toISOString() });
114
+ } catch {
115
+ // first boot
116
+ }
117
+ }
118
+
119
+ function setActive(file: string) {
120
+ if (typeof file !== 'string') return;
121
+ if (state.active === file) return;
122
+ state.active = file || null;
123
+ state.selected = null;
124
+ state.last_change = new Date().toISOString();
125
+ scheduleSave();
126
+ ctx.bus.emit('active', state.active);
127
+ }
128
+
129
+ function setOpenTabs(tabs: string[]) {
130
+ if (!Array.isArray(tabs)) return;
131
+ state.open_tabs = tabs.filter((t): t is string => typeof t === 'string');
132
+ state.last_change = new Date().toISOString();
133
+ scheduleSave();
134
+ }
135
+
136
+ function enrich(sel: Omit<SelectedElement, 'ts' | 'v' | 'canvas'>): SelectedElement {
137
+ const file = typeof sel.file === 'string' ? sel.file : (state.active ?? '');
138
+ const id = typeof sel.id === 'string' && sel.id ? sel.id : undefined;
139
+ const v: 1 | 2 = id ? 2 : 1;
140
+ return {
141
+ file,
142
+ selector: String(sel.selector || ''),
143
+ tag: String(sel.tag || ''),
144
+ classes: String(sel.classes || ''),
145
+ text: String(sel.text || '').slice(0, 240),
146
+ dom_path: Array.isArray(sel.dom_path) ? sel.dom_path.slice(0, 16) : [],
147
+ bounds: sel.bounds ?? null,
148
+ html: String(sel.html || '').slice(0, 4000),
149
+ ts: new Date().toISOString(),
150
+ v,
151
+ ...(id ? { id, canvas: deriveCanvasSlug(file) } : {}),
152
+ };
153
+ }
154
+
155
+ function setSelected(sel: SetSelectedInput) {
156
+ if (sel == null) {
157
+ state.selected = null;
158
+ } else if (Array.isArray(sel)) {
159
+ const enriched = sel
160
+ .filter(
161
+ (s): s is Omit<SelectedElement, 'ts' | 'v' | 'canvas'> => !!s && typeof s === 'object'
162
+ )
163
+ .map(enrich);
164
+ // Writer back-compat: collapse single-entry array to a bare object so
165
+ // legacy readers (`/design:edit`, handoff tooling) keep working without
166
+ // schema awareness. N>1 stays as an array.
167
+ if (enriched.length === 0) state.selected = null;
168
+ else if (enriched.length === 1) state.selected = enriched[0] ?? null;
169
+ else state.selected = enriched;
170
+ } else if (typeof sel === 'object') {
171
+ state.selected = enrich(sel);
172
+ } else {
173
+ state.selected = null;
174
+ }
175
+ state.last_change = new Date().toISOString();
176
+ scheduleSave();
177
+ ctx.bus.emit('selected', state.selected);
178
+ }
179
+
180
+ /**
181
+ * Canvas slug for v2 selections. Mirrors `canvasSlug()` from locator.ts but
182
+ * accepts a designRoot-relative `file` path (which is what the iframe reports)
183
+ * rather than an absolute one. Strips a leading `<designRoot-relative>/` if
184
+ * present and strips the final extension.
185
+ */
186
+ function deriveCanvasSlug(file: string): string {
187
+ let s = (file || '').replace(/^\/+/, '');
188
+ // Strip a leading designRoot prefix if it's part of the file path. The
189
+ // iframe's pathname includes the design root (e.g. `.design/ui/Foo.tsx`);
190
+ // locator.ts strips it via path.relative — mirror that here.
191
+ const dr = ctx.paths.designRel.replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
192
+ if (dr && s.startsWith(`${dr}/`)) s = s.slice(dr.length + 1);
193
+ const dot = s.lastIndexOf('.');
194
+ return dot > 0 ? s.slice(0, dot) : s;
195
+ }
196
+
197
+ return {
198
+ state,
199
+ load,
200
+ setActive,
201
+ setOpenTabs,
202
+ setSelected,
203
+ save,
204
+ injectInspector,
205
+ };
206
+ }
207
+
208
+ // ---------- Inspector script injection ----------
209
+
210
+ function injectInspector(html: string): string {
211
+ const idx = html.lastIndexOf('</body>');
212
+ if (idx === -1) return html + INSPECTOR_SCRIPT;
213
+ return html.slice(0, idx) + INSPECTOR_SCRIPT + html.slice(idx);
214
+ }
215
+
216
+ // Comment-pin rendering overlay injected into every served HTML page under
217
+ // designRoot. Pin layer is the ONLY responsibility — hover/click selection is
218
+ // owned by canvas-shell.tsx (TSX canvases) and isn't applicable to legacy
219
+ // `.html` mocks since the broader migration to TSX. Pin layer keeps working
220
+ // in both, because pins are positioned by selector and updated via the
221
+ // `comments-set` postMessage channel from the shell.
222
+ const INSPECTOR_SCRIPT = `
223
+ <script>
224
+ (function() {
225
+ if (window.__designInspectorAttached) return;
226
+ window.__designInspectorAttached = true;
227
+
228
+ var styleEl = document.createElement('style');
229
+ styleEl.textContent = [
230
+ '.dgn-pin { position: absolute; top: 0; left: 0; z-index: 2147483646; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 999px 999px 999px 4px; background: #facc15; color: #1c1917; font: 600 11px/22px ui-sans-serif, system-ui, sans-serif; text-align: center; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.4); transition: filter 120ms; transform-origin: bottom left; will-change: transform; }',
231
+ '.dgn-pin:hover { filter: brightness(1.1); outline: 2px solid rgba(0,0,0,0.3); }',
232
+ '.dgn-pin.resolved { background: #22c55e; color: #052e16; }',
233
+ '.dgn-pin.focused { box-shadow: 0 4px 12px rgba(0,0,0,0.6), 0 0 0 2px #fff; outline: 2px solid #fff; }'
234
+ ].join('\\n');
235
+ document.documentElement.appendChild(styleEl);
236
+
237
+ var pinLayer = document.createElement('div');
238
+ pinLayer.id = 'dgn-pin-layer';
239
+ pinLayer.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483646;';
240
+ document.documentElement.appendChild(pinLayer);
241
+
242
+ var commentsCache = [];
243
+ var focusedPinId = null;
244
+
245
+ var pinNodes = [];
246
+ var rafToken = null;
247
+
248
+ function buildPinNodes() {
249
+ pinLayer.innerHTML = '';
250
+ pinNodes = [];
251
+ var withSelector = commentsCache.filter(function(c) { return c && c.selector; });
252
+ withSelector.forEach(function(c, i) {
253
+ var target = null;
254
+ try { target = document.querySelector(c.selector); } catch (e) {}
255
+ var pin = document.createElement('button');
256
+ pin.className = 'dgn-pin' + (c.status === 'resolved' ? ' resolved' : '') + (c.id === focusedPinId ? ' focused' : '');
257
+ pin.textContent = String(i + 1);
258
+ pin.title = (c.text || '').slice(0, 200);
259
+ pin.style.pointerEvents = 'auto';
260
+ pin.style.left = '0px';
261
+ pin.style.top = '0px';
262
+ pin.dataset.id = c.id;
263
+ pin.addEventListener('click', function(ev) {
264
+ ev.preventDefault();
265
+ ev.stopPropagation();
266
+ focusedPinId = c.id;
267
+ try { window.parent.postMessage({ dgn: 'comment-click', id: c.id }, '*'); } catch (e) {}
268
+ buildPinNodes();
269
+ });
270
+ pinLayer.appendChild(pin);
271
+ pinNodes.push({ el: pin, comment: c, target: target });
272
+ });
273
+ placePins();
274
+ }
275
+
276
+ function placePins() {
277
+ if (!pinNodes.length) return;
278
+ for (var i = 0; i < pinNodes.length; i++) {
279
+ var node = pinNodes[i];
280
+ var x, y, hidden = false;
281
+ if (!node.target || !node.target.isConnected) {
282
+ try { node.target = document.querySelector(node.comment.selector); } catch (e) {}
283
+ }
284
+ if (node.target) {
285
+ var r = node.target.getBoundingClientRect();
286
+ x = r.left + window.scrollX - 8;
287
+ y = r.top + window.scrollY - 8;
288
+ if (r.width === 0 && r.height === 0) hidden = true;
289
+ } else if (node.comment.bounds) {
290
+ x = node.comment.bounds.x - 8;
291
+ y = node.comment.bounds.y - 8;
292
+ } else {
293
+ hidden = true;
294
+ }
295
+ if (hidden) {
296
+ node.el.style.display = 'none';
297
+ } else {
298
+ node.el.style.display = '';
299
+ var scale = (node.comment.id === focusedPinId) ? 1.2 : 1;
300
+ node.el.style.transform = 'translate(' + Math.round(x) + 'px, ' + Math.round(y) + 'px) scale(' + scale + ')';
301
+ }
302
+ }
303
+ }
304
+
305
+ function tick() {
306
+ rafToken = null;
307
+ placePins();
308
+ if (pinNodes.length) rafToken = requestAnimationFrame(tick);
309
+ }
310
+ function startTick() {
311
+ if (rafToken == null && pinNodes.length) rafToken = requestAnimationFrame(tick);
312
+ }
313
+
314
+ function schedulePins() { buildPinNodes(); startTick(); }
315
+
316
+ window.addEventListener('resize', placePins);
317
+ document.addEventListener('scroll', placePins, { passive: true, capture: true });
318
+ document.addEventListener('wheel', function() { startTick(); }, { passive: true, capture: true });
319
+ document.addEventListener('pointermove', function(e) { if (e.buttons) startTick(); }, { passive: true, capture: true });
320
+ document.addEventListener('keyup', function() { startTick(); }, true);
321
+ if (typeof MutationObserver !== 'undefined') {
322
+ new MutationObserver(function(){ startTick(); }).observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['style', 'class'], childList: true });
323
+ }
324
+
325
+ window.addEventListener('message', function(e) {
326
+ var m = e.data;
327
+ if (!m || typeof m !== 'object' || !m.dgn) return;
328
+ if (m.dgn === 'comments-set' && Array.isArray(m.comments)) {
329
+ commentsCache = m.comments;
330
+ schedulePins();
331
+ } else if (m.dgn === 'comment-focus') {
332
+ focusedPinId = m.id || null;
333
+ pinNodes.forEach(function(node) {
334
+ node.el.classList.toggle('focused', node.comment.id === focusedPinId);
335
+ });
336
+ placePins();
337
+ startTick();
338
+ var c = commentsCache.find(function(x){ return x && x.id === m.id; });
339
+ if (c && c.selector) {
340
+ try {
341
+ var t = document.querySelector(c.selector);
342
+ if (t) t.scrollIntoView({ behavior: 'smooth', block: 'center' });
343
+ } catch (e) {}
344
+ }
345
+ }
346
+ /* force-clear is now consumed by canvas-shell.tsx — the inspector
347
+ overlay has no per-element selection state to clear anymore. */
348
+ });
349
+
350
+ try {
351
+ var p = location.pathname;
352
+ var file;
353
+ if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
354
+ var qs = new URLSearchParams(location.search);
355
+ var canvas = qs.get('canvas') || '';
356
+ var designRel = (qs.get('designRel') || '.design').replace(/^\\/+|\\/+$/g, '');
357
+ file = designRel + '/' + canvas;
358
+ } else {
359
+ file = decodeURIComponent(p).replace(/^\\//, '');
360
+ }
361
+ window.parent.postMessage({ dgn: 'loaded', file: file }, '*');
362
+ } catch (e) {}
363
+ })();
364
+ </script>
365
+ `;
@@ -0,0 +1,159 @@
1
+ // LocatorMap reader/writer for <designRoot>/_locator.json (DDR-019, Phase 3.6 Task 2).
2
+ //
3
+ // _locator.json is a multi-canvas map keyed by canvas slug. Each canvas's value
4
+ // is its LocatorMap (data-cd-id -> source location). The same file holds the
5
+ // state for every canvas under the design root, so concurrent transpiles
6
+ // against different canvases share the file but must not corrupt each other.
7
+ //
8
+ // Concurrency strategy: per-path Promise-chained mutex. Each write to a given
9
+ // _locator.json serialises behind the previous write on the same path. Cheap
10
+ // (no OS-level locking) and sufficient because the dev-server is the only
11
+ // writer.
12
+ //
13
+ // On-disk shape:
14
+ // {
15
+ // "ui/Docs Site": {
16
+ // "a1b2c3d4": { canvas, line, col, jsxPath, componentName },
17
+ // ...
18
+ // },
19
+ // "ui/Canvas Viewport": { ... }
20
+ // }
21
+ // Top-level keys are POSIX-style canvas slugs relative to designRoot, ext-less.
22
+
23
+ import path from 'node:path';
24
+
25
+ export interface LocatorEntry {
26
+ /** Absolute path of the canvas .tsx file the ID belongs to. */
27
+ canvas: string;
28
+ /** 1-based line number of the JSXElement's opening tag. */
29
+ line: number;
30
+ /** 0-based column number of the JSXElement's opening tag. */
31
+ col: number;
32
+ /** Breadcrumb of JSX element-type names from component root down. */
33
+ jsxPath: string[];
34
+ /** Enclosing component name (PascalCase fn/arrow), "" if outside any component. */
35
+ componentName: string;
36
+ }
37
+
38
+ export type LocatorMap = Record<string, LocatorEntry>;
39
+ export type LocatorFile = Record<string, LocatorMap>;
40
+
41
+ /**
42
+ * Canvas slug used as the top-level key in _locator.json. Posix-style,
43
+ * extension-less, relative to designRoot. Matches the slug shape that
44
+ * bin/slug.sh produces for `_history/<slug>/`.
45
+ */
46
+ export function canvasSlug(canvasAbsPath: string, designRoot: string): string {
47
+ const rel = path.posix.normalize(
48
+ path.relative(designRoot, canvasAbsPath).split(path.sep).join('/')
49
+ );
50
+ // strip extension
51
+ const dot = rel.lastIndexOf('.');
52
+ return dot > 0 ? rel.slice(0, dot) : rel;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Per-path mutex. Promise chain — each acquire waits on the previous release.
57
+
58
+ const locks = new Map<string, Promise<void>>();
59
+ function withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
60
+ const prev = locks.get(filePath) ?? Promise.resolve();
61
+ let release!: () => void;
62
+ const gate = new Promise<void>((res) => {
63
+ release = res;
64
+ });
65
+ const next = prev.then(() => gate);
66
+ locks.set(filePath, next);
67
+ return prev.then(fn).finally(() => {
68
+ release();
69
+ // Only the last queued acquire clears the map entry; intermediate ones
70
+ // leave it pointing at the still-pending tail.
71
+ if (locks.get(filePath) === next) locks.delete(filePath);
72
+ });
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Reader
77
+
78
+ export async function readLocatorFile(locatorAbsPath: string): Promise<LocatorFile> {
79
+ const f = Bun.file(locatorAbsPath);
80
+ if (!(await f.exists())) return {};
81
+ try {
82
+ const raw = await f.text();
83
+ if (!raw.trim()) return {};
84
+ const parsed = JSON.parse(raw) as unknown;
85
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
86
+ return parsed as LocatorFile;
87
+ }
88
+ return {};
89
+ } catch {
90
+ // Corrupt / mid-write — treat as empty rather than throw. The next write
91
+ // overwrites cleanly.
92
+ return {};
93
+ }
94
+ }
95
+
96
+ export async function readLocator(
97
+ locatorAbsPath: string,
98
+ slug: string
99
+ ): Promise<LocatorMap | null> {
100
+ const file = await readLocatorFile(locatorAbsPath);
101
+ return file[slug] ?? null;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Writer — atomic per-canvas update. Writes to "<file>.tmp.<rand>", then
106
+ // renames over the original. Per-path mutex prevents concurrent writers from
107
+ // trampling each other's slug entries.
108
+
109
+ export function writeLocator(locatorAbsPath: string, slug: string, map: LocatorMap): Promise<void> {
110
+ return withLock(locatorAbsPath, async () => {
111
+ const current = await readLocatorFile(locatorAbsPath);
112
+ const next: LocatorFile = { ...current, [slug]: map };
113
+ const json = stableStringify(next);
114
+ const tmp = `${locatorAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
115
+ await Bun.write(tmp, json);
116
+ // Bun has no direct rename — use node:fs/promises. fsync of the dir is
117
+ // overkill for a dev-tool sidecar; the rename is atomic on POSIX which is
118
+ // all we ship to.
119
+ const { rename } = await import('node:fs/promises');
120
+ await rename(tmp, locatorAbsPath);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Drop a canvas's slug from the locator file (e.g. on canvas deletion).
126
+ * No-op if the slug isn't present.
127
+ */
128
+ export function clearLocatorSlug(locatorAbsPath: string, slug: string): Promise<void> {
129
+ return withLock(locatorAbsPath, async () => {
130
+ const current = await readLocatorFile(locatorAbsPath);
131
+ if (!(slug in current)) return;
132
+ delete current[slug];
133
+ const json = stableStringify(current);
134
+ const tmp = `${locatorAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
135
+ await Bun.write(tmp, json);
136
+ const { rename } = await import('node:fs/promises');
137
+ await rename(tmp, locatorAbsPath);
138
+ });
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Deterministic JSON — sorted keys at every level, 2-space indent, trailing
143
+ // newline. Makes git diffs of _locator.json reviewable (and stable across
144
+ // re-transpiles when source hasn't changed). The replacer sorts; callers do
145
+ // not need to pre-sort.
146
+
147
+ function stableStringify(obj: LocatorFile): string {
148
+ return `${JSON.stringify(obj, sortedSlugKeysReplacer, 2)}\n`;
149
+ }
150
+
151
+ // biome-ignore lint/suspicious/noExplicitAny: JSON.stringify replacer signature
152
+ function sortedSlugKeysReplacer(_key: string, value: any): any {
153
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
154
+ const sorted: Record<string, unknown> = {};
155
+ for (const k of Object.keys(value).sort()) sorted[k] = value[k];
156
+ return sorted;
157
+ }
158
+ return value;
159
+ }
@@ -0,0 +1,97 @@
1
+ // Memory hygiene: FinalizationRegistry-backed iframe-state cleanup +
2
+ // periodic heap-watch. See plan Task 8 + DDR-009.
3
+ //
4
+ // FinalizationRegistry callbacks are not guaranteed — they are the safety net,
5
+ // not the primary path. The primary path is the explicit `iframe:closed` event
6
+ // emitted on the bus, which calls cleanupFn synchronously.
7
+
8
+ const HEAP_WARN_BYTES = 256 * 1024 * 1024; // 256 MB — log only
9
+ const HEAP_PANIC_BYTES = 384 * 1024 * 1024; // 384 MB — force GC
10
+
11
+ let registry: FinalizationRegistry<{ id: string; cleanupFn: () => void }> | null = null;
12
+
13
+ function ensureRegistry(): FinalizationRegistry<{ id: string; cleanupFn: () => void }> {
14
+ if (registry) return registry;
15
+ registry = new FinalizationRegistry((held) => {
16
+ try {
17
+ held.cleanupFn();
18
+ } catch (err) {
19
+ console.error(`[mem] finalize callback for ${held.id} threw:`, err);
20
+ }
21
+ });
22
+ return registry;
23
+ }
24
+
25
+ /**
26
+ * Register a per-iframe cleanup function. The callback fires when the host
27
+ * object is GC'd, OR when the caller explicitly invokes `cleanupFn`.
28
+ */
29
+ export function registerIframe(id: string, host: object, cleanupFn: () => void) {
30
+ ensureRegistry().register(host, { id, cleanupFn });
31
+ }
32
+
33
+ /**
34
+ * Weak-ref'd map. Lookups deref to undefined when the entry has been GC'd.
35
+ * Caller must null-check on every get().
36
+ */
37
+ export class WeakMapById<T extends object> {
38
+ private map = new Map<string, WeakRef<T>>();
39
+
40
+ set(id: string, value: T) {
41
+ this.map.set(id, new WeakRef(value));
42
+ }
43
+
44
+ get(id: string): T | undefined {
45
+ const ref = this.map.get(id);
46
+ if (!ref) return undefined;
47
+ const value = ref.deref();
48
+ if (!value) {
49
+ this.map.delete(id);
50
+ return undefined;
51
+ }
52
+ return value;
53
+ }
54
+
55
+ delete(id: string) {
56
+ this.map.delete(id);
57
+ }
58
+
59
+ /** Sweep dead refs; returns the number of slots reclaimed. */
60
+ sweep(): number {
61
+ let n = 0;
62
+ for (const [id, ref] of this.map) {
63
+ if (!ref.deref()) {
64
+ this.map.delete(id);
65
+ n++;
66
+ }
67
+ }
68
+ return n;
69
+ }
70
+ }
71
+
72
+ let heapTimer: ReturnType<typeof setInterval> | null = null;
73
+
74
+ export function startHeapWatch(intervalMs = 60_000) {
75
+ if (heapTimer) return;
76
+ heapTimer = setInterval(() => {
77
+ const u = process.memoryUsage();
78
+ if (u.heapTotal > HEAP_PANIC_BYTES) {
79
+ console.warn(
80
+ `[mem] heap ${(u.heapTotal / 1024 / 1024).toFixed(0)}MB > panic threshold — forcing GC`
81
+ );
82
+ // Bun.gc(true) is sync; Node has no equivalent without --expose-gc.
83
+ const gc = (globalThis as { Bun?: { gc?: (sync: boolean) => void } }).Bun?.gc;
84
+ if (gc) gc(true);
85
+ } else if (u.heapTotal > HEAP_WARN_BYTES) {
86
+ console.warn(`[mem] heap ${(u.heapTotal / 1024 / 1024).toFixed(0)}MB > warn threshold`);
87
+ }
88
+ }, intervalMs);
89
+ // Don't keep the event loop alive just for the heap watch.
90
+ const t = heapTimer as unknown as { unref?: () => void };
91
+ t.unref?.();
92
+ }
93
+
94
+ export function stopHeapWatch() {
95
+ if (heapTimer) clearInterval(heapTimer);
96
+ heapTimer = null;
97
+ }