@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,147 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Design Plugin — per-repo config",
4
+ "description": "Per-repo configuration consumed by the design plugin's dev-server, slash commands, and skills. Place at <repo>/.design/config.json.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["name", "designRoot"],
8
+ "properties": {
9
+ "name": {
10
+ "type": "string",
11
+ "description": "Short repo / product name. Shown in server header and as a token in skill prompts."
12
+ },
13
+ "projectLabel": {
14
+ "type": "string",
15
+ "description": "Long label for the design browser (window title and big H1). Defaults to <name> + ' Design'."
16
+ },
17
+ "designRoot": {
18
+ "type": "string",
19
+ "description": "Repo-relative root where canvas + design system files live. Default convention: '.design' (top-level, alongside .git, .claude, etc.). Older repos may use '.ai/design'.",
20
+ "default": ".design"
21
+ },
22
+ "canvasGroups": {
23
+ "type": "array",
24
+ "description": "Top-level groupings shown in the file tree. Each entry maps a folder under designRoot to a sidebar section.",
25
+ "items": {
26
+ "type": "object",
27
+ "required": ["label", "path"],
28
+ "properties": {
29
+ "label": { "type": "string" },
30
+ "path": {
31
+ "type": "string",
32
+ "description": "Folder under designRoot to scan for .html canvases."
33
+ }
34
+ },
35
+ "additionalProperties": false
36
+ },
37
+ "default": [
38
+ { "label": "Design system", "path": "system" },
39
+ { "label": "Canvases", "path": "ui" }
40
+ ]
41
+ },
42
+ "rootClass": {
43
+ "type": "string",
44
+ "description": "Body / canvas root CSS class that the project's tokens are scoped under. Generated canvases use <body class=\"<rootClass>\">.",
45
+ "default": "app"
46
+ },
47
+ "themeDefault": {
48
+ "type": "string",
49
+ "enum": ["dark", "light"],
50
+ "description": "Default theme attribute (data-theme) for new canvases.",
51
+ "default": "dark"
52
+ },
53
+ "tokensCssRel": {
54
+ "type": "string",
55
+ "description": "Path to the design system CSS, relative to designRoot. New canvases <link> to this file.",
56
+ "default": "system/colors_and_type.css"
57
+ },
58
+ "teamAccentDefault": {
59
+ "type": ["string", "null"],
60
+ "description": "Optional default value for data-team attribute on new canvases. Set to null if the project doesn't use multi-team accent overrides."
61
+ },
62
+ "handoffTargets": {
63
+ "type": "array",
64
+ "description": "Where /design:handoff emits target artifacts. Empty array disables handoff. Special `path` values: `registry:item` emits a shadcn registry-item.json sidecar next to the canvas (Phase 3.6 default — consume via `bunx shadcn add file://./<Slug>.registry.json`). Any other path is treated as a repo-relative target dir for legacy direct-migration handoff. Recommended single-entry default: `[{ label: 'shadcn registry', path: 'registry:item', platform: 'web' }]`.",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["label", "path"],
68
+ "properties": {
69
+ "label": { "type": "string" },
70
+ "path": {
71
+ "type": "string",
72
+ "description": "`registry:item` for shadcn sidecar emit, or repo-relative target dir for direct migration."
73
+ },
74
+ "platform": { "type": "string", "enum": ["web", "mobile", "desktop", "other"] }
75
+ },
76
+ "additionalProperties": false
77
+ },
78
+ "default": []
79
+ },
80
+ "newCanvasDir": {
81
+ "type": "string",
82
+ "description": "Default folder (under designRoot) where /design:new scaffolds new canvas .tsx files (Phase 3.6+; older repos may still hold .html canvases pending one-shot migration).",
83
+ "default": "ui"
84
+ },
85
+ "newComponentDir": {
86
+ "type": "string",
87
+ "description": "Default folder (under designRoot) where /design:new puts shared component .jsx files.",
88
+ "default": "ui/components"
89
+ },
90
+ "extensions": {
91
+ "type": "array",
92
+ "description": "User-added directories the docs renderer / completeness-critic should track but not validate against the standard schema. Each entry maps a folder (under the DS) to a label.",
93
+ "items": {
94
+ "type": "object",
95
+ "required": ["label", "path"],
96
+ "properties": {
97
+ "label": { "type": "string" },
98
+ "path": { "type": "string" }
99
+ },
100
+ "additionalProperties": false
101
+ },
102
+ "default": []
103
+ },
104
+ "completenessProfile": {
105
+ "type": "string",
106
+ "enum": ["minimal", "standard", "strict"],
107
+ "description": "Severity profile for the design-system-completeness-critic. 'minimal' = Core blockers only; 'standard' = Core + most Conventional checks; 'strict' = all checks incl. voice/tone/hard-stops in README.",
108
+ "default": "standard"
109
+ },
110
+ "activeFamilies": {
111
+ "type": "array",
112
+ "description": "Token families this project has in scope. Computed during /design:setup-ds. The completeness-critic uses this to gate Conventional checks (e.g. type-mono specimen only required when 'mono' is here).",
113
+ "items": {
114
+ "type": "string",
115
+ "enum": ["accent", "status", "presence", "mono"]
116
+ },
117
+ "default": ["accent"]
118
+ },
119
+ "designSystems": {
120
+ "type": "array",
121
+ "description": "Design systems available in this project. Single-DS projects have one entry; multi-DS projects (marketing vs. admin vs. mobile) list each here. Each canvas's .meta.json declares which DS it uses via the 'designSystem' field.",
122
+ "items": {
123
+ "type": "object",
124
+ "required": ["name", "path"],
125
+ "properties": {
126
+ "name": {
127
+ "type": "string",
128
+ "pattern": "^[a-z][a-z0-9-]*$",
129
+ "description": "Kebab-case slug. Used to match canvas .meta.json.designSystem references."
130
+ },
131
+ "path": {
132
+ "type": "string",
133
+ "description": "Folder under designRoot (e.g. 'system/marketing')."
134
+ },
135
+ "description": { "type": "string" }
136
+ },
137
+ "additionalProperties": false
138
+ },
139
+ "default": []
140
+ },
141
+ "defaultDesignSystem": {
142
+ "type": ["string", "null"],
143
+ "description": "Name of the DS used when a canvas's .meta.json has no 'designSystem' field. For single-DS projects, this is the only DS. For multi-DS, pick the most common.",
144
+ "default": null
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * @file context-menu.tsx — Phase 4.1 right-click context menu
3
+ * @scope plugins/design/dev-server/context-menu.tsx
4
+ * @purpose Floating, context-aware right-click menu. The router calls
5
+ * `openContextMenu({ clientX, clientY, target })`; the menu
6
+ * resolves the section list via the ContextRegistry and renders
7
+ * items with right-aligned shortcut hints. Pattern prior:
8
+ * `audience-pro/components-toast-menu.html` (hairline border,
9
+ * mono shortcut hints, sep lines).
10
+ *
11
+ * Targeting taxonomy:
12
+ * - element — cursor is over a `[data-cd-id]` descendant
13
+ * - artboard-chrome — cursor over an artboard's label/border (`[data-dc-screen]`)
14
+ * - world — cursor on empty canvas background
15
+ * - overlay — cursor over MiniMap / ZoomToolbar / ToolPalette
16
+ * (no menu — bubble through)
17
+ *
18
+ * Items registered for each target type live in the default registry; consumers
19
+ * can extend via `<ContextMenuProvider extra={...}>` (Phase 5+).
20
+ */
21
+
22
+ import {
23
+ type ReactNode,
24
+ createContext,
25
+ useCallback,
26
+ useContext,
27
+ useEffect,
28
+ useMemo,
29
+ useRef,
30
+ useState,
31
+ } from 'react';
32
+
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Types
35
+
36
+ export type ContextTargetKind = 'element' | 'artboard-chrome' | 'world' | 'overlay';
37
+
38
+ export interface ContextTarget {
39
+ kind: ContextTargetKind;
40
+ /** Resolved DOM element under cursor (raw hit). */
41
+ el: Element | null;
42
+ /** Stable `data-cd-id` if the hit is inside a TSX-pipeline-stamped subtree. */
43
+ cdId: string | null;
44
+ /** Owning artboard id (`data-dc-screen`). */
45
+ artboardId: string | null;
46
+ /** Viewport coords for the menu position. */
47
+ clientX: number;
48
+ clientY: number;
49
+ }
50
+
51
+ export interface MenuItem {
52
+ id: string;
53
+ label: string;
54
+ /** Right-aligned shortcut hint (e.g. `⌘C`, `⌫`). */
55
+ shortcut?: string;
56
+ destructive?: boolean;
57
+ disabled?: boolean;
58
+ onSelect: (target: ContextTarget) => void;
59
+ }
60
+
61
+ export type MenuSection = MenuItem[];
62
+
63
+ export type ContextRegistry = Record<ContextTargetKind, MenuSection[]>;
64
+
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // Default registry. Item callbacks intentionally use `console.warn` for the
67
+ // un-implemented affordances; T6 + later phases wire real handlers in.
68
+
69
+ function noop(name: string) {
70
+ return () => {
71
+ if (typeof console !== 'undefined') {
72
+ console.warn(`[context-menu] TODO: ${name}`);
73
+ }
74
+ };
75
+ }
76
+
77
+ const DEFAULT_REGISTRY: ContextRegistry = {
78
+ element: [
79
+ [
80
+ { id: 'add-comment', label: 'Add comment', shortcut: 'C', onSelect: noop('add-comment') },
81
+ { id: 'copy-css', label: 'Copy CSS', shortcut: '⌘⇧C', onSelect: noop('copy-css') },
82
+ { id: 'copy-id', label: 'Copy data-cd-id', onSelect: noop('copy-id') },
83
+ { id: 'inspect', label: 'Inspect', shortcut: '⌥I', onSelect: noop('inspect') },
84
+ ],
85
+ [
86
+ { id: 'hide', label: 'Hide', shortcut: '⌘⇧H', onSelect: noop('hide') },
87
+ { id: 'lock', label: 'Lock', shortcut: '⌘⇧L', onSelect: noop('lock') },
88
+ ],
89
+ ],
90
+ 'artboard-chrome': [
91
+ [
92
+ { id: 'rename', label: 'Rename', shortcut: '↵', onSelect: noop('rename-artboard') },
93
+ { id: 'duplicate', label: 'Duplicate', shortcut: '⌘D', onSelect: noop('duplicate-artboard') },
94
+ ],
95
+ [
96
+ { id: 'fit-one', label: 'Fit just this artboard', onSelect: noop('fit-one') },
97
+ { id: 'reset-pos', label: 'Reset position', onSelect: noop('reset-artboard-pos') },
98
+ ],
99
+ ],
100
+ world: [
101
+ [
102
+ {
103
+ id: 'paste-artboard',
104
+ label: 'Paste artboard',
105
+ shortcut: '⌘V',
106
+ onSelect: noop('paste-artboard'),
107
+ },
108
+ { id: 'fit-view', label: 'Fit to view', shortcut: '1', onSelect: noop('fit-view') },
109
+ { id: 'reset-view', label: 'Reset view', shortcut: '⌘0', onSelect: noop('reset-view') },
110
+ ],
111
+ ],
112
+ overlay: [],
113
+ };
114
+
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+ // Provider / context
117
+
118
+ interface ContextMenuValue {
119
+ open: (target: ContextTarget) => void;
120
+ close: () => void;
121
+ registry: ContextRegistry;
122
+ }
123
+
124
+ const ContextMenuContext = createContext<ContextMenuValue | null>(null);
125
+
126
+ interface InternalState {
127
+ target: ContextTarget | null;
128
+ }
129
+
130
+ const MENU_CSS = `
131
+ .dc-context-menu {
132
+ position: fixed;
133
+ z-index: 7;
134
+ background: var(--bg-1, #fff);
135
+ border: 1px solid var(--border-default, rgba(0,0,0,0.12));
136
+ border-radius: var(--radius-md, 6px);
137
+ box-shadow: var(--shadow-md, 0 8px 24px rgba(0,0,0,0.12));
138
+ padding: 4px;
139
+ min-width: 220px;
140
+ font: inherit;
141
+ font-size: 12px;
142
+ color: var(--fg-0, rgba(20,15,10,0.92));
143
+ user-select: none;
144
+ }
145
+ .dc-context-menu .dc-menu-sep {
146
+ height: 1px;
147
+ background: var(--border-subtle, rgba(0,0,0,0.08));
148
+ margin: 4px -4px;
149
+ }
150
+ .dc-context-menu .dc-menu-item {
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ gap: 16px;
155
+ padding: 6px 10px;
156
+ border-radius: var(--radius-sm, 4px);
157
+ cursor: pointer;
158
+ background: transparent;
159
+ border: 0;
160
+ width: 100%;
161
+ text-align: left;
162
+ font: inherit;
163
+ color: inherit;
164
+ }
165
+ .dc-context-menu .dc-menu-item:hover,
166
+ .dc-context-menu .dc-menu-item:focus-visible {
167
+ background: var(--bg-3, rgba(0,0,0,0.05));
168
+ outline: none;
169
+ }
170
+ .dc-context-menu .dc-menu-item[disabled] {
171
+ opacity: 0.45;
172
+ cursor: not-allowed;
173
+ }
174
+ .dc-context-menu .dc-menu-item.is-destructive:hover,
175
+ .dc-context-menu .dc-menu-item.is-destructive:focus-visible {
176
+ background: var(--status-error, #c0392b);
177
+ color: var(--accent-fg, #fff);
178
+ }
179
+ .dc-context-menu .dc-menu-shortcut {
180
+ color: var(--fg-2, rgba(40,30,20,0.55));
181
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
182
+ font-size: 10px;
183
+ font-variant-numeric: tabular-nums;
184
+ }
185
+ `.trim();
186
+
187
+ function ensureMenuStyles(): void {
188
+ if (typeof document === 'undefined') return;
189
+ if (document.getElementById('dc-context-menu-css')) return;
190
+ const s = document.createElement('style');
191
+ s.id = 'dc-context-menu-css';
192
+ s.textContent = MENU_CSS;
193
+ document.head.appendChild(s);
194
+ }
195
+
196
+ export function ContextMenuProvider({
197
+ children,
198
+ registry = DEFAULT_REGISTRY,
199
+ }: {
200
+ children: ReactNode;
201
+ registry?: ContextRegistry;
202
+ }) {
203
+ const [state, setState] = useState<InternalState>({ target: null });
204
+
205
+ const open = useCallback((target: ContextTarget) => {
206
+ if (target.kind === 'overlay') return;
207
+ setState({ target });
208
+ }, []);
209
+ const close = useCallback(() => setState({ target: null }), []);
210
+
211
+ const value = useMemo<ContextMenuValue>(
212
+ () => ({ open, close, registry }),
213
+ [open, close, registry]
214
+ );
215
+
216
+ return (
217
+ <ContextMenuContext.Provider value={value}>
218
+ {children}
219
+ {state.target ? (
220
+ <ContextMenuView
221
+ target={state.target}
222
+ sections={registry[state.target.kind] ?? []}
223
+ onClose={close}
224
+ />
225
+ ) : null}
226
+ </ContextMenuContext.Provider>
227
+ );
228
+ }
229
+
230
+ export function useContextMenu(): ContextMenuValue {
231
+ const ctx = useContext(ContextMenuContext);
232
+ if (!ctx) {
233
+ throw new Error('useContextMenu must be used inside <ContextMenuProvider>');
234
+ }
235
+ return ctx;
236
+ }
237
+
238
+ export function useContextMenuOptional(): ContextMenuValue | null {
239
+ return useContext(ContextMenuContext);
240
+ }
241
+
242
+ // ─────────────────────────────────────────────────────────────────────────────
243
+ // View
244
+
245
+ function ContextMenuView({
246
+ target,
247
+ sections,
248
+ onClose,
249
+ }: {
250
+ target: ContextTarget;
251
+ sections: MenuSection[];
252
+ onClose: () => void;
253
+ }) {
254
+ ensureMenuStyles();
255
+ const ref = useRef<HTMLDivElement | null>(null);
256
+ const [pos, setPos] = useState<{ x: number; y: number }>({
257
+ x: target.clientX,
258
+ y: target.clientY,
259
+ });
260
+
261
+ // Reposition if menu would overflow the viewport.
262
+ useEffect(() => {
263
+ const el = ref.current;
264
+ if (!el) return;
265
+ const r = el.getBoundingClientRect();
266
+ const vw = window.innerWidth;
267
+ const vh = window.innerHeight;
268
+ let nx = target.clientX;
269
+ let ny = target.clientY;
270
+ if (nx + r.width > vw - 8) nx = Math.max(8, vw - r.width - 8);
271
+ if (ny + r.height > vh - 8) ny = Math.max(8, vh - r.height - 8);
272
+ if (nx !== pos.x || ny !== pos.y) setPos({ x: nx, y: ny });
273
+ // Focus first menu item for keyboard nav.
274
+ const firstBtn = el.querySelector<HTMLButtonElement>('button.dc-menu-item:not([disabled])');
275
+ firstBtn?.focus();
276
+ // Dismiss on outside-click / Esc / scroll.
277
+ const onDocPointer = (e: PointerEvent) => {
278
+ if (!el.contains(e.target as Node)) onClose();
279
+ };
280
+ const onKey = (e: KeyboardEvent) => {
281
+ if (e.key === 'Escape') {
282
+ e.preventDefault();
283
+ onClose();
284
+ return;
285
+ }
286
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
287
+ e.preventDefault();
288
+ const items = Array.from(
289
+ el.querySelectorAll<HTMLButtonElement>('button.dc-menu-item:not([disabled])')
290
+ );
291
+ if (items.length === 0) return;
292
+ const idx = items.findIndex((b) => b === document.activeElement);
293
+ const nextIdx =
294
+ e.key === 'ArrowDown'
295
+ ? (idx + 1) % items.length
296
+ : (idx - 1 + items.length) % items.length;
297
+ items[nextIdx]?.focus();
298
+ }
299
+ };
300
+ const onScroll = () => onClose();
301
+ document.addEventListener('pointerdown', onDocPointer, true);
302
+ document.addEventListener('keydown', onKey, true);
303
+ document.addEventListener('scroll', onScroll, true);
304
+ window.addEventListener('blur', onClose);
305
+ return () => {
306
+ document.removeEventListener('pointerdown', onDocPointer, true);
307
+ document.removeEventListener('keydown', onKey, true);
308
+ document.removeEventListener('scroll', onScroll, true);
309
+ window.removeEventListener('blur', onClose);
310
+ };
311
+ }, [target.clientX, target.clientY, onClose, pos.x, pos.y]);
312
+
313
+ return (
314
+ <div ref={ref} className="dc-context-menu" role="menu" style={{ left: pos.x, top: pos.y }}>
315
+ {sections.map((section, si) => {
316
+ const sectionKey = section.map((i) => i.id).join('|') || `s${si}`;
317
+ return (
318
+ // biome-ignore lint/a11y/useSemanticElements: ARIA group within role="menu"; no native equivalent.
319
+ <div key={sectionKey} role="group">
320
+ {si > 0 ? <div className="dc-menu-sep" aria-hidden="true" /> : null}
321
+ {section.map((item) => (
322
+ <button
323
+ key={item.id}
324
+ type="button"
325
+ role="menuitem"
326
+ disabled={item.disabled}
327
+ className={`dc-menu-item${item.destructive ? ' is-destructive' : ''}`}
328
+ onClick={() => {
329
+ if (item.disabled) return;
330
+ item.onSelect(target);
331
+ onClose();
332
+ }}
333
+ >
334
+ <span>{item.label}</span>
335
+ {item.shortcut ? <span className="dc-menu-shortcut">{item.shortcut}</span> : null}
336
+ </button>
337
+ ))}
338
+ </div>
339
+ );
340
+ })}
341
+ </div>
342
+ );
343
+ }
@@ -0,0 +1,173 @@
1
+ // Shared Context object passed to each module's factory.
2
+ // Owns config + paths + a tiny pub-sub bus the modules use to talk without
3
+ // importing each other. Stateless beyond that — per-conn data lives on Bun's
4
+ // ws.data, per-request state on the Bun.serve context.
5
+
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ export type ConfigSource = '.design/config.json' | 'defaults' | 'defaults (config invalid)';
10
+
11
+ export interface CanvasGroup {
12
+ label: string;
13
+ path: string;
14
+ }
15
+
16
+ export interface DesignSystemEntry {
17
+ name: string;
18
+ path: string;
19
+ description?: string;
20
+ tokensCssRel?: string;
21
+ rootClass?: string;
22
+ themeDefault?: 'dark' | 'light';
23
+ themes?: string[];
24
+ newCanvasDir?: string;
25
+ newComponentDir?: string;
26
+ }
27
+
28
+ export interface DevServerConfig {
29
+ name: string;
30
+ projectLabel: string | null;
31
+ designRoot: string;
32
+ canvasGroups: CanvasGroup[];
33
+ designSystems?: DesignSystemEntry[];
34
+ rootClass: string;
35
+ themeDefault: 'dark' | 'light';
36
+ tokensCssRel: string;
37
+ teamAccentDefault: string | null;
38
+ handoffTargets: unknown[];
39
+ newCanvasDir: string;
40
+ newComponentDir: string;
41
+ _source: ConfigSource;
42
+ }
43
+
44
+ const DEFAULT_CONFIG: Omit<DevServerConfig, '_source'> = {
45
+ name: 'Design',
46
+ projectLabel: null,
47
+ designRoot: '.design',
48
+ canvasGroups: [
49
+ { label: 'Design system', path: 'system' },
50
+ { label: 'Canvases', path: 'ui' },
51
+ ],
52
+ rootClass: 'app',
53
+ themeDefault: 'dark',
54
+ tokensCssRel: 'system/colors_and_type.css',
55
+ teamAccentDefault: null,
56
+ handoffTargets: [],
57
+ newCanvasDir: 'ui',
58
+ newComponentDir: 'ui/components',
59
+ };
60
+
61
+ export interface Paths {
62
+ repoRoot: string;
63
+ designRel: string;
64
+ designRoot: string;
65
+ serverInfoFile: string;
66
+ activeFile: string;
67
+ commentsDir: string;
68
+ canvasStateDir: string;
69
+ historyDir: string;
70
+ tokensUrlRel: string;
71
+ systemDirRel: string;
72
+ }
73
+
74
+ // Tiny pub-sub bus. Lazy — modules subscribe with on('selected', fn) and emit
75
+ // the matching event. Avoids cycling imports between inspect.ts <-> ws.ts.
76
+ export interface Bus {
77
+ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous pubsub — subscribers annotate their own payload shape.
78
+ on(evt: string, fn: (payload: any) => void): () => void;
79
+ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous pubsub — emitters supply their own payload shape.
80
+ emit(evt: string, payload?: any): void;
81
+ }
82
+
83
+ export function createBus(): Bus {
84
+ // biome-ignore lint/suspicious/noExplicitAny: subscribers are typed at the call site; the bus stores the erased type.
85
+ const subs = new Map<string, Set<(p: any) => void>>();
86
+ return {
87
+ on(evt, fn) {
88
+ const set = subs.get(evt) ?? new Set();
89
+ set.add(fn);
90
+ subs.set(evt, set);
91
+ return () => set.delete(fn);
92
+ },
93
+ emit(evt, payload) {
94
+ const set = subs.get(evt);
95
+ if (!set) return;
96
+ for (const fn of set) {
97
+ try {
98
+ fn(payload);
99
+ } catch (err) {
100
+ console.error(`[bus] subscriber for ${evt} threw:`, err);
101
+ }
102
+ }
103
+ },
104
+ };
105
+ }
106
+
107
+ export interface Context {
108
+ cfg: DevServerConfig;
109
+ projectLabel: string;
110
+ paths: Paths;
111
+ bus: Bus;
112
+ }
113
+
114
+ function resolveRepoRoot(): string {
115
+ const i = process.argv.indexOf('--root');
116
+ if (i !== -1 && process.argv[i + 1]) return path.resolve(process.argv[i + 1]);
117
+ if (process.env.CLAUDE_PROJECT_DIR) return path.resolve(process.env.CLAUDE_PROJECT_DIR);
118
+ return process.cwd();
119
+ }
120
+
121
+ function loadConfig(repoRoot: string): DevServerConfig {
122
+ const configPath = path.join(repoRoot, '.design', 'config.json');
123
+ let raw: string;
124
+ try {
125
+ raw = readFileSync(configPath, 'utf8');
126
+ } catch {
127
+ return { ...DEFAULT_CONFIG, _source: 'defaults' };
128
+ }
129
+ try {
130
+ const parsed = JSON.parse(raw);
131
+ return { ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' };
132
+ } catch (e) {
133
+ const msg = e instanceof Error ? e.message : String(e);
134
+ console.error(` warn: ${configPath} is not valid JSON: ${msg}. Using defaults.`);
135
+ return { ...DEFAULT_CONFIG, _source: 'defaults (config invalid)' };
136
+ }
137
+ }
138
+
139
+ export function createContext(): Context {
140
+ const repoRoot = resolveRepoRoot();
141
+
142
+ // Fail loud if launched from a directory that has no .design/ — preserves the
143
+ // load-bearing diagnostic from server.mjs: silent fallback to defaults masks
144
+ // "wrong project root" bugs.
145
+ if (!existsSync(path.join(repoRoot, '.design'))) {
146
+ console.error(` error: no .design/ directory at ${repoRoot}`);
147
+ console.error(' Run from your project root, set $CLAUDE_PROJECT_DIR, or pass --root <path>.');
148
+ process.exit(1);
149
+ }
150
+
151
+ const cfg = loadConfig(repoRoot);
152
+ const designRel = cfg.designRoot.replace(/^\/+|\/+$/g, '');
153
+ const designRoot = path.join(repoRoot, designRel);
154
+ const systemDirRel = cfg.canvasGroups.find((g) => /system/i.test(g.path))?.path ?? 'system';
155
+
156
+ return {
157
+ cfg,
158
+ projectLabel: cfg.projectLabel || `${cfg.name} Design`,
159
+ paths: {
160
+ repoRoot,
161
+ designRel,
162
+ designRoot,
163
+ serverInfoFile: path.join(designRoot, '_server.json'),
164
+ activeFile: path.join(designRoot, '_active.json'),
165
+ commentsDir: path.join(designRoot, '_comments'),
166
+ canvasStateDir: path.join(designRoot, '_canvas-state'),
167
+ historyDir: path.join(designRoot, '_history'),
168
+ tokensUrlRel: path.posix.join(designRel, cfg.tokensCssRel.replace(/^\/+/, '')),
169
+ systemDirRel,
170
+ },
171
+ bus: createBus(),
172
+ };
173
+ }