@1agh/maude 0.28.1 → 0.29.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 (339) hide show
  1. package/README.md +2 -2
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
  3. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +285 -5
  4. package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1206 -119
  5. package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
  6. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  7. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  8. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  9. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  10. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  11. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  12. package/apps/studio/bin/read-annotations.mjs +438 -0
  13. package/apps/studio/bin/read-annotations.sh +11 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  15. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  16. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  17. package/apps/studio/bin/to-lottie-verify.html +68 -0
  18. package/apps/studio/bin/to-lottie.sh +128 -0
  19. package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
  20. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
  21. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  22. package/apps/studio/canvas-create.ts +104 -0
  23. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +46 -12
  24. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
  25. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  26. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  27. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  28. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +56 -41
  29. package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
  30. package/apps/studio/client/canvas-url.js +81 -0
  31. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  32. package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
  33. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  34. package/apps/studio/client/styles/3-shell-maude.css +459 -0
  35. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  36. package/apps/studio/client/styles/4-components-maude.css +74 -0
  37. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
  38. package/apps/studio/client/styles/5-maude-overrides.css +123 -0
  39. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  41. package/apps/studio/client/tour/overlay.jsx +191 -0
  42. package/apps/studio/client/tour/usage-tour.js +35 -0
  43. package/apps/studio/client/whats-new-seen.js +52 -0
  44. package/apps/studio/client/whats-new.jsx +250 -0
  45. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
  46. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  47. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
  48. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
  49. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +1 -1
  50. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
  51. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
  52. package/apps/studio/dist/client.bundle.js +22825 -0
  53. package/apps/studio/dist/comment-mount.js +1960 -0
  54. package/apps/studio/dist/styles.css +5695 -0
  55. package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
  56. package/apps/studio/draw/animate.ts +333 -0
  57. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  58. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  59. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  60. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  61. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  62. package/apps/studio/draw/morph.ts +204 -0
  63. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  64. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  65. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  66. package/apps/studio/draw/serialize-animate.ts +324 -0
  67. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  68. package/apps/studio/draw/test/animate.test.ts +172 -0
  69. package/apps/studio/draw/test/morph.test.ts +107 -0
  70. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  71. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  72. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  73. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +13 -1
  74. package/{plugins/design/dev-server → apps/studio}/http.ts +102 -1
  75. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
  76. package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
  77. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  78. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
  79. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  80. package/{plugins/design/dev-server → apps/studio}/server.ts +18 -0
  81. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +1 -1
  82. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  83. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  84. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  85. package/apps/studio/test/asset-api.test.ts +149 -0
  86. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  87. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  88. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  89. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  90. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +17 -1
  91. package/apps/studio/test/canvas-url.test.ts +99 -0
  92. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  93. package/apps/studio/test/motion-sample.test.ts +35 -0
  94. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  95. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  96. package/apps/studio/test/read-annotations.test.ts +409 -0
  97. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  98. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  99. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  100. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
  101. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
  102. package/apps/studio/test/whats-new.test.ts +101 -0
  103. package/apps/studio/text-imports.d.ts +9 -0
  104. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
  105. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
  106. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  107. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +1 -1
  108. package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +40 -7
  109. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +1 -1
  110. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  111. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  112. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +1 -1
  113. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  114. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +1 -1
  115. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  116. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  117. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +9 -1
  118. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  119. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +3 -1
  120. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  121. package/apps/studio/whats-new.json +139 -0
  122. package/apps/studio/whats-new.schema.json +70 -0
  123. package/apps/studio/whats-new.ts +87 -0
  124. package/cli/bin/maude.mjs +10 -0
  125. package/cli/commands/design.mjs +15 -7
  126. package/cli/commands/design.test.mjs +36 -0
  127. package/cli/commands/help.mjs +8 -2
  128. package/cli/commands/hub.mjs +3 -3
  129. package/cli/commands/hub.test.mjs +1 -1
  130. package/cli/commands/scenario-report.mjs +2 -9
  131. package/cli/lib/flow-design-integration.test.mjs +1 -1
  132. package/cli/lib/gitignore-block.mjs +1 -0
  133. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  134. package/cli/lib/preflight.mjs +1 -1
  135. package/package.json +15 -15
  136. package/plugins/design/templates/brief-board.tsx.template +61 -0
  137. package/plugins/design/templates/canvas.tsx.template +1 -1
  138. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  139. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  140. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  141. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  142. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  143. package/plugins/design/dev-server/dist/styles.css +0 -2710
  144. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  145. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  146. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  147. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  148. /package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
  149. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  150. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  151. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  152. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  153. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  154. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  155. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  156. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  157. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  158. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  159. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  160. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  161. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  162. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  163. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  164. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  165. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  166. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  167. /package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
  168. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  169. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  170. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  171. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  172. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  173. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  174. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  175. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  176. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  177. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  178. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  179. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  180. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  181. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  182. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  183. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  184. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  185. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  186. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  187. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  188. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  189. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  190. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  191. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  192. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  193. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  194. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  195. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  196. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  197. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  198. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  199. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  200. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  201. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -11,6 +11,12 @@ import { createRoot } from 'react-dom/client';
11
11
  // import that Bun erases), so this pulls only string constants into the client
12
12
  // bundle — no React, no input-router. See the tool-cursor handler below.
13
13
  import { resolveToolCursor } from '../canvas-cursors.ts';
14
+ import { canvasUrl } from './canvas-url.js';
15
+ import { TourOverlay } from './tour/overlay.jsx';
16
+ import { USAGE_TOUR } from './tour/usage-tour.js';
17
+ import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
18
+
19
+ const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
14
20
 
15
21
  const SYSTEM_TAB = '__system__';
16
22
  const THEME_STORE = 'mdcc-theme';
@@ -63,73 +69,12 @@ function sectionDefaultOpen(g) {
63
69
 
64
70
  // ---------- Utility ----------
65
71
 
66
- function urlOf(p) {
67
- return '/' + p.split('/').map(encodeURIComponent).join('/');
68
- }
69
-
70
72
  // Iframe src for a canvas path. TSX canvases go through _canvas-shell.html so
71
73
  // the bundled React 19 runtime + importmap can mount the default export. HTML
72
74
  // canvases keep the legacy "serve the file with inspector + Babel injected"
73
75
  // path. Phase 3.6 contract; the path argument is repo-root-relative
74
- // (e.g. ".design/ui/Foo.tsx").
75
- function canvasUrl(p, cfg, opts) {
76
- if (!p.endsWith('.tsx')) return urlOf(p);
77
- const designRel = (cfg?.designRel || '.design').replace(/^\/+|\/+$/g, '');
78
- // Path under designRoot.
79
- let rel = p;
80
- if (rel.startsWith(designRel + '/')) rel = rel.slice(designRel.length + 1);
81
- // Pass `rel` to URLSearchParams RAW — it does encoding once. Pre-encoding
82
- // with encodeURIComponent then handing to URLSearchParams produced
83
- // `Docs%2520Site.tsx` (the `%` of `%20` got re-encoded as `%25`) and broke
84
- // every UI canvas with a space in its filename.
85
- const params = new URLSearchParams();
86
- params.set('canvas', rel);
87
- params.set('designRel', designRel);
88
- // Gallery thumbnails suppress the shell-owned comment layer (`?comments=0`)
89
- // so previews stay non-interactive; opening the same canvas as a real tab
90
- // omits the flag and gets comments. See canvas-comment-mount.tsx.
91
- if (opts?.thumbnail) params.set('comments', '0');
92
- const ds0 = cfg?.designSystems?.[0];
93
- // Specimen detection: anything under `system/<ds>/preview/` belongs to that
94
- // specific DS, so it must render with *that* DS's tokens — not always the
95
- // first one. In a multi-DS project this is what keeps each design system's
96
- // preview distinct (beta previews use beta tokens, not alpha's).
97
- const specMatch = rel.match(/^system\/([^/]+)\/preview\//);
98
- const specDsEntry = specMatch
99
- ? cfg?.designSystems?.find(
100
- (d) => d.path === `system/${specMatch[1]}` || d.path.endsWith(`/${specMatch[1]}`)
101
- )
102
- : null;
103
- // Resolve tokens path. For a specimen, prefer the matching DS's tokensCssRel
104
- // (fall back to the `system/<ds>/colors_and_type.css` convention). Otherwise
105
- // prefer the first designSystem's tokensCssRel — the project's authoritative
106
- // tokens file. The top-level cfg.tokensCssRel is the legacy default
107
- // (`system/colors_and_type.css`) and usually doesn't exist post-bootstrap.
108
- const tokens = specMatch
109
- ? specDsEntry?.tokensCssRel || `system/${specMatch[1]}/colors_and_type.css`
110
- : ds0?.tokensCssRel || cfg?.tokensCssRel;
111
- if (tokens) params.set('tokens', tokens);
112
- if (cfg?.componentsCssRel) params.set('components', cfg.componentsCssRel);
113
- if (specMatch) {
114
- const dsName = specMatch[1];
115
- params.set('layout', `system/${dsName}/preview/_layout.css`);
116
- if (!cfg?.componentsCssRel) {
117
- params.set('components', `system/${dsName}/preview/_components.css`);
118
- }
119
- } else if (ds0?.path) {
120
- // UI canvas — load the project DS's `_components.css` so the dc-canvas /
121
- // dc-section / dc-artboard chrome (and any DS classes the canvas reuses)
122
- // renders correctly.
123
- if (!cfg?.componentsCssRel) {
124
- params.set('components', `${ds0.path}/preview/_components.css`);
125
- }
126
- }
127
- // T2 (9.1-A) — load the iframe from the segregated canvas origin when the
128
- // server advertises one; fall back to a same-origin relative URL otherwise
129
- // (older server, tests). The trailing path + query are identical either way.
130
- const origin = cfg?.canvasOrigin || '';
131
- return `${origin}/_canvas-shell.html?${params.toString()}`;
132
- }
76
+ // (e.g. ".design/ui/Foo.tsx"). Pure resolver extracted to ./canvas-url.js so
77
+ // the token-resolution branches are unit-testable without a DOM (DDR-093).
133
78
 
134
79
  function basename(p) {
135
80
  return p.split('/').pop();
@@ -298,6 +243,556 @@ function Icon({ d, size = 14, color }) {
298
243
  );
299
244
  }
300
245
 
246
+ // ───── maude DS icon set (Plan B) ─────
247
+ // Thin-stroke (1.4) 16×16 geometric glyphs lifted from .design/ui/Studio.tsx —
248
+ // the icon vocabulary the ported `.st-*` chrome composes with. Distinct from the
249
+ // legacy 24×24 single-path `Icon` above (kept for not-yet-ported chrome). Grown
250
+ // per slice; this slice (menubar/statusbar) needs sparkle/check/sun/moon plus a
251
+ // few the sidebar + panels reuse later.
252
+ const STICONS = {
253
+ 'chevron-down': <polyline points="3.5 6 8 10.5 12.5 6" />,
254
+ 'chevron-right': <polyline points="6 3.5 10.5 8 6 12.5" />,
255
+ file: (
256
+ <>
257
+ <path d="M4 2h5l3 3v9H4z" />
258
+ <polyline points="9 2 9 5 12 5" />
259
+ </>
260
+ ),
261
+ folder: <path d="M2 4.5h4l1.3 1.5H14V13H2z" />,
262
+ search: (
263
+ <>
264
+ <circle cx="7" cy="7" r="4" />
265
+ <line x1="10" y1="10" x2="13.5" y2="13.5" />
266
+ </>
267
+ ),
268
+ plus: (
269
+ <>
270
+ <line x1="8" y1="3" x2="8" y2="13" />
271
+ <line x1="3" y1="8" x2="13" y2="8" />
272
+ </>
273
+ ),
274
+ check: <polyline points="3 8.2 6.4 11.5 13 4.2" />,
275
+ x: (
276
+ <>
277
+ <line x1="4.3" y1="4.3" x2="11.7" y2="11.7" />
278
+ <line x1="11.7" y1="4.3" x2="4.3" y2="11.7" />
279
+ </>
280
+ ),
281
+ sun: (
282
+ <>
283
+ <circle cx="8" cy="8" r="2.6" />
284
+ <line x1="8" y1="1.5" x2="8" y2="3" />
285
+ <line x1="8" y1="13" x2="8" y2="14.5" />
286
+ <line x1="1.5" y1="8" x2="3" y2="8" />
287
+ <line x1="13" y1="8" x2="14.5" y2="8" />
288
+ <line x1="3.4" y1="3.4" x2="4.4" y2="4.4" />
289
+ <line x1="11.6" y1="11.6" x2="12.6" y2="12.6" />
290
+ <line x1="12.6" y1="3.4" x2="11.6" y2="4.4" />
291
+ <line x1="4.4" y1="11.6" x2="3.4" y2="12.6" />
292
+ </>
293
+ ),
294
+ moon: <path d="M12.5 9.6A5 5 0 1 1 7 3a4 4 0 0 0 5.5 6.6z" />,
295
+ sparkle: (
296
+ <path d="M8 1.8l1.4 4.8L14 8l-4.6 1.4L8 14.2l-1.4-4.8L2 8l4.6-1.4z" fill="currentColor" stroke="none" />
297
+ ),
298
+ 'panel-left': (
299
+ <>
300
+ <rect x="2.5" y="3" width="11" height="10" rx="1.5" />
301
+ <line x1="6.4" y1="3" x2="6.4" y2="13" />
302
+ </>
303
+ ),
304
+ resolve: (
305
+ <>
306
+ <circle cx="8" cy="8" r="5.6" />
307
+ <polyline points="5.4 8 7.2 9.9 10.6 6" />
308
+ </>
309
+ ),
310
+ reopen: (
311
+ <>
312
+ <path d="M3.2 8a5 5 0 1 1 1.4 3.5" />
313
+ <polyline points="3.2 11.4 3.2 8 6.6 8" />
314
+ </>
315
+ ),
316
+ layers: (
317
+ <>
318
+ <polygon points="8 2.2 13.8 5.5 8 8.8 2.2 5.5" />
319
+ <polyline points="2.2 9 8 12.3 13.8 9" />
320
+ </>
321
+ ),
322
+ sliders: (
323
+ <>
324
+ <line x1="3" y1="5" x2="13" y2="5" />
325
+ <circle cx="6" cy="5" r="1.7" fill="currentColor" />
326
+ <line x1="3" y1="11" x2="13" y2="11" />
327
+ <circle cx="10" cy="11" r="1.7" fill="currentColor" />
328
+ </>
329
+ ),
330
+ code: (
331
+ <>
332
+ <polyline points="6 5 3 8 6 11" />
333
+ <polyline points="10 5 13 8 10 11" />
334
+ </>
335
+ ),
336
+ download: (
337
+ <>
338
+ <line x1="8" y1="2.5" x2="8" y2="10" />
339
+ <polyline points="4.5 7 8 10.5 11.5 7" />
340
+ <polyline points="3 12.8 3 13.6 13 13.6 13 12.8" />
341
+ </>
342
+ ),
343
+ reload: (
344
+ <>
345
+ <path d="M3.2 8a5 5 0 1 1 1.4 3.5" />
346
+ <polyline points="3.2 11.4 3.2 8 6.6 8" />
347
+ </>
348
+ ),
349
+ help: (
350
+ <>
351
+ <circle cx="8" cy="8" r="6" />
352
+ <path d="M6.3 6.2a1.8 1.8 0 1 1 2.3 1.9c-.5.2-.6.5-.6 1v.3" />
353
+ <line x1="8" y1="11.4" x2="8" y2="11.5" />
354
+ </>
355
+ ),
356
+ // Export-format glyphs (Plan C) — one distinct mark per format card.
357
+ image: (
358
+ <>
359
+ <rect x="2.5" y="3" width="11" height="10" rx="1.5" />
360
+ <circle cx="6" cy="6.3" r="1.1" />
361
+ <path d="M3 12l3-2.8 2.2 1.8 2.4-3L13.5 12" />
362
+ </>
363
+ ),
364
+ vector: (
365
+ <>
366
+ <path d="M3.6 11.2C6 5 10 5 12.4 11.2" />
367
+ <rect x="1.7" y="9.8" width="2.6" height="2.6" rx="0.4" />
368
+ <rect x="11.7" y="9.8" width="2.6" height="2.6" rx="0.4" />
369
+ <rect x="6.7" y="2.6" width="2.6" height="2.6" rx="0.4" />
370
+ </>
371
+ ),
372
+ presentation: (
373
+ <>
374
+ <rect x="2.5" y="3" width="11" height="7.4" rx="1" />
375
+ <line x1="8" y1="10.4" x2="8" y2="13" />
376
+ <line x1="5.6" y1="13.4" x2="10.4" y2="13.4" />
377
+ </>
378
+ ),
379
+ archive: (
380
+ <>
381
+ <rect x="2.5" y="3" width="11" height="3" rx="0.8" />
382
+ <path d="M3.6 6v6.2a1 1 0 0 0 1 1h6.8a1 1 0 0 0 1-1V6" />
383
+ <line x1="6.6" y1="8.8" x2="9.4" y2="8.8" />
384
+ </>
385
+ ),
386
+ external: (
387
+ <>
388
+ <path d="M11 8.5V12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3.5" />
389
+ <polyline points="9.5 3 13 3 13 6.5" />
390
+ <line x1="13" y1="3" x2="7.6" y2="8.4" />
391
+ </>
392
+ ),
393
+ };
394
+
395
+ // ⌘K command palette — the mockup's signature surface, wired to real shell
396
+ // actions (theme, system view, comments, reload, help, what's new, new board).
397
+ // Scoped to shell-doable actions only — in-canvas export lives in the iframe.
398
+ function CommandPalette({ open, onClose, actions }) {
399
+ const [q, setQ] = useState('');
400
+ const [active, setActive] = useState(0);
401
+ useEffect(() => {
402
+ if (open) {
403
+ setQ('');
404
+ setActive(0);
405
+ }
406
+ }, [open]);
407
+ const filtered = useMemo(() => {
408
+ const needle = q.trim().toLowerCase();
409
+ if (!needle) return actions;
410
+ return actions.filter(
411
+ (a) =>
412
+ a.label.toLowerCase().includes(needle) ||
413
+ (a.group && a.group.toLowerCase().includes(needle))
414
+ );
415
+ }, [q, actions]);
416
+ useEffect(() => {
417
+ if (active >= filtered.length) setActive(0);
418
+ }, [filtered.length, active]);
419
+ if (!open) return null;
420
+ const run = (a) => {
421
+ onClose();
422
+ a.run();
423
+ };
424
+ return (
425
+ <div
426
+ className="st-scrim"
427
+ role="presentation"
428
+ onMouseDown={(e) => {
429
+ if (e.target === e.currentTarget) onClose();
430
+ }}
431
+ >
432
+ <div className="st-palette" role="dialog" aria-modal="true" aria-label="Command palette">
433
+ <div className="st-pal-search">
434
+ <StIcon name="search" size={18} />
435
+ <input
436
+ // biome-ignore lint/a11y/noAutofocus: command palette opens on an explicit ⌘K.
437
+ autoFocus
438
+ placeholder="Type a command or search…"
439
+ value={q}
440
+ aria-label="Command search"
441
+ onChange={(e) => setQ(e.target.value)}
442
+ onKeyDown={(e) => {
443
+ if (e.key === 'Escape') {
444
+ e.preventDefault();
445
+ onClose();
446
+ } else if (e.key === 'ArrowDown') {
447
+ e.preventDefault();
448
+ setActive((i) => Math.min(filtered.length - 1, i + 1));
449
+ } else if (e.key === 'ArrowUp') {
450
+ e.preventDefault();
451
+ setActive((i) => Math.max(0, i - 1));
452
+ } else if (e.key === 'Enter') {
453
+ e.preventDefault();
454
+ if (filtered[active]) run(filtered[active]);
455
+ }
456
+ }}
457
+ />
458
+ <Kbd>⌘K</Kbd>
459
+ </div>
460
+ <div className="st-pal-list">
461
+ {filtered.length === 0 ? (
462
+ <div className="st-pal-empty">No matching command.</div>
463
+ ) : (
464
+ filtered.map((a, i) => {
465
+ // Emit a group header when the group changes (flat list → grouped
466
+ // display; keyboard nav still indexes across the whole array).
467
+ const header =
468
+ a.group && (i === 0 || filtered[i - 1].group !== a.group) ? (
469
+ <div className="st-pal-group" key={'g-' + a.group}>
470
+ {a.group}
471
+ </div>
472
+ ) : null;
473
+ return (
474
+ <Fragment key={a.id}>
475
+ {header}
476
+ <button
477
+ type="button"
478
+ className={'st-pal-item' + (i === active ? ' is-active' : '')}
479
+ onMouseEnter={() => setActive(i)}
480
+ onClick={() => run(a)}
481
+ >
482
+ <span className="st-pal-icon">
483
+ <StIcon name={a.icon} size={15} />
484
+ </span>
485
+ <span className="st-pal-label">{a.label}</span>
486
+ {a.kbd ? (
487
+ <span className="st-pal-kbd">
488
+ <Kbd>{a.kbd}</Kbd>
489
+ </span>
490
+ ) : null}
491
+ </button>
492
+ </Fragment>
493
+ );
494
+ })
495
+ )}
496
+ </div>
497
+ </div>
498
+ </div>
499
+ );
500
+ }
501
+
502
+ function StIcon({ name, size = 16, className }) {
503
+ return (
504
+ <svg
505
+ className={className}
506
+ width={size}
507
+ height={size}
508
+ viewBox="0 0 16 16"
509
+ fill="none"
510
+ stroke="currentColor"
511
+ strokeWidth="1.4"
512
+ strokeLinecap="round"
513
+ strokeLinejoin="round"
514
+ aria-hidden="true"
515
+ style={{ flex: 'none' }}
516
+ >
517
+ {STICONS[name]}
518
+ </svg>
519
+ );
520
+ }
521
+
522
+ // P3 (Plan C) — menubar presence avatar (matches `.design/ui/Studio.tsx` Avatar).
523
+ // Up to two uppercase glyphs from a name; falls back to "?" for empties.
524
+ function initialsOf(name) {
525
+ if (!name || typeof name !== 'string') return '?';
526
+ const parts = name.trim().split(/[\s._-]+/).filter(Boolean);
527
+ if (!parts.length) return '?';
528
+ // Single-token names (e.g. a git username "1aGh") → first two chars, so the
529
+ // avatar reads as initials rather than a lone count badge.
530
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
531
+ return ((parts[0][0] || '') + (parts[parts.length - 1][0] || '')).toUpperCase() || '?';
532
+ }
533
+ function StAvatar({ initials, hue, title }) {
534
+ return (
535
+ <span className="st-avatar" style={{ background: hue }} title={title} aria-label={title}>
536
+ {initials}
537
+ </span>
538
+ );
539
+ }
540
+
541
+ function Kbd({ children }) {
542
+ return <span className="kbd">{children}</span>;
543
+ }
544
+
545
+ // T5 (Plan C) — shell-level Export & Handoff dialog (maude `.st-dialog`), per
546
+ // `.design/ui/Studio.tsx` HandoffBoard. Wired to the privileged main-origin
547
+ // `POST /_api/export` (7 real formats × scopes). The shadcn card is HANDOFF —
548
+ // a privileged disk-write kept off HTTP routes (DDR-054), so it surfaces the
549
+ // `/design:handoff` command instead. PPTX/Canva kept reachable (no silent cap).
550
+ const EXPORT_CARDS = [
551
+ { id: 'png', label: 'PNG', sub: 'raster · 2×', icon: 'image', format: 'png', options: { scale: 2 } },
552
+ { id: 'pdf', label: 'PDF', sub: 'vector · print', icon: 'file', format: 'pdf' },
553
+ { id: 'svg', label: 'SVG', sub: 'per artboard', icon: 'vector', format: 'svg' },
554
+ { id: 'html', label: 'HTML', sub: 'self-contained', icon: 'code', format: 'html' },
555
+ { id: 'pptx', label: 'PPTX', sub: 'slides', icon: 'presentation', format: 'pptx' },
556
+ { id: 'canva', label: 'Canva', sub: 'handoff bundle', icon: 'external', format: 'canva' },
557
+ { id: 'zip', label: 'ZIP', sub: 'project bundle', icon: 'archive', format: 'zip' },
558
+ { id: 'shadcn', label: 'AI handoff', sub: 'production drop', icon: 'sparkle', handoff: true },
559
+ ];
560
+
561
+ // Mirrors export-dialog.tsx (the in-canvas dialog) so both entry points offer
562
+ // the same settings. Scope validity + PNG scale presets are ported verbatim.
563
+ const EXPORT_SCOPE_LABELS = {
564
+ selection: 'Current selection',
565
+ artboard: 'Active artboard',
566
+ 'canvas-as-separate': 'Canvas · artboards separate',
567
+ 'project-raw': 'Whole project (raw)',
568
+ };
569
+ const EXPORT_VALID_SCOPES = {
570
+ png: ['selection', 'artboard', 'canvas-as-separate'],
571
+ pdf: ['selection', 'artboard', 'canvas-as-separate'],
572
+ svg: ['selection', 'artboard', 'canvas-as-separate'],
573
+ html: ['artboard', 'canvas-as-separate'],
574
+ pptx: ['canvas-as-separate'],
575
+ canva: ['canvas-as-separate'],
576
+ zip: ['project-raw'],
577
+ };
578
+ const PNG_SCALES = [
579
+ { value: 1, label: '1× (native)' },
580
+ { value: 2, label: '2× (retina)' },
581
+ { value: 3, label: '3× (max)' },
582
+ ];
583
+
584
+ function ExportDialog({ mode, initialScope, activePath, onClose }) {
585
+ const [sel, setSel] = useState(mode === 'handoff' ? 'shadcn' : 'png');
586
+ const [scope, setScope] = useState(
587
+ initialScope && EXPORT_SCOPE_LABELS[initialScope] ? initialScope : 'artboard'
588
+ );
589
+ const [scale, setScale] = useState(2);
590
+ const [busy, setBusy] = useState(false);
591
+ const [status, setStatus] = useState(null); // { ok, msg }
592
+ const [recent, setRecent] = useState([]);
593
+ const card = EXPORT_CARDS.find((c) => c.id === sel) || EXPORT_CARDS[0];
594
+ const validScopes = card.handoff ? [] : EXPORT_VALID_SCOPES[card.format] || ['artboard'];
595
+
596
+ // Keep the scope valid for the chosen format (pptx/zip etc. only allow a
597
+ // subset) — mirrors VALID_SCOPES_PER_FORMAT in the in-canvas dialog.
598
+ useEffect(() => {
599
+ if (validScopes.length && !validScopes.includes(scope)) setScope(validScopes[0]);
600
+ }, [validScopes, scope]);
601
+
602
+ const loadRecent = useCallback(() => {
603
+ fetch('/_api/export-history')
604
+ .then((r) => r.json())
605
+ .then((d) => setRecent(Array.isArray(d?.history) ? d.history.slice(0, 6) : []))
606
+ .catch(() => {});
607
+ }, []);
608
+ useEffect(() => {
609
+ loadRecent();
610
+ }, [loadRecent]);
611
+
612
+ useEffect(() => {
613
+ function onKey(e) {
614
+ if (e.key === 'Escape') {
615
+ e.preventDefault();
616
+ onClose();
617
+ }
618
+ }
619
+ window.addEventListener('keydown', onKey);
620
+ return () => window.removeEventListener('keydown', onKey);
621
+ }, [onClose]);
622
+
623
+ async function doExport() {
624
+ if (card.handoff) {
625
+ const p = activePath && activePath !== SYSTEM_TAB ? activePath : '<canvas>.tsx';
626
+ const cmd = `/design:handoff ${p}`;
627
+ try {
628
+ await navigator.clipboard?.writeText(cmd);
629
+ } catch {}
630
+ setStatus({ ok: true, msg: `Copied: ${cmd} — run it in Claude Code.` });
631
+ return;
632
+ }
633
+ setBusy(true);
634
+ setStatus(null);
635
+ const options = card.format === 'png' ? { scale } : {};
636
+ try {
637
+ const r = await fetch('/_api/export', {
638
+ method: 'POST',
639
+ headers: { 'content-type': 'application/json' },
640
+ body: JSON.stringify({ format: card.format, scope, options }),
641
+ });
642
+ if (!r.ok) {
643
+ setStatus({ ok: false, msg: (await r.text()) || `Export failed (${r.status})` });
644
+ setBusy(false);
645
+ return;
646
+ }
647
+ const disp = r.headers.get('Content-Disposition') || '';
648
+ const fn = /filename="([^"]+)"/.exec(disp);
649
+ const filename = (fn && fn[1]) || `export.${card.format}`;
650
+ const blob = await r.blob();
651
+ const url = URL.createObjectURL(blob);
652
+ const a = document.createElement('a');
653
+ a.href = url;
654
+ a.download = filename;
655
+ document.body.appendChild(a);
656
+ a.click();
657
+ a.remove();
658
+ URL.revokeObjectURL(url);
659
+ setStatus({ ok: true, msg: `Exported ${filename}` });
660
+ loadRecent();
661
+ } catch (err) {
662
+ setStatus({ ok: false, msg: err && err.message ? err.message : String(err) });
663
+ }
664
+ setBusy(false);
665
+ }
666
+
667
+ return (
668
+ <div
669
+ className="st-scrim"
670
+ role="presentation"
671
+ onMouseDown={(e) => {
672
+ if (e.target === e.currentTarget) onClose();
673
+ }}
674
+ >
675
+ <div className="st-dialog" role="dialog" aria-modal="true" aria-label="Export and handoff">
676
+ <div className="st-dialog-hd">
677
+ <span className="st-dialog-title">Export &amp; handoff</span>
678
+ <button type="button" className="st-iconbtn" aria-label="Close" onClick={onClose}>
679
+ <StIcon name="x" size={15} />
680
+ </button>
681
+ </div>
682
+ <div className="st-dialog-bd">
683
+ <div className="st-rp-hd">
684
+ {activePath && activePath !== SYSTEM_TAB
685
+ ? `Format · ${displayName(basename(activePath))}`
686
+ : 'Format'}
687
+ </div>
688
+ <div className="st-fmt-grid">
689
+ {EXPORT_CARDS.map((c) => (
690
+ <button
691
+ type="button"
692
+ key={c.id}
693
+ className={'st-fmt' + (c.id === sel ? ' is-on' : '')}
694
+ onClick={() => {
695
+ setSel(c.id);
696
+ setStatus(null);
697
+ }}
698
+ >
699
+ <StIcon name={c.icon} size={16} />
700
+ <span className="st-fmt-name">{c.label}</span>
701
+ <span className="st-fmt-sub">{c.sub}</span>
702
+ </button>
703
+ ))}
704
+ </div>
705
+ {!card.handoff && (
706
+ <div className="st-dialog-row">
707
+ <label className="st-dialog-lbl" htmlFor="st-export-scope">
708
+ Scope
709
+ </label>
710
+ <select
711
+ id="st-export-scope"
712
+ className="st-select"
713
+ value={scope}
714
+ onChange={(e) => setScope(e.target.value)}
715
+ >
716
+ {validScopes.map((s) => (
717
+ <option key={s} value={s}>
718
+ {EXPORT_SCOPE_LABELS[s]}
719
+ </option>
720
+ ))}
721
+ </select>
722
+ </div>
723
+ )}
724
+ {!card.handoff && card.format === 'png' && (
725
+ <div className="st-dialog-row">
726
+ <label className="st-dialog-lbl" htmlFor="st-export-size">
727
+ Size
728
+ </label>
729
+ <select
730
+ id="st-export-size"
731
+ className="st-select"
732
+ value={scale}
733
+ onChange={(e) => setScale(Number(e.target.value))}
734
+ >
735
+ {PNG_SCALES.map((s) => (
736
+ <option key={s.value} value={s.value}>
737
+ {s.label}
738
+ </option>
739
+ ))}
740
+ </select>
741
+ </div>
742
+ )}
743
+ {!card.handoff && card.format === 'png' && (
744
+ <div className="st-mono" style={{ fontSize: 11, color: 'var(--fg-3)' }}>
745
+ Resolution multiplier — {scale}× ≈ {1440 * scale}×{900 * scale} for a 1440×900 artboard.
746
+ </div>
747
+ )}
748
+ {card.handoff && (
749
+ <div className="callout callout--info" style={{ fontSize: 12 }}>
750
+ Hands the active canvas off to production. Copies{' '}
751
+ <span className="st-mono">/design:handoff &lt;path&gt;</span> — run it in Claude Code to
752
+ emit a ready-to-drop production component next to the canvas.
753
+ </div>
754
+ )}
755
+ {status && (
756
+ <div
757
+ className={'callout ' + (status.ok ? 'callout--success' : 'callout--error')}
758
+ style={{ fontSize: 12 }}
759
+ >
760
+ {status.msg}
761
+ </div>
762
+ )}
763
+ {recent.length > 0 && (
764
+ <div className="st-export-recent">
765
+ <div className="st-rp-hd">Recent</div>
766
+ {recent.map((h, i) => (
767
+ <div className="st-export-recent-row" key={i}>
768
+ <span>
769
+ {String(h.format || '').toUpperCase()} ·{' '}
770
+ {EXPORT_SCOPE_LABELS[h.scope] || h.scope}
771
+ </span>
772
+ <span className="st-mono">{h.filename}</span>
773
+ </div>
774
+ ))}
775
+ </div>
776
+ )}
777
+ </div>
778
+ <div className="st-dialog-ft">
779
+ <button type="button" className="btn btn--ghost" onClick={onClose}>
780
+ Cancel
781
+ </button>
782
+ <button type="button" className="btn btn--primary" disabled={busy} onClick={doExport}>
783
+ <StIcon name="download" size={14} />
784
+ {card.handoff
785
+ ? 'Copy handoff command'
786
+ : busy
787
+ ? 'Exporting…'
788
+ : `Export ${card.label}`}
789
+ </button>
790
+ </div>
791
+ </div>
792
+ </div>
793
+ );
794
+ }
795
+
301
796
  // ───── Tree (CV-08 spec) ─────
302
797
  // File rows use `.tp-row` with optional .dir / .sel / .star / .modified
303
798
  // modifiers + a leading `.glyph` (▾ open dir, ▸ closed dir / selected file,
@@ -317,14 +812,14 @@ function DirRow({ name, depth, defaultOpen, children }) {
317
812
  role="treeitem"
318
813
  aria-expanded={open}
319
814
  tabIndex={-1}
320
- className="tp-row dir"
815
+ className="st-row"
321
816
  style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
322
817
  onClick={() => setOpen((v) => !v)}
323
818
  >
324
- <span className="glyph" aria-hidden="true">
325
- {open ? '' : ''}
819
+ <span className="st-row-glyph">
820
+ <StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
326
821
  </span>
327
- <span className="name">{name}</span>
822
+ <span className="st-row-name">{name}</span>
328
823
  </button>
329
824
  {open && children}
330
825
  </Fragment>
@@ -340,30 +835,31 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
340
835
  return (
341
836
  <Fragment>
342
837
  <div
343
- className={'tp-row ds-folder' + (active ? ' sel' : '')}
838
+ className={'st-row st-ds-folder' + (active ? ' is-sel' : '')}
344
839
  style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
345
840
  role="treeitem"
346
841
  aria-expanded={open}
347
842
  >
348
843
  <button
349
844
  type="button"
350
- className="ds-folder-chev"
845
+ className="st-ds-chev"
351
846
  onClick={() => setOpen((v) => !v)}
352
847
  aria-label={open ? 'Collapse design system' : 'Expand design system'}
353
848
  title={open ? 'Collapse' : 'Expand'}
354
849
  >
355
- <span className="glyph" aria-hidden="true">
356
- {open ? '▾' : '▸'}
357
- </span>
850
+ <StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
358
851
  </button>
359
852
  <button
360
853
  type="button"
361
- className="ds-folder-open"
854
+ className="st-ds-open"
362
855
  onClick={() => onOpenSystem(dsName)}
363
856
  aria-label={`Open ${dsName} design system view`}
364
857
  title="Open the design system view"
365
858
  >
366
- <span className="name">{name}</span>
859
+ <span className="st-row-glyph">
860
+ <StIcon name="folder" size={13} />
861
+ </span>
862
+ <span className="st-row-name">{name}</span>
367
863
  </button>
368
864
  </div>
369
865
  {open && children}
@@ -371,7 +867,7 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
371
867
  );
372
868
  }
373
869
 
374
- function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar }) {
870
+ function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kind, sidecar }) {
375
871
  const isSel = file.path === activePath;
376
872
  const isCanvas = CANVAS_EXT_RE.test(file.name);
377
873
  // Non-canvas rows (PROJECT *.md, RUNTIME _active.json, ...) are display-only —
@@ -379,7 +875,10 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
379
875
  // hint via `aria-disabled`.
380
876
  const inert = !isCanvas;
381
877
  const label = isCanvas ? displayName(file.name) : file.name;
382
- return (
878
+ // Delete only real canvases in a deletable group (onDelete is undefined for the
879
+ // DS group + runtime files); the server enforces the rest.
880
+ const canDelete = isCanvas && typeof onDelete === 'function' && kind !== 'runtime';
881
+ const row = (
383
882
  <button
384
883
  type="button"
385
884
  role="treeitem"
@@ -387,10 +886,7 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
387
886
  aria-disabled={inert ? 'true' : undefined}
388
887
  tabIndex={isSel ? 0 : -1}
389
888
  className={
390
- 'tp-row' +
391
- (isSel ? ' sel' : '') +
392
- (kind === 'runtime' ? ' muted' : '') +
393
- (sidecar ? ' sidecar' : '')
889
+ 'st-row' + (isSel ? ' is-sel' : '') + (kind === 'runtime' ? ' is-muted' : '')
394
890
  }
395
891
  style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
396
892
  title={file.path + (oc ? ` — ${oc} open` : inert ? ' (file index only)' : '')}
@@ -398,13 +894,33 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
398
894
  if (!inert) onOpen(file.path);
399
895
  }}
400
896
  >
401
- <span className="glyph" aria-hidden="true">
402
- {isSel ? '▸' : '·'}
897
+ <span className="st-row-glyph">
898
+ <StIcon name="file" size={13} />
403
899
  </span>
404
- <span className="name">{label}</span>
405
- {oc > 0 && <span className="badge">{oc}</span>}
900
+ <span className="st-row-name">{label}</span>
901
+ {oc > 0 && <span className="st-row-badge">{oc}</span>}
406
902
  </button>
407
903
  );
904
+ if (!canDelete) return row;
905
+ // A sibling delete button (can't nest a button in the row button). The wrapper
906
+ // is presentational so the treeitem stays the tree's child for a11y.
907
+ return (
908
+ <div className="st-row-wrap" role="none">
909
+ {row}
910
+ <button
911
+ type="button"
912
+ className="st-row-del"
913
+ title={`Delete ${label}`}
914
+ aria-label={`Delete canvas ${label}`}
915
+ onClick={(e) => {
916
+ e.stopPropagation();
917
+ onDelete(file.path, label);
918
+ }}
919
+ >
920
+ <Icon d="M3 6h18 M8 6V4h8v2 M6 6l1 14h10l1-14 M10 11v6 M14 11v6" size={12} />
921
+ </button>
922
+ </div>
923
+ );
408
924
  }
409
925
 
410
926
  function CanvasRow({
@@ -414,6 +930,7 @@ function CanvasRow({
414
930
  kind,
415
931
  activePath,
416
932
  onOpen,
933
+ onDelete,
417
934
  openCount: oc,
418
935
  showHidden,
419
936
  forceOpen,
@@ -432,6 +949,7 @@ function CanvasRow({
432
949
  file={primary}
433
950
  activePath={activePath}
434
951
  onOpen={onOpen}
952
+ onDelete={onDelete}
435
953
  openCount={oc}
436
954
  depth={depth}
437
955
  kind={kind}
@@ -446,12 +964,12 @@ function CanvasRow({
446
964
  aria-selected={isSel}
447
965
  aria-expanded={open}
448
966
  tabIndex={isSel ? 0 : -1}
449
- className={'tp-row canvas-row' + (isSel ? ' sel' : '')}
967
+ className={'st-row st-canvas-row' + (isSel ? ' is-sel' : '')}
450
968
  style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
451
969
  title={primary.path}
452
970
  onClick={(e) => {
453
971
  // Click the chevron region → toggle disclosure. Click anywhere else → open canvas.
454
- if (e.target.closest('.canvas-chev')) {
972
+ if (e.target.closest('.st-canvas-chev')) {
455
973
  setOpenState((v) => !v);
456
974
  return;
457
975
  }
@@ -459,17 +977,16 @@ function CanvasRow({
459
977
  }}
460
978
  >
461
979
  <span
462
- className="glyph canvas-chev"
463
- aria-hidden="true"
980
+ className="st-row-glyph st-canvas-chev"
464
981
  onClick={(e) => {
465
982
  e.stopPropagation();
466
983
  setOpenState((v) => !v);
467
984
  }}
468
985
  >
469
- {open ? '' : ''}
986
+ <StIcon name={open ? 'chevron-down' : 'chevron-right'} size={13} />
470
987
  </span>
471
- <span className="name">{displayName(primary.name)}</span>
472
- {oc > 0 && <span className="badge">{oc}</span>}
988
+ <span className="st-row-name">{displayName(primary.name)}</span>
989
+ {oc > 0 && <span className="st-row-badge">{oc}</span>}
473
990
  </button>
474
991
  {open &&
475
992
  sidecars.map((sc) => (
@@ -500,6 +1017,7 @@ function Tree({
500
1017
  dsFolders,
501
1018
  activeDsName,
502
1019
  onOpenSystem,
1020
+ onDelete,
503
1021
  }) {
504
1022
  const dirs = Object.keys(node)
505
1023
  .filter((k) => k !== '_files')
@@ -535,6 +1053,7 @@ function Tree({
535
1053
  sidecars={entry.sidecars}
536
1054
  activePath={activePath}
537
1055
  onOpen={onOpen}
1056
+ onDelete={onDelete}
538
1057
  openCount={openCount(commentsByFile[entry.primary.path])}
539
1058
  depth={depth}
540
1059
  kind={kind}
@@ -555,6 +1074,7 @@ function Tree({
555
1074
  kind={kind}
556
1075
  />
557
1076
  ))}
1077
+ {/* orphans are sidecars/loose files — no canvas to delete, so no onDelete */}
558
1078
  {dirs.map((d) => {
559
1079
  const dsMatch = dsFolderByName?.get(d);
560
1080
  const childTree = (
@@ -569,6 +1089,7 @@ function Tree({
569
1089
  search={search}
570
1090
  activeDsName={activeDsName}
571
1091
  onOpenSystem={onOpenSystem}
1092
+ onDelete={onDelete}
572
1093
  />
573
1094
  );
574
1095
  if (dsMatch && onOpenSystem) {
@@ -630,12 +1151,39 @@ function Sidebar({
630
1151
  showHidden,
631
1152
  sectionsExpanded,
632
1153
  onToggleSection,
1154
+ onNewBoard,
1155
+ onDeleteBoard,
1156
+ collapsed,
1157
+ onCollapse,
633
1158
  }) {
634
1159
  const filteredGroups = useMemo(() => {
635
1160
  if (!search) return groups;
636
1161
  return groups.map((g) => ({ ...g, tree: filterTree(g.tree, search), filtered: !!search }));
637
1162
  }, [groups, search]);
638
1163
 
1164
+ // Phase 22 — inline "new brief board" composer in the tree header. Click +,
1165
+ // type a name, Enter to create (Esc cancels). The board opens active so it's
1166
+ // ready to annotate; generation (ingest) still goes through /design:new.
1167
+ const [creating, setCreating] = useState(false);
1168
+ const [newName, setNewName] = useState('');
1169
+ const [newErr, setNewErr] = useState('');
1170
+ const [newBusy, setNewBusy] = useState(false);
1171
+
1172
+ const submitNewBoard = useCallback(async () => {
1173
+ const name = newName.trim();
1174
+ if (!name || newBusy) return;
1175
+ setNewBusy(true);
1176
+ setNewErr('');
1177
+ const res = await onNewBoard(name);
1178
+ setNewBusy(false);
1179
+ if (res?.ok) {
1180
+ setCreating(false);
1181
+ setNewName('');
1182
+ } else {
1183
+ setNewErr(res?.error || 'could not create board');
1184
+ }
1185
+ }, [newName, newBusy, onNewBoard]);
1186
+
639
1187
  // Mock uses `42 / 42` — total openable canvases, not every listed file.
640
1188
  // We count canvas files (TSX Phase 3.6+ default, HTML legacy) so the counter
641
1189
  // matches "canvases you can mount".
@@ -652,41 +1200,109 @@ function Sidebar({
652
1200
  }, [filteredGroups]);
653
1201
 
654
1202
  return (
655
- <nav className="sidebar">
656
- <div className="tree-panel-hd">
657
- <span>FILES</span>
658
- <span className="ct" title={wsConnected ? 'live · file index synced' : 'reconnecting…'}>
659
- <span className={'live-dot' + (wsConnected ? ' connected' : '')} aria-hidden="true" />
660
- {htmlShown} / {htmlCount}
661
- </span>
1203
+ <nav className={'st-sidebar' + (collapsed ? ' is-collapsed' : '')} aria-label="Files">
1204
+ <div className="st-sb-hd">
1205
+ <span className="st-sb-title">Files</span>
1206
+ <div className="st-sb-hd-actions">
1207
+ <button
1208
+ type="button"
1209
+ className="st-iconbtn"
1210
+ title="New blank brief board"
1211
+ aria-label="New blank brief board"
1212
+ aria-expanded={creating}
1213
+ onClick={() => {
1214
+ setNewErr('');
1215
+ setCreating((v) => !v);
1216
+ }}
1217
+ >
1218
+ <StIcon name="plus" size={15} />
1219
+ </button>
1220
+ <span className="st-live" title={wsConnected ? 'live · file index synced' : 'reconnecting…'}>
1221
+ <span className={'st-live-dot' + (wsConnected ? ' is-connected' : '')} aria-hidden="true" />
1222
+ {htmlShown} / {htmlCount}
1223
+ </span>
1224
+ {onCollapse && (
1225
+ <button
1226
+ type="button"
1227
+ className="st-iconbtn"
1228
+ aria-label="Collapse sidebar"
1229
+ title="Collapse sidebar (T)"
1230
+ onClick={onCollapse}
1231
+ >
1232
+ <StIcon name="panel-left" size={15} />
1233
+ </button>
1234
+ )}
1235
+ </div>
662
1236
  </div>
663
1237
 
664
- <div className="tree-panel-search">
665
- <Icon d="M21 21l-4.35-4.35 M11 19a8 8 0 100-16 8 8 0 000 16z" size={12} />
666
- <input
667
- type="search"
668
- placeholder="filter (⌘F)"
669
- value={search}
670
- onChange={(e) => setSearch(e.target.value)}
671
- aria-label="Filter files"
672
- />
673
- {search ? (
1238
+ {creating ? (
1239
+ <div className="st-newboard">
1240
+ <input
1241
+ type="text"
1242
+ // biome-ignore lint/a11y/noAutofocus: deliberate — the composer opens on an explicit click.
1243
+ autoFocus
1244
+ placeholder="brief board name…"
1245
+ value={newName}
1246
+ maxLength={60}
1247
+ disabled={newBusy}
1248
+ aria-label="New brief board name"
1249
+ onChange={(e) => setNewName(e.target.value)}
1250
+ onKeyDown={(e) => {
1251
+ if (e.key === 'Enter') {
1252
+ e.preventDefault();
1253
+ submitNewBoard();
1254
+ } else if (e.key === 'Escape') {
1255
+ e.preventDefault();
1256
+ setCreating(false);
1257
+ setNewName('');
1258
+ setNewErr('');
1259
+ }
1260
+ }}
1261
+ />
674
1262
  <button
675
- className="search-clear"
676
- onClick={() => setSearch('')}
677
- title="Clear (Esc)"
678
- aria-label="Clear search"
1263
+ type="button"
1264
+ className="st-newboard-go"
1265
+ disabled={newBusy || !newName.trim()}
1266
+ title="Create (Enter)"
1267
+ aria-label="Create brief board"
1268
+ onClick={submitNewBoard}
679
1269
  >
680
- ×
1270
+ {newBusy ? '…' : '↵'}
681
1271
  </button>
682
- ) : (
683
- <span className="search-kbd" aria-hidden="true">
684
- /
685
- </span>
686
- )}
1272
+ </div>
1273
+ ) : null}
1274
+ {newErr ? (
1275
+ <div className="st-newboard-err" role="alert">
1276
+ {newErr}
1277
+ </div>
1278
+ ) : null}
1279
+
1280
+ <div className="st-search">
1281
+ <div className="st-search-box">
1282
+ <StIcon name="search" size={13} />
1283
+ <input
1284
+ type="search"
1285
+ placeholder="Search canvases…"
1286
+ value={search}
1287
+ onChange={(e) => setSearch(e.target.value)}
1288
+ aria-label="Filter files"
1289
+ />
1290
+ {search ? (
1291
+ <button
1292
+ className="st-search-clear"
1293
+ onClick={() => setSearch('')}
1294
+ title="Clear (Esc)"
1295
+ aria-label="Clear search"
1296
+ >
1297
+ ×
1298
+ </button>
1299
+ ) : (
1300
+ <Kbd>/</Kbd>
1301
+ )}
1302
+ </div>
687
1303
  </div>
688
1304
 
689
- <div className="tree-panel-body" role="tree" aria-label="Project file tree">
1305
+ <div className="st-tree" role="tree" aria-label="Project file tree">
690
1306
  {filteredGroups.map((g) => {
691
1307
  // Hide gitignored runtime / orphan-only project sections by default.
692
1308
  // Active search overrides — if the user typed a query, they want hits
@@ -711,21 +1327,18 @@ function Sidebar({
711
1327
  const explicit = sectionsExpanded[g.label];
712
1328
  // Active search forces every section open so hits aren't hidden.
713
1329
  const sectionOpen = !!search || (explicit === undefined ? defaultOpen : explicit);
714
- const chev = sectionOpen ? '▾' : '▸';
715
1330
  return (
716
- <Fragment key={g.label}>
1331
+ <div className="st-tree-section" key={g.label}>
717
1332
  <button
718
1333
  type="button"
719
- className="tp-section-hd clickable section-toggle"
1334
+ className="st-tree-sec-hd"
720
1335
  onClick={() => onToggleSection(g.label, defaultOpen)}
721
1336
  aria-expanded={sectionOpen}
722
1337
  title={sectionOpen ? 'Collapse section' : 'Expand section'}
723
1338
  >
724
- <span className="chev" aria-hidden="true">
725
- {chev}
726
- </span>
727
- <span className="section-label">{meta.title}</span>
728
- {pill && <span className="pill">{pill}</span>}
1339
+ <StIcon name={sectionOpen ? 'chevron-down' : 'chevron-right'} size={13} />
1340
+ <span className="st-sec-name">{meta.title}</span>
1341
+ {pill && <span className="st-pill">{pill}</span>}
729
1342
  </button>
730
1343
  {sectionOpen &&
731
1344
  (hasItems ? (
@@ -741,11 +1354,12 @@ function Sidebar({
741
1354
  dsFolders={g.dsFolders}
742
1355
  activeDsName={activeDsName}
743
1356
  onOpenSystem={isDs ? onOpenSystem : undefined}
1357
+ onDelete={isDs ? undefined : onDeleteBoard}
744
1358
  />
745
1359
  ) : (
746
- <div className="tp-empty">{search ? 'No matches.' : 'Empty.'}</div>
1360
+ <div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
747
1361
  ))}
748
- </Fragment>
1362
+ </div>
749
1363
  );
750
1364
  })}
751
1365
  </div>
@@ -753,9 +1367,29 @@ function Sidebar({
753
1367
  );
754
1368
  }
755
1369
 
1370
+ // Collapsed rail — a thin strip shown when the sidebar is collapsed, with a
1371
+ // re-open affordance + quick search/files shortcuts (mockup CollapsedRail).
1372
+ function CollapsedRail({ shown, onExpand, onSearch }) {
1373
+ return (
1374
+ <div className={'st-rail' + (shown ? ' is-shown' : '')}>
1375
+ <div className="st-rail-inner">
1376
+ <button type="button" className="st-iconbtn" aria-label="Expand sidebar" title="Expand sidebar (T)" onClick={onExpand}>
1377
+ <StIcon name="panel-left" size={15} />
1378
+ </button>
1379
+ <button type="button" className="st-iconbtn" aria-label="Search" title="Search (/)" onClick={onSearch}>
1380
+ <StIcon name="search" size={15} />
1381
+ </button>
1382
+ <button type="button" className="st-iconbtn" aria-label="Files" title="Files" onClick={onExpand}>
1383
+ <StIcon name="folder" size={15} />
1384
+ </button>
1385
+ </div>
1386
+ </div>
1387
+ );
1388
+ }
1389
+
756
1390
  // Help modal — hosts the cheatsheet that used to live in the left sidebar.
757
1391
  // Triggered from the menubar's Help item. Esc + backdrop click close it.
758
- function HelpModal({ open, onClose }) {
1392
+ function HelpModal({ open, onClose, onStartTour }) {
759
1393
  useEffect(() => {
760
1394
  if (!open) return;
761
1395
  function onKey(e) {
@@ -784,6 +1418,16 @@ function HelpModal({ open, onClose }) {
784
1418
  Help · shortcuts &amp; commands
785
1419
  </span>
786
1420
  <span className="sku">MDCC-DEV-SRV / v{MDCC_VERSION}</span>
1421
+ {onStartTour && (
1422
+ <button
1423
+ type="button"
1424
+ className="mdcc-tour__back"
1425
+ style={{ marginLeft: 'auto' }}
1426
+ onClick={onStartTour}
1427
+ >
1428
+ ▶ Take the tour
1429
+ </button>
1430
+ )}
787
1431
  <button
788
1432
  type="button"
789
1433
  className="help-modal-close"
@@ -1067,13 +1711,14 @@ function HelpModal({ open, onClose }) {
1067
1711
 
1068
1712
  const MENU_NAMES = ['File', 'Edit', 'View', 'Selection', 'Tools', 'Help'];
1069
1713
 
1070
- function ViewDropdown({ panels, onToggle, onClose }) {
1714
+ // Shared close-on-Esc / outside-click effect for the menubar dropdowns.
1715
+ function useDropdownClose(onClose) {
1071
1716
  useEffect(() => {
1072
1717
  function onKey(e) {
1073
1718
  if (e.key === 'Escape') onClose();
1074
1719
  }
1075
1720
  function onDocClick(e) {
1076
- if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
1721
+ if (!e.target.closest('.st-dropdown, .st-menu')) onClose();
1077
1722
  }
1078
1723
  window.addEventListener('keydown', onKey);
1079
1724
  window.addEventListener('mousedown', onDocClick);
@@ -1082,16 +1727,19 @@ function ViewDropdown({ panels, onToggle, onClose }) {
1082
1727
  window.removeEventListener('mousedown', onDocClick);
1083
1728
  };
1084
1729
  }, [onClose]);
1730
+ }
1085
1731
 
1732
+ function ViewDropdown({ panels, onToggle, onClose }) {
1733
+ useDropdownClose(onClose);
1086
1734
  return (
1087
- <div className="mb-dropdown" role="menu" aria-label="View" style={{ left: '146px' }}>
1088
- <div className="mb-dd-hd">Panels</div>
1735
+ <div className="st-dropdown" role="menu" aria-label="View" style={{ left: 152 }}>
1736
+ <div className="st-dd-hd">Panels</div>
1089
1737
  {panels.map((p) => (
1090
1738
  <button
1091
1739
  key={p.id}
1092
1740
  type="button"
1093
1741
  role="menuitem"
1094
- className={'mb-dd-item' + (p.checked ? ' active' : '')}
1742
+ className={'st-dd-item' + (p.checked ? ' is-on' : '')}
1095
1743
  aria-disabled={p.disabled ? 'true' : undefined}
1096
1744
  onClick={() => {
1097
1745
  if (!p.disabled) {
@@ -1100,37 +1748,27 @@ function ViewDropdown({ panels, onToggle, onClose }) {
1100
1748
  }
1101
1749
  }}
1102
1750
  >
1103
- <span className="lbl">
1104
- <span className="check">{p.checked ? '✓' : ''}</span>
1751
+ <span className="st-dd-lead">
1752
+ <span className="st-dd-check">{p.checked ? <StIcon name="check" size={13} /> : null}</span>
1105
1753
  <span>{p.label}</span>
1106
1754
  </span>
1107
- {p.phase ? (
1108
- <span className="phase-tag">{p.phase}</span>
1109
- ) : (
1110
- <span className="shortcut">{p.shortcut || ''}</span>
1111
- )}
1755
+ {p.phase ? <span className="st-dd-phase">{p.phase}</span> : <Kbd>{p.shortcut || ''}</Kbd>}
1112
1756
  </button>
1113
1757
  ))}
1114
- <div className="mb-dd-sep" />
1115
- <div className="mb-dd-hd">Zoom</div>
1758
+ <div className="st-dd-sep" />
1759
+ <div className="st-dd-hd">Zoom</div>
1116
1760
  {[
1117
1761
  { label: 'Zoom In', shortcut: '⌘ +' },
1118
1762
  { label: 'Zoom Out', shortcut: '⌘ −' },
1119
1763
  { label: 'Fit to Screen', shortcut: '⌘ 0' },
1120
1764
  { label: 'Actual Size · 100 %', shortcut: '⌥ ⌘ 0' },
1121
1765
  ].map((z) => (
1122
- <button
1123
- key={z.label}
1124
- type="button"
1125
- role="menuitem"
1126
- className="mb-dd-item"
1127
- aria-disabled="true"
1128
- >
1129
- <span className="lbl">
1130
- <span className="check" />
1766
+ <button key={z.label} type="button" role="menuitem" className="st-dd-item" aria-disabled="true">
1767
+ <span className="st-dd-lead">
1768
+ <span className="st-dd-check" />
1131
1769
  <span>{z.label}</span>
1132
1770
  </span>
1133
- <span className="phase-tag">Phase 4</span>
1771
+ <span className="st-dd-phase">Phase 4</span>
1134
1772
  </button>
1135
1773
  ))}
1136
1774
  </div>
@@ -1141,42 +1779,29 @@ function ViewDropdown({ panels, onToggle, onClose }) {
1141
1779
  // Phase 5.1 — Selection + Tools dropdowns (mirror ViewDropdown shape).
1142
1780
 
1143
1781
  function SelectionDropdown({ onAction, onClose }) {
1144
- useEffect(() => {
1145
- function onKey(e) {
1146
- if (e.key === 'Escape') onClose();
1147
- }
1148
- function onDocClick(e) {
1149
- if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
1150
- }
1151
- window.addEventListener('keydown', onKey);
1152
- window.addEventListener('mousedown', onDocClick);
1153
- return () => {
1154
- window.removeEventListener('keydown', onKey);
1155
- window.removeEventListener('mousedown', onDocClick);
1156
- };
1157
- }, [onClose]);
1782
+ useDropdownClose(onClose);
1158
1783
  const items = [
1159
1784
  { id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
1160
1785
  { id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⌘ ⇧ A' },
1161
1786
  ];
1162
1787
  return (
1163
- <div className="mb-dropdown" role="menu" aria-label="Selection" style={{ left: '195px' }}>
1788
+ <div className="st-dropdown" role="menu" aria-label="Selection" style={{ left: 214 }}>
1164
1789
  {items.map((it) => (
1165
1790
  <button
1166
1791
  key={it.id}
1167
1792
  type="button"
1168
1793
  role="menuitem"
1169
- className="mb-dd-item"
1794
+ className="st-dd-item"
1170
1795
  onClick={() => {
1171
1796
  onAction(it.id);
1172
1797
  onClose();
1173
1798
  }}
1174
1799
  >
1175
- <span className="lbl">
1176
- <span className="check" />
1800
+ <span className="st-dd-lead">
1801
+ <span className="st-dd-check" />
1177
1802
  <span>{it.label}</span>
1178
1803
  </span>
1179
- <span className="shortcut">{it.shortcut}</span>
1804
+ <Kbd>{it.shortcut}</Kbd>
1180
1805
  </button>
1181
1806
  ))}
1182
1807
  </div>
@@ -1184,21 +1809,8 @@ function SelectionDropdown({ onAction, onClose }) {
1184
1809
  }
1185
1810
 
1186
1811
  function ToolsDropdown({ onAction, onClose }) {
1187
- useEffect(() => {
1188
- function onKey(e) {
1189
- if (e.key === 'Escape') onClose();
1190
- }
1191
- function onDocClick(e) {
1192
- if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
1193
- }
1194
- window.addEventListener('keydown', onKey);
1195
- window.addEventListener('mousedown', onDocClick);
1196
- return () => {
1197
- window.removeEventListener('keydown', onKey);
1198
- window.removeEventListener('mousedown', onDocClick);
1199
- };
1200
- }, [onClose]);
1201
- // Mirrors DEFAULT_TOOLS in plugins/design/dev-server/use-tool-mode.tsx —
1812
+ useDropdownClose(onClose);
1813
+ // Mirrors DEFAULT_TOOLS in apps/studio/use-tool-mode.tsx —
1202
1814
  // kept in sync by hand because the menubar lives in the dev-server shell
1203
1815
  // (no shared bundle with the canvas iframes).
1204
1816
  const tools = [
@@ -1214,25 +1826,102 @@ function ToolsDropdown({ onAction, onClose }) {
1214
1826
  { id: 'eraser', label: 'Eraser', shortcut: 'E' },
1215
1827
  ];
1216
1828
  return (
1217
- <div className="mb-dropdown" role="menu" aria-label="Tools" style={{ left: '253px' }}>
1218
- {tools.map((t) => (
1219
- <button
1220
- key={t.id}
1221
- type="button"
1222
- role="menuitem"
1223
- className="mb-dd-item"
1224
- onClick={() => {
1225
- onAction(t.id);
1226
- onClose();
1227
- }}
1228
- >
1229
- <span className="lbl">
1230
- <span className="check" />
1231
- <span>{t.label}</span>
1232
- </span>
1233
- <span className="shortcut">{t.shortcut}</span>
1234
- </button>
1235
- ))}
1829
+ <div className="st-dropdown" role="menu" aria-label="Tools" style={{ left: 290 }}>
1830
+ <div className="st-dd-hd">Tool palette</div>
1831
+ {tools.map((t) => (
1832
+ <button
1833
+ key={t.id}
1834
+ type="button"
1835
+ role="menuitem"
1836
+ className="st-dd-item"
1837
+ onClick={() => {
1838
+ onAction(t.id);
1839
+ onClose();
1840
+ }}
1841
+ >
1842
+ <span className="st-dd-lead">
1843
+ <span className="st-dd-check" />
1844
+ <span>{t.label}</span>
1845
+ </span>
1846
+ <Kbd>{t.shortcut}</Kbd>
1847
+ </button>
1848
+ ))}
1849
+ </div>
1850
+ );
1851
+ }
1852
+
1853
+ // Plan C follow-up — File + Edit menus, previously inert. Both dispatch to real
1854
+ // shell flows (File) or the in-canvas undo stack / selection bridges (Edit).
1855
+ function FileDropdown({ onAction, onClose }) {
1856
+ useDropdownClose(onClose);
1857
+ const items = [
1858
+ { id: 'new', label: 'New canvas…', shortcut: '⌘N' },
1859
+ { id: 'export', label: 'Export…', shortcut: '⇧⌘E' },
1860
+ { id: 'handoff', label: 'Handoff to production', shortcut: '⇧⌘H' },
1861
+ { sep: true },
1862
+ { id: 'reload', label: 'Reload canvas', shortcut: '⌘R' },
1863
+ ];
1864
+ return (
1865
+ <div className="st-dropdown" role="menu" aria-label="File" style={{ left: 40 }}>
1866
+ {items.map((it, i) =>
1867
+ it.sep ? (
1868
+ <div key={'s' + i} className="st-dd-sep" />
1869
+ ) : (
1870
+ <button
1871
+ key={it.id}
1872
+ type="button"
1873
+ role="menuitem"
1874
+ className="st-dd-item"
1875
+ onClick={() => {
1876
+ onAction(it.id);
1877
+ onClose();
1878
+ }}
1879
+ >
1880
+ <span className="st-dd-lead">
1881
+ <span className="st-dd-check" />
1882
+ <span>{it.label}</span>
1883
+ </span>
1884
+ <Kbd>{it.shortcut}</Kbd>
1885
+ </button>
1886
+ )
1887
+ )}
1888
+ </div>
1889
+ );
1890
+ }
1891
+
1892
+ function EditDropdown({ onAction, onClose }) {
1893
+ useDropdownClose(onClose);
1894
+ const items = [
1895
+ { id: 'undo', label: 'Undo', shortcut: '⌘Z' },
1896
+ { id: 'redo', label: 'Redo', shortcut: '⇧⌘Z' },
1897
+ { sep: true },
1898
+ { id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
1899
+ { id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⇧⌘A' },
1900
+ ];
1901
+ return (
1902
+ <div className="st-dropdown" role="menu" aria-label="Edit" style={{ left: 90 }}>
1903
+ {items.map((it, i) =>
1904
+ it.sep ? (
1905
+ <div key={'s' + i} className="st-dd-sep" />
1906
+ ) : (
1907
+ <button
1908
+ key={it.id}
1909
+ type="button"
1910
+ role="menuitem"
1911
+ className="st-dd-item"
1912
+ onClick={() => {
1913
+ onAction(it.id);
1914
+ onClose();
1915
+ }}
1916
+ >
1917
+ <span className="st-dd-lead">
1918
+ <span className="st-dd-check" />
1919
+ <span>{it.label}</span>
1920
+ </span>
1921
+ <Kbd>{it.shortcut}</Kbd>
1922
+ </button>
1923
+ )
1924
+ )}
1236
1925
  </div>
1237
1926
  );
1238
1927
  }
@@ -1254,6 +1943,15 @@ function Menubar({
1254
1943
  annotationsVisible,
1255
1944
  onToggleAnnotations,
1256
1945
  postToActiveCanvas,
1946
+ onOpenWhatsNew,
1947
+ whatsNewCount,
1948
+ artboardCount = 0,
1949
+ presence = null,
1950
+ inspectorOpen,
1951
+ onToggleInspector,
1952
+ onNewCanvas,
1953
+ onOpenExport,
1954
+ onReload,
1257
1955
  }) {
1258
1956
  const isSystem = activePath === SYSTEM_TAB;
1259
1957
  const stamp = isSystem ? 'SYSTEM' : activePath ? 'CANVAS' : 'IDLE';
@@ -1284,7 +1982,7 @@ function Menubar({
1284
1982
  disabled: false,
1285
1983
  },
1286
1984
  { id: 'layers', label: 'Layers Panel', phase: 'Phase 12', disabled: true },
1287
- { id: 'inspector', label: 'Inspector', phase: 'Phase 12', disabled: true },
1985
+ { id: 'inspector', label: 'Inspector', shortcut: 'I', checked: inspectorOpen, disabled: false },
1288
1986
  {
1289
1987
  id: 'annotate',
1290
1988
  label: 'Annotations',
@@ -1295,8 +1993,9 @@ function Menubar({
1295
1993
  { id: 'present', label: 'Presentation Mode', phase: 'Phase 6', disabled: true },
1296
1994
  ];
1297
1995
 
1996
+ const DROPDOWN_MENUS = ['file', 'edit', 'view', 'selection', 'tools'];
1298
1997
  function onMenuClick(key) {
1299
- if (key === 'view' || key === 'selection' || key === 'tools') {
1998
+ if (DROPDOWN_MENUS.includes(key)) {
1300
1999
  setOpenMenu(openMenu === key ? null : key);
1301
2000
  } else if (key === 'help') {
1302
2001
  setOpenMenu(null);
@@ -1305,34 +2004,63 @@ function Menubar({
1305
2004
  }
1306
2005
 
1307
2006
  return (
1308
- <header className="mb" role="menubar" aria-label="Application menubar">
1309
- <span className="mb-brand">
1310
- <span className="dot" aria-hidden="true" />
1311
- <span>maude</span>
2007
+ <header className="st-menubar" role="menubar" aria-label="Application menubar">
2008
+ <span className="st-brand">
2009
+ <span className="st-brand-mark">
2010
+ <svg viewBox="0 0 32 32" width="100%" height="100%" fill="none" aria-hidden="true"><path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" /></svg>
2011
+ </span>
2012
+ <span className="st-brand-name">maude</span>
1312
2013
  </span>
1313
- <nav className="mb-menus" aria-label="Application menus">
2014
+ <nav className="st-menus" aria-label="Application menus">
1314
2015
  {MENU_NAMES.map((name) => {
1315
2016
  const key = name.toLowerCase();
1316
- const hasDropdown = key === 'view' || key === 'selection' || key === 'tools';
2017
+ const hasDropdown = DROPDOWN_MENUS.includes(key);
1317
2018
  const interactive = hasDropdown || key === 'help';
1318
2019
  const open = openMenu === key;
1319
2020
  return (
1320
2021
  <button
1321
2022
  key={key}
1322
2023
  type="button"
1323
- className="mb-menu"
2024
+ className="st-menu"
1324
2025
  role="menuitem"
2026
+ data-tour={key === 'help' ? 'help' : undefined}
1325
2027
  aria-haspopup={hasDropdown ? 'menu' : undefined}
1326
2028
  aria-expanded={hasDropdown ? open : undefined}
1327
- aria-disabled={interactive ? undefined : 'true'}
1328
- title={interactive ? '' : 'Coming in a later phase'}
1329
2029
  onClick={() => onMenuClick(key)}
2030
+ // F4 — once any menu is open, hovering another trigger switches to
2031
+ // it (base-ui menubar behavior). Only among dropdown menus.
2032
+ onMouseEnter={() => {
2033
+ if (openMenu !== null && hasDropdown) setOpenMenu(key);
2034
+ }}
1330
2035
  >
1331
2036
  {name}
1332
2037
  </button>
1333
2038
  );
1334
2039
  })}
1335
2040
  </nav>
2041
+ {openMenu === 'file' && (
2042
+ <FileDropdown
2043
+ onAction={(id) => {
2044
+ if (id === 'new') onNewCanvas?.();
2045
+ else if (id === 'export') onOpenExport?.('export');
2046
+ else if (id === 'handoff') onOpenExport?.('handoff');
2047
+ else if (id === 'reload') onReload?.();
2048
+ }}
2049
+ onClose={() => setOpenMenu(null)}
2050
+ />
2051
+ )}
2052
+ {openMenu === 'edit' && (
2053
+ <EditDropdown
2054
+ onAction={(id) => {
2055
+ if (id === 'undo') postToActiveCanvas({ dgn: 'undo' });
2056
+ else if (id === 'redo') postToActiveCanvas({ dgn: 'redo' });
2057
+ else if (id === 'deselect-all') postToActiveCanvas({ dgn: 'selection-clear' });
2058
+ else if (id === 'select-all-annotations')
2059
+ postToActiveCanvas({ dgn: 'annotation-select-all' });
2060
+ }}
2061
+ onClose={() => setOpenMenu(null)}
2062
+ />
2063
+ )}
1336
2064
  {openMenu === 'view' && (
1337
2065
  <ViewDropdown
1338
2066
  panels={panels}
@@ -1341,6 +2069,7 @@ function Menubar({
1341
2069
  else if (id === 'comments') onToggleComments();
1342
2070
  else if (id === 'hidden') onToggleShowHidden();
1343
2071
  else if (id === 'annotate') onToggleAnnotations();
2072
+ else if (id === 'inspector') onToggleInspector();
1344
2073
  }}
1345
2074
  onClose={() => setOpenMenu(null)}
1346
2075
  />
@@ -1361,65 +2090,34 @@ function Menubar({
1361
2090
  onClose={() => setOpenMenu(null)}
1362
2091
  />
1363
2092
  )}
1364
- <div className="mb-spacer" />
1365
- <div className="mb-status">
1366
- <span className="cv-stamp">{stamp}</span>
1367
- <span className="file" title={activePath || ''}>
2093
+ <div className="st-mb-right">
2094
+ {presence ? <div className="st-presence">{presence}</div> : null}
2095
+ <button
2096
+ type="button"
2097
+ className="st-whatsnew"
2098
+ data-unseen={whatsNewCount > 0 ? 'true' : 'false'}
2099
+ aria-label={`What's new${whatsNewCount > 0 ? ` — ${whatsNewCount} unseen` : ''}`}
2100
+ title="What's new"
2101
+ onClick={onOpenWhatsNew}
2102
+ >
2103
+ <StIcon name="sparkle" size={15} />
2104
+ </button>
2105
+ <span className="st-stamp">{stamp}</span>
2106
+ <span className="st-mb-file" title={activePath || ''}>
1368
2107
  {fileLabel}
1369
2108
  </span>
1370
- <span className="sep" />
1371
- <span>
1372
- <span className="accent-dot">●</span> <b>{tabsCount}</b> ARTBOARDS
1373
- </span>
1374
- <span className="sep" />
1375
- <span title="Pan/zoom in Phase 4">
1376
- ZOOM <b>100%</b>
1377
- </span>
1378
- <span className="sep" />
1379
- <span className="ok">
1380
- <b>{project || 'MDCC'}</b>
2109
+ <span className="st-mb-sep" />
2110
+ <span className="st-mb-count" title="Artboards in the open canvas">
2111
+ <span className="st-dot" style={{ background: 'var(--accent)' }} />
2112
+ {artboardCount} ARTBOARDS
1381
2113
  </span>
2114
+ <span className="st-mb-sep" />
2115
+ <span className="st-mb-proj">{project || 'maude'}</span>
1382
2116
  </div>
1383
2117
  </header>
1384
2118
  );
1385
2119
  }
1386
2120
 
1387
- function ThemeToggle({ theme, onToggle }) {
1388
- // Show the icon of the theme you'll switch TO — clearer affordance than current state.
1389
- // Sun + Moon paths are condensed Lucide-style (single-path so the existing <Icon> works).
1390
- const sun =
1391
- 'M12 7a5 5 0 100 10 5 5 0 000-10z M12 3v1 M12 20v1 M21 12h-1 M4 12H3 M16.95 7.05l-.71.71 M7.05 16.95l-.71.71 M16.95 16.95l-.71-.71 M7.05 7.05l-.71-.71';
1392
- const moon = 'M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z';
1393
- const next = theme === 'dark' ? 'light' : 'dark';
1394
- return (
1395
- <button
1396
- type="button"
1397
- className="theme-toggle"
1398
- onClick={onToggle}
1399
- title={`Switch to ${next} theme`}
1400
- aria-label={`Switch to ${next} theme`}
1401
- >
1402
- <Icon d={theme === 'dark' ? sun : moon} size={14} />
1403
- <span className="theme-toggle-label">{next}</span>
1404
- </button>
1405
- );
1406
- }
1407
-
1408
- function Wordmark({ project, port, version }) {
1409
- return (
1410
- <div className="wm" aria-label="maude design server">
1411
- <span className="wm-glyph">maude-design-server</span>
1412
- <span className="wm-sub">
1413
- <span>CANVAS · {(project || 'MAUDE').toUpperCase()}</span>
1414
- <span className="wm-sep">/</span>
1415
- <b>v{version}</b>
1416
- <span className="wm-sep">/</span>
1417
- <span>localhost:{port || '4399'}</span>
1418
- </span>
1419
- </div>
1420
- );
1421
- }
1422
-
1423
2121
  function Viewport({
1424
2122
  tabs,
1425
2123
  activePath,
@@ -1431,35 +2129,38 @@ function Viewport({
1431
2129
  cfg,
1432
2130
  }) {
1433
2131
  return (
1434
- <div className="viewport">
2132
+ <div className="viewport st-stage">
1435
2133
  {tabs.length === 0 && (
1436
- <>
1437
- <Wordmark
1438
- project={project}
1439
- port={typeof window !== 'undefined' ? window.location.port : ''}
1440
- version={MDCC_VERSION}
1441
- />
1442
- <div className="empty-state">
1443
- <div className="big">No mock open</div>
1444
- <div className="small">
1445
- ← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open
1446
- the <strong>Design system</strong> view above it.
1447
- <br />
1448
- <br />
1449
- Tabs work like in an editor — close with the × on each tab. <kbd>⌘R</kbd> reloads the
1450
- active iframe.
1451
- <br />
1452
- <br />
1453
- <strong>Element selection:</strong> hold <kbd>⌘</kbd> inside the canvas and hover for
1454
- a preview, click to select. <kbd>⌘⇧</kbd>+click adds to a multi-selection.{' '}
1455
- <kbd>V</kbd>/<kbd>H</kbd>/<kbd>C</kbd> swap tool; right-click opens the context menu.
1456
- <br />
1457
- <br />
1458
- Active file, selection, and comments are tracked in <code>_active.json</code> +{' '}
1459
- <code>_comments/</code> — Claude reads them when you run <code>/design</code>.
1460
- </div>
2134
+ <div className="st-empty">
2135
+ <div className="st-empty-brand">
2136
+ <span className="st-brand-mark">
2137
+ <svg viewBox="0 0 32 32" width="100%" height="100%" fill="none" aria-hidden="true"><path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" /></svg>
2138
+ </span>
2139
+ <span className="st-empty-wm">maude</span>
2140
+ <span className="st-empty-sub st-mono">
2141
+ CANVAS · {(project || 'MAUDE').toUpperCase()} / v{MDCC_VERSION} /
2142
+ localhost:{typeof window !== 'undefined' ? window.location.port : '4399'}
2143
+ </span>
1461
2144
  </div>
1462
- </>
2145
+ <div className="st-empty-title">No canvas open</div>
2146
+ <div className="st-empty-body">
2147
+ ← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open the{' '}
2148
+ <strong>Design system</strong> view above it.
2149
+ <br />
2150
+ <br />
2151
+ Tabs work like in an editor — close with the × on each tab. <Kbd>⌘R</Kbd> reloads the
2152
+ active iframe.
2153
+ <br />
2154
+ <br />
2155
+ <strong>Element selection:</strong> hold <Kbd>⌘</Kbd> inside the canvas and hover for a
2156
+ preview, click to select. <Kbd>⌘⇧</Kbd>+click adds to a multi-selection. <Kbd>V</Kbd>
2157
+ /<Kbd>H</Kbd>/<Kbd>C</Kbd> swap tool; right-click opens the context menu.
2158
+ <br />
2159
+ <br />
2160
+ Active file, selection, and comments are tracked in <code>_active.json</code> +{' '}
2161
+ <code>_comments/</code> — Claude reads them when you run <code>/design</code>.
2162
+ </div>
2163
+ </div>
1463
2164
  )}
1464
2165
  {tabs.map((t) => {
1465
2166
  if (t.path === SYSTEM_TAB) {
@@ -1749,33 +2450,6 @@ function Gallery({ title, items, onOpen, kind, cfg }) {
1749
2450
 
1750
2451
  // ---------- Comment composer / viewer ----------
1751
2452
 
1752
- function CommentBar({ activePath, comments }) {
1753
- // Phase 6 — the shell-side composer + chip strip + focused-row chrome were
1754
- // removed. The iframe overlay (comments-overlay.tsx) owns composer + pin
1755
- // bubbles + thread popover. BottomBar shrinks to a live open-count summary
1756
- // so the shell still surfaces total review activity at a glance.
1757
- if (!activePath) return null;
1758
- const openComments = (comments || []).filter((c) => c.status !== 'resolved');
1759
- if (openComments.length === 0) return null;
1760
- return (
1761
- <div className="comment-bar">
1762
- <div className="cb-row strip">
1763
- <span className="cb-label">
1764
- {openComments.length} open comment{openComments.length === 1 ? '' : 's'}
1765
- </span>
1766
- </div>
1767
- </div>
1768
- );
1769
- }
1770
-
1771
- function StatusBarSlot({ label, children, className = '' }) {
1772
- return (
1773
- <span className={'sb-slot ' + className} role="group" aria-label={label}>
1774
- {children}
1775
- </span>
1776
- );
1777
- }
1778
-
1779
2453
  function StatusBar({
1780
2454
  activePath,
1781
2455
  selected,
@@ -1797,66 +2471,108 @@ function StatusBar({
1797
2471
  : selected
1798
2472
  ? selected.selector
1799
2473
  : '';
2474
+ const nextTheme = theme === 'dark' ? 'light' : 'dark';
2475
+
2476
+ // P5 (Plan C) — always-on hub-sync slot. `/_sync-status` returns one of three
2477
+ // shapes: solo `{linked:false}` (hide), DDR-060 `{notSyncable,tsxCount,reason}`
2478
+ // (linked but 0 syncable), or the connection-state machine `{state,queuedOps,
2479
+ // flash,…}` (the common linked case the old notSyncable-only guard never
2480
+ // showed — the "hub sync se neukazuje" bug). Map each to a label + dot tone.
2481
+ const syncSlot = (() => {
2482
+ if (!syncStatus || syncStatus.linked === false) return null;
2483
+ if (syncStatus.notSyncable) {
2484
+ return {
2485
+ online: false,
2486
+ label: `0 syncable${syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}`,
2487
+ title: syncStatus.reason || 'Linked to a hub, but no canvases are syncable.',
2488
+ };
2489
+ }
2490
+ const q = syncStatus.queuedOps ?? 0;
2491
+ const synced = syncStatus.state === 'online' || syncStatus.flash === 'synced';
2492
+ if (synced) {
2493
+ return {
2494
+ online: true,
2495
+ label: q > 0 ? `${q} ↑` : 'synced',
2496
+ title: q > 0 ? `${q} edit(s) queued to push` : 'All changes synced to the hub',
2497
+ };
2498
+ }
2499
+ return {
2500
+ online: false,
2501
+ label: `${q} ↑`,
2502
+ title:
2503
+ syncStatus.state === 'connecting'
2504
+ ? 'Connecting to the hub…'
2505
+ : 'Offline — edits queued, will sync when the hub reconnects',
2506
+ };
2507
+ })();
2508
+
1800
2509
  return (
1801
- <div className="statusbar" role="contentinfo">
1802
- <StatusBarSlot label="Active file" className="sb-active">
1803
- <span className="sb-key">active</span>
1804
- <span className="sb-file" title={activePath || ''}>
2510
+ <footer className="st-statusbar" role="contentinfo">
2511
+ <span className="st-sb-slot st-sb-active" role="group" aria-label="Active file">
2512
+ <span className="lead" aria-hidden="true" />
2513
+ <span className="lbl">active</span>
2514
+ <span className="val" title={activePath || ''}>
1805
2515
  {isSystem ? '▦ design system' : activePath || '—'}
1806
2516
  </span>
1807
- </StatusBarSlot>
2517
+ </span>
1808
2518
 
1809
2519
  {selected && selected.selector && !isSystem && (
1810
- <StatusBarSlot label="Selected element" className="sb-selected">
1811
- <span className="sb-dot" aria-hidden="true">
1812
-
1813
- </span>
1814
- <span className="sb-sel-text" title={title}>
2520
+ <span className="st-sb-slot st-sb-sel" role="group" aria-label="Selected element">
2521
+ <span className="lbl">selected</span>
2522
+ <span className="val" title={title}>
1815
2523
  {text}
1816
2524
  </span>
1817
2525
  <button
1818
2526
  type="button"
1819
- className="sb-clear-sel"
2527
+ className="st-sb-sel-clear"
1820
2528
  onClick={onClearSelected}
1821
2529
  title="Clear (Esc inside iframe)"
1822
2530
  aria-label="Clear selection"
1823
2531
  >
1824
2532
  ×
1825
2533
  </button>
1826
- </StatusBarSlot>
2534
+ </span>
1827
2535
  )}
1828
2536
 
1829
- <StatusBarSlot label="Open comments" className="sb-unread">
1830
- <span className="sb-key">comments</span>
1831
- <span className="sb-count">{openCount}</span>
1832
- </StatusBarSlot>
1833
-
1834
- <StatusBarSlot label="Connection" className="sb-live">
1835
- <span className={'sb-live-dot' + (wsConnected ? ' connected' : '')} aria-hidden="true" />
1836
- <span className="sb-key">{wsConnected ? 'live' : 'reconnecting'}</span>
1837
- </StatusBarSlot>
1838
-
1839
- {/* DDR-060 / 9.1-D — linked-to-hub-but-nothing-syncs state lives here in the
1840
- status bar (DS-styled, hover for detail) instead of a floating off-brand
1841
- pill. Only shown when the project is linked + has 0 syncable canvases. */}
1842
- {syncStatus?.notSyncable && (
1843
- <StatusBarSlot label="Hub sync" className="sb-sync">
1844
- <span className="sb-sync-dot" aria-hidden="true" />
2537
+ <span className="st-sb-slot" role="group" aria-label="Open comments">
2538
+ <span className="lbl">comments</span>
2539
+ <span className="val">{openCount} open</span>
2540
+ </span>
2541
+
2542
+ <span className="st-sb-spacer" />
2543
+
2544
+ <span className="st-sb-slot" role="group" aria-label="Connection">
2545
+ <span className={'st-live-dot' + (wsConnected ? ' is-connected' : '')} aria-hidden="true" />
2546
+ <span className="lbl">{wsConnected ? 'live' : 'reconnecting'}</span>
2547
+ </span>
2548
+
2549
+ {/* P5 (Plan C) always-on hub-sync slot (was notSyncable-only per DDR-060
2550
+ / 9.1-D). Now also surfaces the connection-state machine's queued/synced
2551
+ counter for the common linked case. Solo projects render nothing. */}
2552
+ {syncSlot && (
2553
+ <span className="st-sb-slot st-sb-sync" role="group" aria-label="Hub sync">
1845
2554
  <span
1846
- className="sb-key"
1847
- title={syncStatus.reason || 'Linked to a hub, but no canvases are syncable.'}
1848
- >
1849
- 0 syncable{syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}
2555
+ className={'st-sb-sync-dot' + (syncSlot.online ? ' is-online' : '')}
2556
+ aria-hidden="true"
2557
+ />
2558
+ <span className="lbl">hub sync</span>
2559
+ <span className="val" title={syncSlot.title}>
2560
+ {syncSlot.label}
1850
2561
  </span>
1851
- </StatusBarSlot>
2562
+ </span>
1852
2563
  )}
1853
2564
 
1854
- <span className="sb-spacer" />
1855
-
1856
- <StatusBarSlot label="Theme" className="sb-theme">
1857
- <ThemeToggle theme={theme} onToggle={onToggleTheme} />
1858
- </StatusBarSlot>
1859
- </div>
2565
+ <button
2566
+ type="button"
2567
+ className="st-sb-theme"
2568
+ onClick={onToggleTheme}
2569
+ title={`Switch to ${nextTheme} theme`}
2570
+ aria-label={`Switch to ${nextTheme} theme`}
2571
+ >
2572
+ <StIcon name={theme === 'dark' ? 'sun' : 'moon'} size={13} />
2573
+ {nextTheme}
2574
+ </button>
2575
+ </footer>
1860
2576
  );
1861
2577
  }
1862
2578
 
@@ -1897,108 +2613,120 @@ function CommentsPanel({
1897
2613
  }
1898
2614
 
1899
2615
  return (
1900
- <aside className="rsidebar">
1901
- <div className="rsidebar-header">
1902
- <h2>
1903
- <span>Comments</span>
1904
- <span className="total">{counts.all}</span>
1905
- </h2>
1906
- <div className="rsidebar-filters" role="tablist">
2616
+ <aside className="st-rpanel" aria-label="Comments">
2617
+ <div className="st-rp-tabs st-rp-tabs--filters">
2618
+ <div className="st-cm-filters" role="tablist">
1907
2619
  <button
1908
- className={'rsidebar-filter' + (filter === 'all' ? ' active' : '')}
2620
+ type="button"
2621
+ className={'st-cm-filter' + (filter === 'all' ? ' is-active' : '')}
1909
2622
  role="tab"
1910
2623
  aria-selected={filter === 'all'}
1911
2624
  onClick={() => setFilter('all')}
1912
2625
  >
1913
- All <span className="fc">{counts.all}</span>
2626
+ All · {counts.all}
1914
2627
  </button>
1915
2628
  <button
1916
- className={'rsidebar-filter' + (filter === 'open' ? ' active' : '')}
2629
+ type="button"
2630
+ className={'st-cm-filter' + (filter === 'open' ? ' is-active' : '')}
1917
2631
  role="tab"
1918
2632
  aria-selected={filter === 'open'}
1919
2633
  onClick={() => setFilter('open')}
1920
2634
  >
1921
- Open <span className="fc">{counts.open}</span>
2635
+ Open · {counts.open}
1922
2636
  </button>
1923
2637
  <button
1924
- className={'rsidebar-filter' + (filter === 'resolved' ? ' active' : '')}
2638
+ type="button"
2639
+ className={'st-cm-filter' + (filter === 'resolved' ? ' is-active' : '')}
1925
2640
  role="tab"
1926
2641
  aria-selected={filter === 'resolved'}
1927
2642
  onClick={() => setFilter('resolved')}
1928
2643
  >
1929
- Resolved <span className="fc">{counts.resolved}</span>
2644
+ Resolved · {counts.resolved}
1930
2645
  </button>
1931
2646
  </div>
1932
2647
  </div>
1933
- <div className="rsidebar-body">
2648
+ <div className="st-rp-body" style={{ gap: 'var(--space-4)' }}>
1934
2649
  {groups.length === 0 ? (
1935
- <div className="rsidebar-empty">
2650
+ <div className="st-rp-empty">
1936
2651
  <p>No comments {filter !== 'all' ? `with status “${filter}”` : 'yet'}.</p>
1937
- <p style={{ marginTop: 12 }}>
1938
- Open a canvas, hold <kbd>⌘</kbd> and click an element, then press <kbd>C</kbd> — or
1939
- hold <kbd>⌘⇧</kbd> and click directly.
2652
+ <p>
2653
+ Open a canvas, hold <Kbd>⌘</Kbd> and click an element, then press <Kbd>C</Kbd> — or
2654
+ hold <Kbd>⌘⇧</Kbd> and click directly.
1940
2655
  </p>
1941
2656
  </div>
1942
2657
  ) : (
1943
2658
  groups.map((g) => (
1944
- <div key={g.file} className="rs-group">
1945
- <button className="rs-group-h" onClick={() => onJump(g.file, null)} title={g.file}>
1946
- <span className="rs-group-name">{displayName(basename(g.file))}</span>
1947
- <span className="rs-group-count">{g.comments.length}</span>
2659
+ <Fragment key={g.file}>
2660
+ <button
2661
+ type="button"
2662
+ className="st-cm-group-hd"
2663
+ onClick={() => onJump(g.file, null)}
2664
+ title={g.file}
2665
+ >
2666
+ <span>{displayName(basename(g.file))}</span>
2667
+ <span className="st-mono">{g.comments.length}</span>
1948
2668
  </button>
1949
2669
  {g.comments.map((c) => (
1950
2670
  <div
1951
2671
  key={c.id}
1952
2672
  className={
1953
- 'rs-comment' +
1954
- (c.status === 'resolved' ? ' resolved' : '') +
1955
- (c.id === focusedId ? ' active-pin' : '')
2673
+ 'st-comment' +
2674
+ (c.status === 'resolved' ? ' is-resolved' : '') +
2675
+ (c.id === focusedId ? ' is-active' : '')
1956
2676
  }
1957
2677
  onClick={() => onJump(g.file, c.id)}
1958
2678
  >
1959
- <div className="rs-comment-head">
1960
- <span className="rs-num">{c.n || '·'}</span>
1961
- <span className="rs-time">{timeAgo(c.created)}</span>
2679
+ <div className="st-comment-hd">
2680
+ <span className="st-pin st-pin--inline">{c.n || '·'}</span>
2681
+ <span className="st-comment-time">{timeAgo(c.created)}</span>
1962
2682
  </div>
1963
- <div className="rs-comment-text">{c.text}</div>
1964
- <div className="rs-comment-foot">
1965
- <code title={(c.dom_path || []).join(' > ')}>{c.selector || '—'}</code>
1966
- <div className="rs-comment-actions">
2683
+ <div className="st-comment-txt">{c.text}</div>
2684
+ <div className="st-comment-foot">
2685
+ <span className="st-comment-sel" title={(c.dom_path || []).join(' > ')}>
2686
+ {c.selector || '—'}
2687
+ </span>
2688
+ <span className="st-mini-act">
1967
2689
  {c.status === 'resolved' ? (
1968
2690
  <button
1969
- className="rs-act"
2691
+ type="button"
2692
+ className="st-iconbtn"
2693
+ aria-label="Reopen"
1970
2694
  onClick={(e) => {
1971
2695
  e.stopPropagation();
1972
2696
  onReopen(c.id);
1973
2697
  }}
1974
2698
  >
1975
-
2699
+ <StIcon name="reopen" size={14} />
1976
2700
  </button>
1977
2701
  ) : (
1978
2702
  <button
1979
- className="rs-act"
2703
+ type="button"
2704
+ className="st-iconbtn"
2705
+ aria-label="Resolve"
1980
2706
  onClick={(e) => {
1981
2707
  e.stopPropagation();
1982
2708
  onResolve(c.id);
1983
2709
  }}
1984
2710
  >
1985
-
2711
+ <StIcon name="resolve" size={14} />
1986
2712
  </button>
1987
2713
  )}
1988
2714
  <button
1989
- className="rs-act danger"
2715
+ type="button"
2716
+ className="st-iconbtn"
2717
+ aria-label="Delete"
1990
2718
  onClick={(e) => {
1991
2719
  e.stopPropagation();
1992
2720
  onDelete(c.id);
1993
2721
  }}
1994
2722
  >
1995
- ×
2723
+ <StIcon name="x" size={14} />
1996
2724
  </button>
1997
- </div>
2725
+ </span>
1998
2726
  </div>
1999
2727
  </div>
2000
2728
  ))}
2001
- </div>
2729
+ </Fragment>
2002
2730
  ))
2003
2731
  )}
2004
2732
  </div>
@@ -2013,6 +2741,10 @@ function CommentsPanel({
2013
2741
  // for 3s right after a reconnect. Driven entirely by the 'sync:status' payload
2014
2742
  // the dev-server's linked-mode sync runtime broadcasts.
2015
2743
  function SyncBanner({ status }) {
2744
+ // Plan C follow-up — the banner overlapped the menubar and wasn't dismissable.
2745
+ // Dismissal is keyed on the connection state, so a transition (reconnect flash,
2746
+ // escalation to offline-long) re-surfaces it; a stable state stays hidden.
2747
+ const [dismissedKey, setDismissedKey] = useState(null);
2016
2748
  if (!status || status.linked === false) return null;
2017
2749
  // DDR-060 / 9.1-D — the "linked but 0 syncable" state is surfaced in the
2018
2750
  // status bar (sb-sync slot), NOT as a floating banner. This component owns
@@ -2022,61 +2754,179 @@ function SyncBanner({ status }) {
2022
2754
  const showFlash = flash === 'synced';
2023
2755
  const offline = state === 'offline' || state === 'offline-long';
2024
2756
  if (!offline && !showFlash) return null;
2757
+ const dismissKey = `${state}:${showFlash ? 'flash' : 'offline'}`;
2758
+ if (dismissedKey === dismissKey) return null;
2025
2759
 
2026
- let bg;
2027
- let fg;
2028
- let border;
2760
+ let variant;
2029
2761
  let text;
2030
2762
  if (showFlash) {
2031
- bg = '#dcfce7';
2032
- fg = '#166534';
2033
- border = '#86efac';
2763
+ variant = 'success';
2034
2764
  text = 'Synced with hub';
2035
2765
  } else if (state === 'offline-long') {
2036
- bg = '#fee2e2';
2037
- fg = '#991b1b';
2038
- border = '#fca5a5';
2766
+ variant = 'error';
2039
2767
  text = `Long offline — ${queuedOps} edit(s) queued. Consider \`git commit && git push\` as backup.`;
2040
2768
  } else {
2041
- bg = '#fef9c3';
2042
- fg = '#854d0e';
2043
- border = '#fde047';
2769
+ variant = 'warn';
2044
2770
  text = `Working offline · ${queuedOps} edit(s) queued · will sync when the hub reconnects.`;
2045
2771
  }
2046
2772
  const conflictNote =
2047
2773
  conflicts && conflicts.length > 0 ? ` (${conflicts.length} conflict notice(s))` : '';
2048
2774
 
2049
2775
  return (
2050
- <div
2051
- role="status"
2052
- aria-live="polite"
2053
- style={{
2054
- position: 'fixed',
2055
- top: 12,
2056
- left: '50%',
2057
- transform: 'translateX(-50%)',
2058
- zIndex: 10003,
2059
- display: 'flex',
2060
- alignItems: 'center',
2061
- gap: 10,
2062
- padding: '8px 14px',
2063
- background: bg,
2064
- color: fg,
2065
- border: `1px solid ${border}`,
2066
- borderRadius: 999,
2067
- boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
2068
- font: '500 13px/1.2 system-ui, -apple-system, sans-serif',
2069
- maxWidth: '80vw',
2070
- }}
2071
- >
2776
+ <div role="status" aria-live="polite" className={`st-banner st-banner--${variant}`}>
2777
+ <span className="st-banner-dot" aria-hidden="true" />
2072
2778
  <span>
2073
2779
  {text}
2074
2780
  {conflictNote}
2075
2781
  </span>
2782
+ <button
2783
+ type="button"
2784
+ className="st-banner-close"
2785
+ aria-label="Dismiss"
2786
+ title="Dismiss"
2787
+ onClick={() => setDismissedKey(dismissKey)}
2788
+ >
2789
+ ×
2790
+ </button>
2076
2791
  </div>
2077
2792
  );
2078
2793
  }
2079
2794
 
2795
+ // ---------- Inspector panel (display-only) ----------
2796
+ //
2797
+ // T6 (Plan C) — right-dock Inspect / Layers / CSS tabs per `.design/ui/Studio.tsx`
2798
+ // InspectorPanel. DISPLAY-ONLY: reads the live `selected` payload from the
2799
+ // inspector bridge (`bounds`, `tag`, `classes`, `dom_path`, `html`). The
2800
+ // mockup's live-CSS-knob WRITEBACK is Phase 12 (needs a canvas-origin write
2801
+ // bridge, DDR-054) — the CSS tab shows markup read-only + keeps that callout, so
2802
+ // it never implies functionality it lacks (the exact reason DDR-096 deferred it).
2803
+ function InspectorPanel({ selected, onClose }) {
2804
+ const [tab, setTab] = useState('inspect');
2805
+ // `selected` may be a single element, an array (multi-select), or null.
2806
+ const el = Array.isArray(selected) ? selected[0] : selected;
2807
+ const tabBtn = (id, label, icon) => (
2808
+ <button
2809
+ type="button"
2810
+ className={'st-rp-tab' + (tab === id ? ' is-active' : '')}
2811
+ onClick={() => setTab(id)}
2812
+ >
2813
+ <StIcon name={icon} size={14} />
2814
+ {label}
2815
+ </button>
2816
+ );
2817
+ const b = el?.bounds || null;
2818
+ return (
2819
+ <aside className="st-rpanel" aria-label="Inspector">
2820
+ <div className="st-rp-tabs">
2821
+ {tabBtn('inspect', 'Inspect', 'sliders')}
2822
+ {tabBtn('layers', 'Layers', 'layers')}
2823
+ {tabBtn('css', 'CSS', 'code')}
2824
+ <button
2825
+ type="button"
2826
+ className="st-iconbtn"
2827
+ aria-label="Close inspector"
2828
+ style={{ marginLeft: 'auto' }}
2829
+ onClick={onClose}
2830
+ >
2831
+ <StIcon name="x" size={14} />
2832
+ </button>
2833
+ </div>
2834
+ <div className="st-rp-body">
2835
+ {!el ? (
2836
+ <div className="st-rp-empty">
2837
+ Hold <Kbd>⌘</Kbd> inside the canvas and click an element to inspect it.
2838
+ </div>
2839
+ ) : tab === 'inspect' ? (
2840
+ <>
2841
+ <div className="st-rp-hd">{el.selector || el.tag || 'element'}</div>
2842
+ <div className="st-insp-row">
2843
+ <span className="st-insp-label">Pos</span>
2844
+ <div className="st-insp-fields">
2845
+ <span className="st-field-lead">
2846
+ <span className="k">X</span>
2847
+ <input className="st-field" value={b ? Math.round(b.x) : '—'} readOnly />
2848
+ </span>
2849
+ <span className="st-field-lead">
2850
+ <span className="k">Y</span>
2851
+ <input className="st-field" value={b ? Math.round(b.y) : '—'} readOnly />
2852
+ </span>
2853
+ </div>
2854
+ </div>
2855
+ <div className="st-insp-row">
2856
+ <span className="st-insp-label">Size</span>
2857
+ <div className="st-insp-fields">
2858
+ <span className="st-field-lead">
2859
+ <span className="k">W</span>
2860
+ <input className="st-field" value={b ? Math.round(b.w) : '—'} readOnly />
2861
+ </span>
2862
+ <span className="st-field-lead">
2863
+ <span className="k">H</span>
2864
+ <input className="st-field" value={b ? Math.round(b.h) : '—'} readOnly />
2865
+ </span>
2866
+ </div>
2867
+ </div>
2868
+ <div className="st-insp-row">
2869
+ <span className="st-insp-label">Tag</span>
2870
+ <div className="st-insp-fields">
2871
+ <span className="st-mono" style={{ fontSize: 11, color: 'var(--fg-0)' }}>
2872
+ {el.tag || '—'}
2873
+ </span>
2874
+ </div>
2875
+ </div>
2876
+ {el.classes ? (
2877
+ <div className="st-insp-row">
2878
+ <span className="st-insp-label">Class</span>
2879
+ <div className="st-insp-fields">
2880
+ <span className="st-mono" style={{ fontSize: 11, color: 'var(--fg-1)' }}>
2881
+ {el.classes}
2882
+ </span>
2883
+ </div>
2884
+ </div>
2885
+ ) : null}
2886
+ <div className="callout callout--info" style={{ fontSize: 12 }}>
2887
+ Computed fill / radius / type readout lands with the live CSS bridge (Phase 12).
2888
+ </div>
2889
+ </>
2890
+ ) : tab === 'layers' ? (
2891
+ <>
2892
+ <div className="st-rp-hd">Layers · ancestry</div>
2893
+ {Array.isArray(el.dom_path) && el.dom_path.length ? (
2894
+ el.dom_path.map((node, i) => (
2895
+ <div
2896
+ key={i}
2897
+ className={'st-layer' + (i === el.dom_path.length - 1 ? ' is-sel' : '')}
2898
+ style={{ paddingLeft: 8 + i * 12 }}
2899
+ >
2900
+ <StIcon name="square" size={13} />
2901
+ {node}
2902
+ </div>
2903
+ ))
2904
+ ) : (
2905
+ <div className="st-rp-empty">No ancestry path for this selection.</div>
2906
+ )}
2907
+ </>
2908
+ ) : (
2909
+ <>
2910
+ <div className="st-rp-hd">Markup · {el.selector || el.tag}</div>
2911
+ <div className="st-css">
2912
+ <div>
2913
+ <span className="comment">/* read-only — outerHTML snapshot */</span>
2914
+ </div>
2915
+ <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
2916
+ {(el.html || '').slice(0, 600) || '(no markup captured)'}
2917
+ </div>
2918
+ </div>
2919
+ <div className="callout callout--info" style={{ fontSize: 12 }}>
2920
+ Phase 12 — knob edits will write back to the artboard live and stage a diff for
2921
+ handoff. Today the inspector is read-only.
2922
+ </div>
2923
+ </>
2924
+ )}
2925
+ </div>
2926
+ </aside>
2927
+ );
2928
+ }
2929
+
2080
2930
  // ---------- App ----------
2081
2931
 
2082
2932
  function App() {
@@ -2105,7 +2955,11 @@ function App() {
2105
2955
  .then((data) => {
2106
2956
  if (cancelled) return;
2107
2957
  const designRel = (data.designRoot || '.design').replace(/^\/+|\/+$/g, '');
2108
- setCfg({
2958
+ // Functional merge — the `/_config` and `/_index-data` fetches race, and
2959
+ // the latter contributes `canvasDesignSystems` (DDR-093). A full-replace
2960
+ // here would clobber that map if it resolved second.
2961
+ setCfg((prev) => ({
2962
+ ...prev,
2109
2963
  designRel,
2110
2964
  tokensCssRel: data.tokensCssRel,
2111
2965
  // Pass through designSystems so canvasUrl can resolve the right
@@ -2118,7 +2972,7 @@ function App() {
2118
2972
  // from the main origin's /_api). Absent on older servers → relative
2119
2973
  // URL fallback keeps same-origin behavior.
2120
2974
  canvasOrigin: data.canvasOrigin,
2121
- });
2975
+ }));
2122
2976
  })
2123
2977
  .catch(() => {});
2124
2978
  return () => {
@@ -2154,7 +3008,35 @@ function App() {
2154
3008
  const [showHidden, setShowHidden] = useState(() => readBoolStore(SHOW_HIDDEN_STORE, false));
2155
3009
  const [sectionsExpanded, setSectionsExpanded] = useState(() => readJsonStore(SECTIONS_STORE, {}));
2156
3010
  const [helpOpen, setHelpOpen] = useState(false);
3011
+ const [paletteOpen, setPaletteOpen] = useState(false);
3012
+ // T5/T6 (Plan C) — shell-level export/handoff dialog + inspector panel state.
3013
+ // The palette (T4) drives them; the dialog (T5) + panel (T6) consume them.
3014
+ const [exportDialog, setExportDialog] = useState(null); // null | { mode: 'export'|'handoff', scope? }
3015
+ const [inspectorOpen, setInspectorOpen] = useState(false);
3016
+ const whatsNew = useWhatsNew(MDCC_VERSION);
3017
+ const [tourSteps, setTourSteps] = useState(null);
3018
+ const [usageNudge, setUsageNudge] = useState(() => !readBoolStore(USAGE_TOUR_STORE, false));
3019
+ const startTour = useCallback((steps) => {
3020
+ setTourSteps(Array.isArray(steps) && steps.length ? steps : null);
3021
+ }, []);
3022
+ const markUsageSeen = useCallback(() => {
3023
+ setUsageNudge(false);
3024
+ try {
3025
+ localStorage.setItem(USAGE_TOUR_STORE, '1');
3026
+ } catch {}
3027
+ }, []);
2157
3028
  const [annotationsVisible, setAnnotationsVisible] = useState(true);
3029
+ // P2/P3 (Plan C) — top-bar live state. (Zoom lives in the canvas toolbar pill,
3030
+ // so the top bar no longer mirrors it.)
3031
+ // activeArtboards — real artboard count of the open canvas, read from its
3032
+ // `<canvas>.meta.json` sidecar (shell-side, no iframe dep).
3033
+ // gitUser — local user (name/initials) for the menubar presence avatar.
3034
+ // agentActive — transient flag set on `ai-activity`, cleared after idle, so
3035
+ // the menubar shows a live agent avatar while Claude edits.
3036
+ const [activeArtboards, setActiveArtboards] = useState(0);
3037
+ const [gitUser, setGitUser] = useState(null);
3038
+ const [agentActive, setAgentActive] = useState(false);
3039
+ const agentIdleRef = useRef(null);
2158
3040
  const wsRef = useRef(null);
2159
3041
  const iframesRef = useRef(new Map());
2160
3042
 
@@ -2186,6 +3068,45 @@ function App() {
2186
3068
  });
2187
3069
  }, [activePath]);
2188
3070
 
3071
+ // P3 (Plan C) — local git user for the menubar presence avatar. One-shot.
3072
+ useEffect(() => {
3073
+ let cancelled = false;
3074
+ fetch('/_api/git-user')
3075
+ .then((r) => r.json())
3076
+ .then((d) => {
3077
+ if (cancelled) return;
3078
+ const n = d && typeof d.name === 'string' ? d.name.trim() : '';
3079
+ if (n) setGitUser(n);
3080
+ })
3081
+ .catch(() => {});
3082
+ return () => {
3083
+ cancelled = true;
3084
+ };
3085
+ }, []);
3086
+
3087
+ // P2 (Plan C) — when the active canvas changes, read its real artboard count
3088
+ // from the `<canvas>.meta.json` sidecar (shell-side, no iframe dep).
3089
+ useEffect(() => {
3090
+ if (!activePath || activePath === SYSTEM_TAB) {
3091
+ setActiveArtboards(0);
3092
+ return;
3093
+ }
3094
+ let cancelled = false;
3095
+ fetch('/_api/canvas-meta?file=' + encodeURIComponent(activePath))
3096
+ .then((r) => r.json())
3097
+ .then((meta) => {
3098
+ if (cancelled) return;
3099
+ const n = Array.isArray(meta?.artboards) ? meta.artboards.length : 0;
3100
+ setActiveArtboards(n);
3101
+ })
3102
+ .catch(() => {
3103
+ if (!cancelled) setActiveArtboards(0);
3104
+ });
3105
+ return () => {
3106
+ cancelled = true;
3107
+ };
3108
+ }, [activePath]);
3109
+
2189
3110
  // Sync theme to <html data-theme> + localStorage on every change.
2190
3111
  useEffect(() => {
2191
3112
  try {
@@ -2247,6 +3168,13 @@ function App() {
2247
3168
  tree: buildTree(g.paths, g.stripPrefix),
2248
3169
  }));
2249
3170
  setGroups(built);
3171
+ // DDR-093 — fold the server-resolved per-canvas DS map into cfg so
3172
+ // canvasUrl() injects each UI canvas's OWN design-system tokens instead of
3173
+ // always designSystems[0]. Functional merge to coexist with the /_config
3174
+ // fetch (either may land first). `?? {}` keeps older servers (no map) on
3175
+ // the ds0 fallback. Re-runs on every tree reload, so adding/retargeting a
3176
+ // canvas refreshes the map.
3177
+ setCfg((prev) => ({ ...prev, canvasDesignSystems: data.canvasDesignSystems ?? {} }));
2250
3178
  } catch (e) {
2251
3179
  console.error('failed to load tree', e);
2252
3180
  }
@@ -2323,6 +3251,12 @@ function App() {
2323
3251
  } else if (m.type === 'comments' && typeof m.file === 'string') {
2324
3252
  setCommentsByFile((prev) => ({ ...prev, [m.file]: m.comments || [] }));
2325
3253
  } else if (m.type === 'ai-activity' && typeof m.file === 'string') {
3254
+ // P3 (Plan C) — surface live agent activity as a menubar presence
3255
+ // avatar. Set the flag and (re)arm an idle timer so the avatar
3256
+ // fades once Claude stops editing.
3257
+ setAgentActive(true);
3258
+ if (agentIdleRef.current) clearTimeout(agentIdleRef.current);
3259
+ agentIdleRef.current = setTimeout(() => setAgentActive(false), 8000);
2326
3260
  // Phase 8 Task 4 — relay to every open iframe; each canvas's
2327
3261
  // AiBanner filters by its own file path. Lightweight broadcast
2328
3262
  // (one envelope per change, not per iframe count).
@@ -2430,6 +3364,63 @@ function App() {
2430
3364
 
2431
3365
  const reloadTree = useCallback(() => loadTree(), [loadTree]);
2432
3366
 
3367
+ // Phase 22 — create a blank brief board from the tree header. POSTs to the
3368
+ // main-origin-only /_api/canvas (the untrusted canvas iframe can't reach it),
3369
+ // then refreshes the tree and opens the new board so it's immediately the
3370
+ // active canvas to annotate. Returns {ok} | {ok:false,error} so the Sidebar
3371
+ // can surface a validation/duplicate message inline.
3372
+ const createBoard = useCallback(
3373
+ async (name) => {
3374
+ try {
3375
+ const r = await fetch('/_api/canvas', {
3376
+ method: 'POST',
3377
+ headers: { 'Content-Type': 'application/json' },
3378
+ body: JSON.stringify({ name, kind: 'brief-board' }),
3379
+ });
3380
+ const j = await r.json().catch(() => ({}));
3381
+ if (!r.ok || !j.ok) return { ok: false, error: j.error || `create failed (${r.status})` };
3382
+ await loadTree();
3383
+ openTab(j.file);
3384
+ return { ok: true, file: j.file };
3385
+ } catch (e) {
3386
+ return { ok: false, error: e instanceof Error ? e.message : 'network error' };
3387
+ }
3388
+ },
3389
+ [loadTree, openTab]
3390
+ );
3391
+
3392
+ // Phase 22 — soft-delete a canvas from the file tree. Confirms (destructive),
3393
+ // DELETEs to the main-origin-only endpoint, refreshes the tree, and resets the
3394
+ // active tab if the deleted canvas was open. The server moves the whole sidecar
3395
+ // set to .design/_trash/ — recoverable locally.
3396
+ const deleteBoard = useCallback(
3397
+ async (filePath, label) => {
3398
+ const ok = window.confirm(
3399
+ `Move “${label}” to trash?\n\nIts annotations, history and comments move with it. ` +
3400
+ `You can restore it from .design/_trash/.`
3401
+ );
3402
+ if (!ok) return;
3403
+ try {
3404
+ const r = await fetch(`/_api/canvas?file=${encodeURIComponent(filePath)}`, {
3405
+ method: 'DELETE',
3406
+ });
3407
+ const j = await r.json().catch(() => ({}));
3408
+ if (!r.ok || !j.ok) {
3409
+ window.alert(`Could not delete: ${j.error || `error ${r.status}`}`);
3410
+ return;
3411
+ }
3412
+ await loadTree();
3413
+ if (activePath === filePath) {
3414
+ setTabs([]);
3415
+ setActivePath(null);
3416
+ }
3417
+ } catch (e) {
3418
+ window.alert(`Delete failed: ${e instanceof Error ? e.message : 'network error'}`);
3419
+ }
3420
+ },
3421
+ [loadTree, activePath]
3422
+ );
3423
+
2433
3424
  const clearSelected = useCallback(() => {
2434
3425
  wsSend({ type: 'clear-select' });
2435
3426
  setSelected(null);
@@ -2536,6 +3527,7 @@ function App() {
2536
3527
  payload: {
2537
3528
  file: p.file,
2538
3529
  selector: p.selector,
3530
+ index: p.index,
2539
3531
  dom_path: p.dom_path,
2540
3532
  tag: p.tag,
2541
3533
  classes: p.classes,
@@ -2553,6 +3545,24 @@ function App() {
2553
3545
  setFocusedCommentId((prev) => (prev === m.id ? null : prev));
2554
3546
  } else if (m.dgn === 'comment-click' && m.id) {
2555
3547
  setFocusedCommentId(m.id);
3548
+ } else if (m.dgn === 'artboards' && typeof m.count === 'number') {
3549
+ // P2 (Plan C) — optional iframe-reported artboard count; overrides the
3550
+ // meta.json-derived seed when the canvas knows better. Clamp.
3551
+ const n = Math.round(m.count);
3552
+ if (Number.isFinite(n) && n >= 0 && n <= 999) setActiveArtboards(n);
3553
+ } else if (m.dgn === 'toggle-palette') {
3554
+ // ⌘K pressed while focus was inside the canvas iframe — the injected
3555
+ // inspector forwards the chord here since the iframe's keydown never
3556
+ // reaches the shell's window listener. Mirror that handler's toggle.
3557
+ setPaletteOpen((v) => !v);
3558
+ } else if (m.dgn === 'open-export') {
3559
+ // Plan C — the in-canvas toolbar / context menu route here so they open
3560
+ // the SAME shell Export dialog as the menubar (one look, all settings).
3561
+ // Carry the context-menu's scope hint (e.g. "Export selection").
3562
+ setExportDialog({
3563
+ mode: 'export',
3564
+ scope: m.detail && typeof m.detail.scope === 'string' ? m.detail.scope : undefined,
3565
+ });
2556
3566
  } else if (m.dgn === 'loaded' && m.file) {
2557
3567
  // iframe finished loading — push current comments + carry over focused pin if any
2558
3568
  const list = commentsByFile[m.file] || [];
@@ -2709,6 +3719,12 @@ function App() {
2709
3719
  // ⌘F) still fire regardless of focus, mirroring browser convention.
2710
3720
  const inCanvasIframe = document.activeElement?.tagName === 'IFRAME';
2711
3721
 
3722
+ // Cmd+K / Ctrl+K — toggle the command palette (works even in inputs).
3723
+ if (meta && (e.key === 'k' || e.key === 'K')) {
3724
+ e.preventDefault();
3725
+ setPaletteOpen((v) => !v);
3726
+ return;
3727
+ }
2712
3728
  // Cmd+R — reload active iframe (override browser reload)
2713
3729
  if (meta && (e.key === 'r' || e.key === 'R')) {
2714
3730
  e.preventDefault();
@@ -2745,7 +3761,7 @@ function App() {
2745
3761
  // / — focus search (or ⌘F per CV-08 placeholder hint)
2746
3762
  if (e.key === '/') {
2747
3763
  e.preventDefault();
2748
- const inp = document.querySelector('.tree-panel-search input');
3764
+ const inp = document.querySelector('.st-search input');
2749
3765
  if (inp) inp.focus();
2750
3766
  return;
2751
3767
  }
@@ -2753,7 +3769,7 @@ function App() {
2753
3769
  e.preventDefault();
2754
3770
  if (!sidebarOpen) setSidebarOpen(true);
2755
3771
  setTimeout(() => {
2756
- const inp = document.querySelector('.tree-panel-search input');
3772
+ const inp = document.querySelector('.st-search input');
2757
3773
  if (inp) inp.focus();
2758
3774
  }, 0);
2759
3775
  return;
@@ -2790,6 +3806,12 @@ function App() {
2790
3806
  }
2791
3807
  return;
2792
3808
  }
3809
+ // I — toggle Inspector panel (T6, Plan C)
3810
+ if ((e.key === 'i' || e.key === 'I') && !meta && !e.shiftKey) {
3811
+ e.preventDefault();
3812
+ setInspectorOpen((v) => !v);
3813
+ return;
3814
+ }
2793
3815
  // ? or F1 — open Help modal
2794
3816
  if (e.key === '?' || e.key === 'F1') {
2795
3817
  e.preventDefault();
@@ -2822,8 +3844,6 @@ function App() {
2822
3844
  if (el) iframesRef.current.set(path, el);
2823
3845
  }, []);
2824
3846
 
2825
- const activeFileComments =
2826
- activePath && activePath !== SYSTEM_TAB ? commentsByFile[activePath] || [] : [];
2827
3847
  const totalOpen = totalCounts(commentsByFile).open;
2828
3848
 
2829
3849
  // Suppress the native browser context menu across the shell — the canvas
@@ -2840,88 +3860,142 @@ function App() {
2840
3860
  e.preventDefault();
2841
3861
  }, []);
2842
3862
 
3863
+ // ⌘K palette actions — shell-doable only (in-canvas export lives in the iframe).
3864
+ // T4 (Plan C) — grouped command set per `.design/ui/Studio.tsx` AB-D.
3865
+ // `group` drives the section headers; the list stays a flat array so keyboard
3866
+ // nav indexes straight across groups.
3867
+ const paletteActions = useMemo(
3868
+ () => [
3869
+ // ── Canvas ──────────────────────────────────────────────────────────
3870
+ {
3871
+ id: 'new',
3872
+ group: 'Canvas',
3873
+ label: 'New canvas…',
3874
+ icon: 'plus',
3875
+ kbd: '⌘N',
3876
+ run: () => {
3877
+ setSidebarOpen(true);
3878
+ setTimeout(
3879
+ () => document.querySelector('[aria-label="New blank brief board"]')?.click(),
3880
+ 60
3881
+ );
3882
+ },
3883
+ },
3884
+ {
3885
+ id: 'export',
3886
+ group: 'Canvas',
3887
+ label: 'Export…',
3888
+ icon: 'download',
3889
+ kbd: '⇧⌘E',
3890
+ run: () => setExportDialog({ mode: 'export' }),
3891
+ },
3892
+ {
3893
+ id: 'handoff',
3894
+ group: 'Canvas',
3895
+ label: 'Handoff to production',
3896
+ icon: 'share',
3897
+ kbd: '⇧⌘H',
3898
+ run: () => setExportDialog({ mode: 'handoff' }),
3899
+ },
3900
+ // ── View ────────────────────────────────────────────────────────────
3901
+ {
3902
+ id: 'system',
3903
+ group: 'View',
3904
+ label: 'Open design system view',
3905
+ icon: 'sliders',
3906
+ kbd: 'S',
3907
+ run: () => openSystem(),
3908
+ },
3909
+ {
3910
+ id: 'comments',
3911
+ group: 'View',
3912
+ label: 'Toggle comments panel',
3913
+ icon: 'resolve',
3914
+ kbd: '⌘⇧M',
3915
+ run: () => setCommentsPanelOpen((v) => !v),
3916
+ },
3917
+ {
3918
+ id: 'inspector',
3919
+ group: 'View',
3920
+ label: 'Open inspector',
3921
+ icon: 'sliders',
3922
+ kbd: 'I',
3923
+ run: () => setInspectorOpen(true),
3924
+ },
3925
+ {
3926
+ id: 'reload',
3927
+ group: 'View',
3928
+ label: 'Reload active canvas',
3929
+ icon: 'reload',
3930
+ kbd: '⌘R',
3931
+ run: () => reloadActive(),
3932
+ },
3933
+ // ── Tools ───────────────────────────────────────────────────────────
3934
+ {
3935
+ id: 'draw',
3936
+ group: 'Tools',
3937
+ label: 'Draw a mark with the SVG agent',
3938
+ icon: 'pen',
3939
+ run: () => {
3940
+ // The shell can't invoke Claude — surface the command for the user to
3941
+ // paste into Claude Code (clipboard is the honest, useful affordance).
3942
+ try {
3943
+ navigator.clipboard?.writeText('/design:draw ');
3944
+ } catch {}
3945
+ },
3946
+ },
3947
+ {
3948
+ id: 'theme',
3949
+ group: 'Tools',
3950
+ label: 'Toggle light / dark theme',
3951
+ icon: 'sun',
3952
+ run: () => toggleTheme(),
3953
+ },
3954
+ // ── Help ────────────────────────────────────────────────────────────
3955
+ {
3956
+ id: 'whatsnew',
3957
+ group: 'Help',
3958
+ label: "What's new in maude",
3959
+ icon: 'sparkle',
3960
+ run: () => whatsNew.openPanel(),
3961
+ },
3962
+ {
3963
+ id: 'help',
3964
+ group: 'Help',
3965
+ label: 'Help · shortcuts & commands',
3966
+ icon: 'help',
3967
+ kbd: '?',
3968
+ run: () => setHelpOpen(true),
3969
+ },
3970
+ ],
3971
+ [openSystem, toggleTheme, reloadActive, whatsNew]
3972
+ );
3973
+
2843
3974
  return (
2844
- <div
2845
- className={
2846
- 'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')
2847
- }
2848
- onContextMenu={onShellContextMenu}
2849
- >
3975
+ <div className="maude" data-theme={theme} onContextMenu={onShellContextMenu}>
2850
3976
  <SyncBanner status={syncStatus} />
3977
+ {!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
2851
3978
  {gitLifecycle && (
2852
- <div
2853
- role="status"
2854
- aria-live="polite"
2855
- style={{
2856
- position: 'fixed',
2857
- top: 12,
2858
- left: '50%',
2859
- transform: 'translateX(-50%)',
2860
- zIndex: 10002,
2861
- display: 'flex',
2862
- alignItems: 'center',
2863
- gap: 12,
2864
- padding: '8px 14px',
2865
- background: '#dbeafe',
2866
- color: '#1e40af',
2867
- border: '1px solid #93c5fd',
2868
- borderRadius: 999,
2869
- boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
2870
- font: '500 13px/1.2 system-ui, -apple-system, sans-serif',
2871
- }}
2872
- >
3979
+ <div role="status" aria-live="polite" className="st-banner st-banner--info">
3980
+ <span className="st-banner-dot" aria-hidden="true" />
2873
3981
  <span>Repo state changed — reload to sync?</span>
2874
3982
  <button
2875
3983
  type="button"
3984
+ className="btn btn--primary btn--sm"
2876
3985
  onClick={() => {
2877
3986
  try {
2878
3987
  window.location.reload();
2879
3988
  } catch {}
2880
3989
  }}
2881
- style={{
2882
- padding: '3px 10px',
2883
- background: '#2563eb',
2884
- color: '#fff',
2885
- border: 'none',
2886
- borderRadius: 4,
2887
- font: '500 11px/1.2 system-ui',
2888
- cursor: 'pointer',
2889
- }}
2890
3990
  >
2891
3991
  Reload
2892
3992
  </button>
2893
- <button
2894
- type="button"
2895
- onClick={() => setGitLifecycle(null)}
2896
- style={{
2897
- padding: '3px 10px',
2898
- background: 'transparent',
2899
- color: '#1e40af',
2900
- border: '1px solid #93c5fd',
2901
- borderRadius: 4,
2902
- font: '500 11px/1.2 system-ui',
2903
- cursor: 'pointer',
2904
- }}
2905
- >
3993
+ <button type="button" className="btn btn--ghost btn--sm" onClick={() => setGitLifecycle(null)}>
2906
3994
  Dismiss
2907
3995
  </button>
2908
3996
  </div>
2909
3997
  )}
2910
- <Sidebar
2911
- groups={groups}
2912
- activePath={activePath}
2913
- activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
2914
- onOpen={openTab}
2915
- onOpenSystem={openSystem}
2916
- wsConnected={wsConnected}
2917
- search={search}
2918
- setSearch={setSearch}
2919
- commentsByFile={commentsByFile}
2920
- showHidden={showHidden}
2921
- sectionsExpanded={sectionsExpanded}
2922
- onToggleSection={toggleSection}
2923
- />
2924
- <div className="main">
3998
+ <div className="st-shell">
2925
3999
  <Menubar
2926
4000
  activePath={activePath}
2927
4001
  project={project}
@@ -2939,20 +4013,90 @@ function App() {
2939
4013
  annotationsVisible={annotationsVisible}
2940
4014
  onToggleAnnotations={toggleAnnotations}
2941
4015
  postToActiveCanvas={postToActiveCanvas}
4016
+ onOpenWhatsNew={whatsNew.openPanel}
4017
+ whatsNewCount={whatsNew.unseen.length}
4018
+ artboardCount={activeArtboards}
4019
+ inspectorOpen={inspectorOpen}
4020
+ onToggleInspector={() => setInspectorOpen((v) => !v)}
4021
+ onNewCanvas={() => {
4022
+ setSidebarOpen(true);
4023
+ setTimeout(
4024
+ () => document.querySelector('[aria-label="New blank brief board"]')?.click(),
4025
+ 60
4026
+ );
4027
+ }}
4028
+ onOpenExport={(mode) => setExportDialog({ mode })}
4029
+ onReload={reloadActive}
4030
+ presence={
4031
+ <>
4032
+ <StAvatar
4033
+ initials={initialsOf(gitUser || 'you')}
4034
+ hue="var(--accent)"
4035
+ title={gitUser ? `${gitUser} (you)` : 'You'}
4036
+ />
4037
+ {agentActive && (
4038
+ <StAvatar initials="C" hue="var(--presence-agent)" title="Claude · editing" />
4039
+ )}
4040
+ </>
4041
+ }
2942
4042
  />
2943
- <Viewport
2944
- tabs={tabs}
2945
- activePath={activePath}
2946
- registerIframe={registerIframe}
2947
- systemData={systemData}
2948
- onOpenFromSystem={openTab}
2949
- onSelectDs={loadSystemData}
2950
- project={project}
2951
- cfg={cfg}
2952
- />
2953
- {activePath && activePath !== SYSTEM_TAB && (
2954
- <CommentBar activePath={activePath} comments={activeFileComments} />
2955
- )}
4043
+ <div className="st-body">
4044
+ <CollapsedRail
4045
+ shown={!sidebarOpen}
4046
+ onExpand={() => setSidebarOpen(true)}
4047
+ onSearch={() => {
4048
+ setSidebarOpen(true);
4049
+ setTimeout(() => document.querySelector('.st-search input')?.focus(), 60);
4050
+ }}
4051
+ />
4052
+ <Sidebar
4053
+ groups={groups}
4054
+ activePath={activePath}
4055
+ activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
4056
+ onOpen={openTab}
4057
+ onOpenSystem={openSystem}
4058
+ wsConnected={wsConnected}
4059
+ search={search}
4060
+ setSearch={setSearch}
4061
+ commentsByFile={commentsByFile}
4062
+ showHidden={showHidden}
4063
+ sectionsExpanded={sectionsExpanded}
4064
+ onToggleSection={toggleSection}
4065
+ onNewBoard={createBoard}
4066
+ onDeleteBoard={deleteBoard}
4067
+ collapsed={!sidebarOpen}
4068
+ onCollapse={() => setSidebarOpen(false)}
4069
+ />
4070
+ <div className="main">
4071
+ <Viewport
4072
+ tabs={tabs}
4073
+ activePath={activePath}
4074
+ registerIframe={registerIframe}
4075
+ systemData={systemData}
4076
+ onOpenFromSystem={openTab}
4077
+ onSelectDs={loadSystemData}
4078
+ project={project}
4079
+ cfg={cfg}
4080
+ />
4081
+ </div>
4082
+ {/* Right dock — one panel at a time. Inspector takes precedence when
4083
+ open (T6); else the comments panel. */}
4084
+ {inspectorOpen ? (
4085
+ <InspectorPanel selected={selected} onClose={() => setInspectorOpen(false)} />
4086
+ ) : commentsPanelOpen ? (
4087
+ <CommentsPanel
4088
+ commentsByFile={commentsByFile}
4089
+ filter={commentsFilter}
4090
+ setFilter={setCommentsFilter}
4091
+ activePath={activePath}
4092
+ focusedId={focusedCommentId}
4093
+ onJump={jumpToComment}
4094
+ onResolve={resolveComment}
4095
+ onReopen={reopenComment}
4096
+ onDelete={deleteComment}
4097
+ />
4098
+ ) : null}
4099
+ </div>
2956
4100
  <StatusBar
2957
4101
  activePath={activePath}
2958
4102
  selected={selected}
@@ -2964,20 +4108,59 @@ function App() {
2964
4108
  syncStatus={syncStatus}
2965
4109
  />
2966
4110
  </div>
2967
- {commentsPanelOpen && (
2968
- <CommentsPanel
2969
- commentsByFile={commentsByFile}
2970
- filter={commentsFilter}
2971
- setFilter={setCommentsFilter}
4111
+ <CommandPalette
4112
+ open={paletteOpen}
4113
+ onClose={() => setPaletteOpen(false)}
4114
+ actions={paletteActions}
4115
+ />
4116
+ {exportDialog && (
4117
+ <ExportDialog
4118
+ mode={exportDialog.mode}
4119
+ initialScope={exportDialog.scope}
2972
4120
  activePath={activePath}
2973
- focusedId={focusedCommentId}
2974
- onJump={jumpToComment}
2975
- onResolve={resolveComment}
2976
- onReopen={reopenComment}
2977
- onDelete={deleteComment}
4121
+ onClose={() => setExportDialog(null)}
2978
4122
  />
2979
4123
  )}
2980
- <HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
4124
+ <HelpModal
4125
+ open={helpOpen}
4126
+ onClose={() => setHelpOpen(false)}
4127
+ onStartTour={() => {
4128
+ setHelpOpen(false);
4129
+ startTour(USAGE_TOUR);
4130
+ }}
4131
+ />
4132
+ <WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
4133
+ {usageNudge && !tourSteps && (
4134
+ <div className="mdcc-tour-nudge" role="status" aria-live="polite">
4135
+ <div className="mdcc-tour-nudge__body">
4136
+ New here? Take a 60-second tour of the canvas browser.
4137
+ </div>
4138
+ <button
4139
+ type="button"
4140
+ className="mdcc-tour-nudge__cta"
4141
+ onClick={() => {
4142
+ markUsageSeen();
4143
+ startTour(USAGE_TOUR);
4144
+ }}
4145
+ >
4146
+ Start
4147
+ </button>
4148
+ <button
4149
+ type="button"
4150
+ className="mdcc-tour-nudge__skip"
4151
+ aria-label="Dismiss"
4152
+ onClick={markUsageSeen}
4153
+ >
4154
+ ×
4155
+ </button>
4156
+ </div>
4157
+ )}
4158
+ <TourOverlay
4159
+ steps={tourSteps ?? []}
4160
+ open={!!tourSteps}
4161
+ onClose={() => setTourSteps(null)}
4162
+ onComplete={markUsageSeen}
4163
+ />
2981
4164
  </div>
2982
4165
  );
2983
4166
  }