@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
@@ -3,9 +3,10 @@
3
3
 
4
4
  import crypto from 'node:crypto';
5
5
  import type { Dirent } from 'node:fs';
6
- import { readdir, readFile, stat as statp } from 'node:fs/promises';
6
+ import { mkdir, readdir, readFile, rename, stat as statp } from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
 
9
+ import { renderBriefBoard, validateCanvasName } from './canvas-create.ts';
9
10
  import type { Context } from './context.ts';
10
11
 
11
12
  const SKIP_DIRS = new Set([
@@ -95,6 +96,9 @@ export interface Comment {
95
96
  id: string;
96
97
  file: string;
97
98
  selector: string;
99
+ /** Occurrence index among `querySelectorAll(selector)` — disambiguates a
100
+ * component repeated within one artboard. Absent on legacy comments. */
101
+ index?: number;
98
102
  dom_path: string[];
99
103
  tag: string;
100
104
  classes: string;
@@ -129,6 +133,14 @@ export interface ExportHistoryEntry {
129
133
  at: string;
130
134
  }
131
135
 
136
+ export type CreateCanvasResult =
137
+ | { ok: true; file: string; rel: string; slug: string }
138
+ | { ok: false; status: number; error: string };
139
+
140
+ export type DeleteCanvasResult =
141
+ | { ok: true; rel: string; slug: string; trashed: string[]; trashDir: string }
142
+ | { ok: false; status: number; error: string };
143
+
132
144
  export interface Api {
133
145
  // File tree
134
146
  fileSlug(file: string): string;
@@ -168,6 +180,16 @@ export interface Api {
168
180
  // Annotations sidecar (Phase 5 — .design/<slug>.annotations.svg)
169
181
  loadAnnotations(file: string): Promise<string | null>;
170
182
  saveAnnotations(file: string, svg: string): Promise<boolean>;
183
+ // Phase 23 — content-addressed binary image write (drag-drop / paste / picker)
184
+ saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
185
+ // Create a blank brief board from the browser (Phase 22 — POST /_api/canvas)
186
+ createCanvas(input: {
187
+ name?: unknown;
188
+ kind?: unknown;
189
+ group?: unknown;
190
+ }): Promise<CreateCanvasResult>;
191
+ // Soft-delete a canvas from the browser (Phase 22 — DELETE /_api/canvas)
192
+ deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult>;
171
193
  // Aggregate data
172
194
  buildIndexData(): Promise<unknown>;
173
195
  buildSystemData(dsName?: string | null): Promise<unknown>;
@@ -200,8 +222,107 @@ const ANNOTATION_SVG_ELEMENTS = new Set([
200
222
  'polygon',
201
223
  'circle',
202
224
  'text',
225
+ // Annotation polish (item 4a) — `tspan` carries one line of multi-line text
226
+ // inside a `<text>` (SVG `<text>` ignores `\n`). Inert presentational element
227
+ // with no script capability — same safety class as `text`. Only `x`/`dy`
228
+ // (geometry) survive; the attribute denylist strips any on*/style/href.
229
+ 'tspan',
230
+ // Phase 23 — `image` (dropped/pasted raster). The ONLY element allowed to keep
231
+ // an href, and ONLY a relative assets/<sha8>.<ext> path (ASSET_IMAGE_HREF_RE) —
232
+ // every external / data: / javascript: / `..` href is still stripped. <image>
233
+ // is a passive include with no script capability. See DDR (Task 9).
234
+ 'image',
203
235
  ]);
204
236
 
237
+ /**
238
+ * Phase 23 — the ONLY href shape allowed to survive on an `<image>`: a relative,
239
+ * single-segment `assets/<name>.<ext>` path. Anchored (`^…$`), no scheme, no `/`
240
+ * beyond the one `assets/` segment (blocks `assets/../../etc/passwd`), no query,
241
+ * ext ∈ the four raster types the upload route accepts. Everything else —
242
+ * external, `data:`, `javascript:`, `..`, or any href on a non-image element —
243
+ * is stripped by Rule 3. Pairs with the asset-write caps (DDR Task 9).
244
+ */
245
+ export const ASSET_IMAGE_HREF_RE = /^assets\/[A-Za-z0-9._-]+\.(?:png|jpe?g|webp|gif)$/;
246
+
247
+ /** Phase 23 — hard ceiling on a single uploaded asset (10 MB). */
248
+ export const ASSET_MAX_BYTES = 10 * 1024 * 1024;
249
+
250
+ /**
251
+ * Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
252
+ * write budget for `/_api/asset`. Content-addressing dedupes IDENTICAL bytes,
253
+ * but a one-byte mutation (a PNG `tEXt` chunk / a single pixel) yields a fresh
254
+ * sha8 each time, so dedup is NOT a disk-fill defense. This caps total bytes a
255
+ * single dev-server instance will ever write to `assets/` — generous for real
256
+ * reference material, but bounds a scripted loop from the untrusted canvas
257
+ * origin. Overridable via `MAUDE_ASSET_SESSION_BUDGET` (bytes) for power users.
258
+ */
259
+ export const ASSET_SESSION_BUDGET = (() => {
260
+ const env = Number(process.env.MAUDE_ASSET_SESSION_BUDGET);
261
+ return Number.isFinite(env) && env > 0 ? env : 256 * 1024 * 1024;
262
+ })();
263
+
264
+ /**
265
+ * Phase 23 — content-type sniff from the first bytes (magic numbers). The
266
+ * declared name / extension / Content-Type is NEVER trusted — the bytes decide
267
+ * the stored extension (a `.png` name carrying GIF bytes is stored as `.gif`).
268
+ * SVG (XML/text) matches nothing here → returns null → rejected, so a
269
+ * script-bearing vector can't ride in through the image route. See DDR (Task 9).
270
+ */
271
+ export function sniffImageType(bytes: Uint8Array): 'png' | 'jpg' | 'gif' | 'webp' | null {
272
+ const b = bytes;
273
+ // PNG — 89 50 4E 47 0D 0A 1A 0A
274
+ if (
275
+ b.length >= 8 &&
276
+ b[0] === 0x89 &&
277
+ b[1] === 0x50 &&
278
+ b[2] === 0x4e &&
279
+ b[3] === 0x47 &&
280
+ b[4] === 0x0d &&
281
+ b[5] === 0x0a &&
282
+ b[6] === 0x1a &&
283
+ b[7] === 0x0a
284
+ ) {
285
+ return 'png';
286
+ }
287
+ // JPEG — FF D8 FF
288
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpg';
289
+ // GIF — "GIF87a" / "GIF89a"
290
+ if (
291
+ b.length >= 6 &&
292
+ b[0] === 0x47 &&
293
+ b[1] === 0x49 &&
294
+ b[2] === 0x46 &&
295
+ b[3] === 0x38 &&
296
+ (b[4] === 0x37 || b[4] === 0x39) &&
297
+ b[5] === 0x61
298
+ ) {
299
+ return 'gif';
300
+ }
301
+ // WEBP — "RIFF"????"WEBP"
302
+ if (
303
+ b.length >= 12 &&
304
+ b[0] === 0x52 &&
305
+ b[1] === 0x49 &&
306
+ b[2] === 0x46 &&
307
+ b[3] === 0x46 &&
308
+ b[8] === 0x57 &&
309
+ b[9] === 0x45 &&
310
+ b[10] === 0x42 &&
311
+ b[11] === 0x50
312
+ ) {
313
+ return 'webp';
314
+ }
315
+ return null;
316
+ }
317
+
318
+ export interface SaveAssetResult {
319
+ ok: boolean;
320
+ status?: number;
321
+ error?: string;
322
+ /** Relative `assets/<sha8>.<ext>` path on success. */
323
+ path?: string;
324
+ }
325
+
205
326
  /**
206
327
  * A3 (DDR-060 F1 re-audit) — sanitize an annotation SVG before it is persisted /
207
328
  * synced / mirrored into the collab Y.Map. ALLOWLIST, not denylist (the F1
@@ -224,37 +345,59 @@ const ANNOTATION_SVG_ELEMENTS = new Set([
224
345
  * synced file a peer / Claude-context ingests (defense-in-depth, DDR-054 §3).
225
346
  */
226
347
  export function sanitizeAnnotationSvg(svg: string): string {
227
- return (
228
- svg
229
- // 1. Remove the CONTENT of executable / instruction-bearing elements (not
230
- // just their tags) so an injected script body / `@import` / prompt-
231
- // injection string can't survive as inert text a future raw-renderer or
232
- // Claude-context read might act on. Namespace-tolerant (`svg:script`),
233
- // non-greedy to the first matching dangerous close tag.
234
- .replace(
235
- /<\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\b[\s\S]*?<\s*\/\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\s*>/gi,
236
- ''
237
- )
238
- // 2. Element allowlist — drop the markup of any tag whose LOCAL name isn't
239
- // in the fixed annotation vocabulary. `[^>]*` stops at the first `>`;
240
- // annotation attrs never contain a literal `>`.
241
- .replace(/<\/?\s*([a-zA-Z][\w:-]*)\b[^>]*>/g, (match, rawName: string) => {
242
- const local = String(rawName).split(':').pop()?.toLowerCase() ?? '';
243
- return ANNOTATION_SVG_ELEMENTS.has(local) ? match : '';
244
- })
245
- // 3. Attribute denylist on the surviving allowlisted elements — the legit
246
- // vocabulary uses no on*= / style= / *href=, so stripping them closes
247
- // inline handlers, CSS url(javascript:), and entity-encoded hrefs.
248
- // The leading boundary is a LOOKBEHIND on whitespace / quote / slash
249
- // (not a consumed `\s`) so a handler glued to the previous attribute's
250
- // closing quote `<circle r="2"onload="…"/>`, which HTML/SVG parsers
251
- // accept as a distinct attribute — is also stripped (Phase 24 security
252
- // review, DDR-067). The non-consuming lookbehind leaves the preceding
253
- // quote intact so the legit attribute it belonged to survives.
254
- .replace(
255
- /(?<=[\s"'/])(?:on[a-z]+|style|(?:[\w-]+:)?href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
256
- ''
257
- )
348
+ // Phase 23 — <image> href handling. Rule 3 below strips EVERY href (that's the
349
+ // zero-bypass invariant). To let a legit assets/ href survive WITHOUT
350
+ // re-opening the denylist race, we (a) neutralize any input-supplied marker so
351
+ // only this pre-pass can mint one, (b) on `<image>` ONLY, hoist a regex-valid
352
+ // assets href into a sanitizer-inert `data-mdcc-asset` marker (an external /
353
+ // data: / `..` / non-image href is left as a plain href → stripped by Rule 3),
354
+ // then (c) after the three rules, restore the marker back to `href` — but ONLY
355
+ // after RE-validating the value, so a forged marker can never smuggle a
356
+ // scheme/traversal back in. The restored value is, by construction, a safe
357
+ // same-origin path. See ASSET_IMAGE_HREF_RE + DDR (Task 9).
358
+ const hoisted = svg
359
+ .replace(/\sdata-mdcc-asset\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
360
+ .replace(/<\s*(?:[\w-]+:)?image\b[^>]*>/gi, (tag) => {
361
+ const m = tag.match(/(?:xlink:)?href\s*=\s*"([^"]*)"|(?:xlink:)?href\s*=\s*'([^']*)'/i);
362
+ const val = m ? (m[1] ?? m[2] ?? '') : '';
363
+ if (!val || !ASSET_IMAGE_HREF_RE.test(val)) return tag;
364
+ return tag.replace(/(?:xlink:)?href\s*=\s*("[^"]*"|'[^']*')/i, `data-mdcc-asset="${val}"`);
365
+ });
366
+ const cleaned = hoisted
367
+ // 1. Remove the CONTENT of executable / instruction-bearing elements (not
368
+ // just their tags) so an injected script body / `@import` / prompt-
369
+ // injection string can't survive as inert text a future raw-renderer or
370
+ // Claude-context read might act on. Namespace-tolerant (`svg:script`),
371
+ // non-greedy to the first matching dangerous close tag.
372
+ .replace(
373
+ /<\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\b[\s\S]*?<\s*\/\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\s*>/gi,
374
+ ''
375
+ )
376
+ // 2. Element allowlist — drop the markup of any tag whose LOCAL name isn't
377
+ // in the fixed annotation vocabulary. `[^>]*` stops at the first `>`;
378
+ // annotation attrs never contain a literal `>`.
379
+ .replace(/<\/?\s*([a-zA-Z][\w:-]*)\b[^>]*>/g, (match, rawName: string) => {
380
+ const local = String(rawName).split(':').pop()?.toLowerCase() ?? '';
381
+ return ANNOTATION_SVG_ELEMENTS.has(local) ? match : '';
382
+ })
383
+ // 3. Attribute denylist on the surviving allowlisted elements — the legit
384
+ // vocabulary uses no on*= / style= / *href=, so stripping them closes
385
+ // inline handlers, CSS url(javascript:), and entity-encoded hrefs.
386
+ // The leading boundary is a LOOKBEHIND on whitespace / quote / slash
387
+ // (not a consumed `\s`) so a handler glued to the previous attribute's
388
+ // closing quote — `<circle r="2"onload="…"/>`, which HTML/SVG parsers
389
+ // accept as a distinct attribute — is also stripped (Phase 24 security
390
+ // review, DDR-067). The non-consuming lookbehind leaves the preceding
391
+ // quote intact so the legit attribute it belonged to survives.
392
+ .replace(
393
+ /(?<=[\s"'/])(?:on[a-z]+|style|(?:[\w-]+:)?href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
394
+ ''
395
+ );
396
+ // Post-pass — restore the validated assets href on <image>. Re-validate the
397
+ // marker value (defense-in-depth: a forged data-mdcc-asset can only resolve to
398
+ // a safe same-origin assets path; anything with a scheme/traversal is dropped).
399
+ return cleaned.replace(/\sdata-mdcc-asset\s*=\s*"([^"]*)"/gi, (_whole, val: string) =>
400
+ ASSET_IMAGE_HREF_RE.test(val) ? ` href="${val}"` : ''
258
401
  );
259
402
  }
260
403
 
@@ -471,6 +614,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
471
614
  id: newCommentId(),
472
615
  file: payload.file,
473
616
  selector: String(payload.selector || ''),
617
+ index: typeof payload.index === 'number' ? payload.index : undefined,
474
618
  dom_path: Array.isArray(payload.dom_path) ? payload.dom_path.slice(0, 16) : [],
475
619
  tag: String(payload.tag || ''),
476
620
  classes: String(payload.classes || ''),
@@ -712,6 +856,276 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
712
856
  return true;
713
857
  }
714
858
 
859
+ // Phase 23 — content-addressed asset write. Reachable from the (potentially
860
+ // untrusted, DDR-054) canvas origin, so every cap is load-bearing, NOT
861
+ // optional (DDR Task 9):
862
+ // • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
863
+ // SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
864
+ // • ≤ 10 MB ceiling (assets get their OWN cap; never routed through the 1 MB
865
+ // SVG-text gate in saveAnnotations).
866
+ // • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
867
+ // dedupe → a malicious canvas can't fill the disk with N copies of one
868
+ // image, and orphan-on-delete is safe (shared content survives).
869
+ // • resolved-path containment assert (defense-in-depth; the name carries no
870
+ // user input, but a poisoned designRoot must still not escape).
871
+ // Running total of bytes this server instance has actually written (post-dedupe).
872
+ let assetBytesWritten = 0;
873
+ async function saveAsset(bytes: Uint8Array): Promise<SaveAssetResult> {
874
+ if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
875
+ if (bytes.length > ASSET_MAX_BYTES) {
876
+ return { ok: false, status: 413, error: 'asset exceeds the 10 MB cap' };
877
+ }
878
+ const kind = sniffImageType(bytes);
879
+ if (!kind) {
880
+ return {
881
+ ok: false,
882
+ status: 415,
883
+ error: 'unsupported image type — png/jpeg/gif/webp only (SVG rejected)',
884
+ };
885
+ }
886
+ const sha8 = crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 8);
887
+ const name = `${sha8}.${kind}`;
888
+ const assetsDir = path.join(paths.designRoot, 'assets');
889
+ const fileAbs = path.join(assetsDir, name);
890
+ // Containment backstop — the name is content-addressed (sha8 hex + sniffed
891
+ // ext), so there is no user-controlled path segment, but assert anyway.
892
+ const resolved = path.resolve(fileAbs);
893
+ const assetsResolved = path.resolve(assetsDir);
894
+ if (resolved !== path.join(assetsResolved, name)) {
895
+ return { ok: false, status: 400, error: 'resolved asset path escapes assets dir' };
896
+ }
897
+ try {
898
+ // Dedupe — identical bytes hash to the same name; skip the write if present.
899
+ if (!(await Bun.file(fileAbs).exists())) {
900
+ // Aggregate write budget (DDR-088 follow-up) — bounds a scripted
901
+ // one-byte-mutation disk-fill loop from the untrusted canvas origin.
902
+ // Only a genuinely NEW file counts (a dedupe hit is free).
903
+ if (assetBytesWritten + bytes.length > ASSET_SESSION_BUDGET) {
904
+ return {
905
+ ok: false,
906
+ status: 429,
907
+ error: 'asset write budget exceeded for this server session',
908
+ };
909
+ }
910
+ await mkdir(assetsDir, { recursive: true });
911
+ await Bun.write(fileAbs, bytes);
912
+ assetBytesWritten += bytes.length;
913
+ }
914
+ } catch (err) {
915
+ return { ok: false, status: 500, error: err instanceof Error ? err.message : 'write failed' };
916
+ }
917
+ return { ok: true, path: `assets/${name}` };
918
+ }
919
+
920
+ // Phase 22 — create a blank brief board from the browser file tree. Wired ONLY
921
+ // on the main origin (server.ts startMainServer); the segregated canvas origin
922
+ // (DDR-054) never exposes this — an untrusted canvas iframe must not be able to
923
+ // write arbitrary `.tsx` files. The single user-controlled value (`name`) is
924
+ // gated by `validateCanvasName` (path + JSX + JSON injection boundary); `group`
925
+ // is allowlisted to the configured canvas groups; a `resolve()` containment
926
+ // assert is the defense-in-depth backstop.
927
+ async function createCanvas(input: {
928
+ name?: unknown;
929
+ kind?: unknown;
930
+ group?: unknown;
931
+ }): Promise<CreateCanvasResult> {
932
+ // v1 only stamps blank boards — generation stays with `/design:new` (Claude).
933
+ const kind = input.kind == null || input.kind === '' ? 'brief-board' : input.kind;
934
+ if (kind !== 'brief-board') {
935
+ return { ok: false, status: 400, error: 'only kind "brief-board" is supported' };
936
+ }
937
+ const v = validateCanvasName(input.name);
938
+ if (!v.ok || !v.name || !v.componentName) {
939
+ return { ok: false, status: 400, error: v.error ?? 'invalid name' };
940
+ }
941
+
942
+ // Group must be one of the configured canvas groups (+ the new-canvas dir).
943
+ const allowed = new Set<string>(cfg.canvasGroups.map((g) => g.path));
944
+ allowed.add(cfg.newCanvasDir || 'ui');
945
+ const group =
946
+ input.group == null || input.group === '' ? cfg.newCanvasDir || 'ui' : String(input.group);
947
+ if (!allowed.has(group)) {
948
+ return { ok: false, status: 400, error: `group must be one of: ${[...allowed].join(', ')}` };
949
+ }
950
+
951
+ const groupAbs = path.join(paths.designRoot, group);
952
+ const fileAbs = path.join(groupAbs, `${v.name}.tsx`);
953
+ // Containment backstop, two layers (Phase 22 security review F2). The name
954
+ // regex already forbids traversal, but `group` comes from config
955
+ // (canvasGroups[].path / newCanvasDir), which `normalizeConfig` does NOT
956
+ // validate for `..`. So assert (1) the GROUP resolves inside designRoot —
957
+ // a config-supplied `../../etc` must not relocate the write out of the
958
+ // project — and (2) the file resolves inside that group. Without (1) a
959
+ // poisoned config would be an arbitrary-directory `.tsx` write (latent today
960
+ // because config isn't sync-scoped; closed now so a future config-sync can't
961
+ // re-open it).
962
+ const resolvedDesignRoot = path.resolve(paths.designRoot);
963
+ const resolvedGroup = path.resolve(groupAbs);
964
+ if (
965
+ resolvedGroup !== resolvedDesignRoot &&
966
+ !resolvedGroup.startsWith(`${resolvedDesignRoot}${path.sep}`)
967
+ ) {
968
+ return { ok: false, status: 400, error: 'canvas group resolves outside the design root' };
969
+ }
970
+ if (
971
+ path.resolve(fileAbs) !== path.join(resolvedGroup, `${v.name}.tsx`) ||
972
+ !path.resolve(fileAbs).startsWith(`${resolvedGroup}${path.sep}`)
973
+ ) {
974
+ return { ok: false, status: 400, error: 'resolved path escapes the canvas group' };
975
+ }
976
+ if (await Bun.file(fileAbs).exists()) {
977
+ return {
978
+ ok: false,
979
+ status: 409,
980
+ error: `a canvas named "${v.name}" already exists in ${group}`,
981
+ };
982
+ }
983
+
984
+ const rel = path.posix.join(group, `${v.name}.tsx`);
985
+ const slug = fileSlug(rel);
986
+ const dsName = cfg.defaultDesignSystem || 'project';
987
+ const platform = 'desktop';
988
+ const tsx = renderBriefBoard({
989
+ name: v.name,
990
+ componentName: v.componentName,
991
+ dsName,
992
+ platform,
993
+ seedHint: 'Empty brief board — annotate me',
994
+ historyDir: path.posix.join(paths.designRel, '_history', slug),
995
+ });
996
+ const now = new Date().toISOString();
997
+ const meta = {
998
+ kind: 'brief-board',
999
+ title: v.name,
1000
+ subtitle: 'brief board',
1001
+ brief: v.name,
1002
+ designSystem: dsName,
1003
+ platform,
1004
+ created: now,
1005
+ last_modified: now,
1006
+ };
1007
+ await Bun.write(fileAbs, tsx);
1008
+ await Bun.write(
1009
+ path.join(groupAbs, `${v.name}.meta.json`),
1010
+ `${JSON.stringify(meta, null, 2)}\n`
1011
+ );
1012
+ // designRel-prefixed path — matches the file-tree `file.path` shape so the
1013
+ // client can open it directly after reloadTree().
1014
+ return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
1015
+ }
1016
+
1017
+ // Phase 22 — SOFT-delete a canvas (DELETE /_api/canvas). Same trust boundary as
1018
+ // createCanvas: main-origin-only, never the untrusted canvas iframe origin
1019
+ // (DDR-054). Destructive, so it MOVES the whole sidecar set to
1020
+ // `<designRoot>/_trash/<stamp>__<slug>/` (recoverable locally) instead of a hard
1021
+ // rm — pairs with the existing `_history/` + /design:rollback model. Hub
1022
+ // propagation of the delete is deliberately out of scope (Phase 26 consent).
1023
+ async function deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult> {
1024
+ const raw = input?.file;
1025
+ if (typeof raw !== 'string' || !raw.trim()) {
1026
+ return { ok: false, status: 400, error: 'file is required' };
1027
+ }
1028
+ let rel = raw.trim();
1029
+ try {
1030
+ rel = decodeURIComponent(rel);
1031
+ } catch {
1032
+ /* leave as-is */
1033
+ }
1034
+ rel = rel.replace(/^\/+/, '');
1035
+ const prefix = `${paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
1036
+ if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
1037
+
1038
+ // Only a real `.tsx` canvas, no traversal.
1039
+ if (rel.includes('..')) return { ok: false, status: 400, error: 'invalid path' };
1040
+ if (!/\.tsx$/i.test(rel)) {
1041
+ return { ok: false, status: 400, error: 'only .tsx canvases can be deleted' };
1042
+ }
1043
+
1044
+ const fileAbs = path.join(paths.designRoot, rel);
1045
+ const resolvedDesignRoot = path.resolve(paths.designRoot);
1046
+ const resolvedFile = path.resolve(fileAbs);
1047
+ if (
1048
+ resolvedFile !== resolvedDesignRoot &&
1049
+ !resolvedFile.startsWith(`${resolvedDesignRoot}${path.sep}`)
1050
+ ) {
1051
+ return { ok: false, status: 400, error: 'path escapes the design root' };
1052
+ }
1053
+
1054
+ // Must live under a NON-DS canvas group — never the design system (`system`),
1055
+ // never a loose root file (config.json, README). Deleting DS sources is not a
1056
+ // canvas operation.
1057
+ const deletable = cfg.canvasGroups.filter(
1058
+ (g) => g.label !== 'Design system' && !/^system(\/|$)/.test(g.path)
1059
+ );
1060
+ const inGroup = deletable.some((g) => {
1061
+ const gAbs = path.resolve(path.join(paths.designRoot, g.path));
1062
+ return resolvedFile.startsWith(`${gAbs}${path.sep}`);
1063
+ });
1064
+ if (!inGroup) {
1065
+ return {
1066
+ ok: false,
1067
+ status: 400,
1068
+ error: 'only canvases under a managed canvas group can be deleted',
1069
+ };
1070
+ }
1071
+
1072
+ if (!(await Bun.file(fileAbs).exists())) {
1073
+ return { ok: false, status: 404, error: 'canvas not found' };
1074
+ }
1075
+
1076
+ const slug = fileSlug(rel);
1077
+ const base = path.basename(rel).replace(/\.tsx$/i, '');
1078
+ const groupDir = path.dirname(fileAbs);
1079
+ // Filesystem-safe wall-clock stamp (no `:`) so repeated deletes of the same
1080
+ // canvas don't collide in _trash.
1081
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1082
+ const trashDir = path.join(paths.designRoot, '_trash', `${stamp}__${slug}`);
1083
+ await mkdir(trashDir, { recursive: true });
1084
+
1085
+ const trashed: string[] = [];
1086
+ const moveIfExists = async (src: string, destName: string) => {
1087
+ try {
1088
+ await statp(src); // throws if absent (works for files AND dirs)
1089
+ } catch {
1090
+ return;
1091
+ }
1092
+ try {
1093
+ await rename(src, path.join(trashDir, destName));
1094
+ trashed.push(path.relative(paths.designRoot, src));
1095
+ } catch {
1096
+ /* best-effort — a sidecar that can't move shouldn't abort the delete */
1097
+ }
1098
+ };
1099
+
1100
+ // Primary + the full sidecar set (annotations, meta, history, canvas-state,
1101
+ // comments). Flattened names so the trash dir is a self-contained bundle.
1102
+ await moveIfExists(fileAbs, `${base}.tsx`);
1103
+ await moveIfExists(path.join(groupDir, `${base}.meta.json`), `${base}.meta.json`);
1104
+ await moveIfExists(
1105
+ path.join(paths.designRoot, `${slug}.annotations.svg`),
1106
+ `${slug}.annotations.svg`
1107
+ );
1108
+ await moveIfExists(path.join(paths.designRoot, '_history', slug), `_history__${slug}`);
1109
+ await moveIfExists(
1110
+ path.join(paths.canvasStateDir, `${slug}.json`),
1111
+ `_canvas-state__${slug}.json`
1112
+ );
1113
+ await moveIfExists(path.join(paths.commentsDir, `${slug}.json`), `_comments__${slug}.json`);
1114
+
1115
+ await Bun.write(
1116
+ path.join(trashDir, '_trash-manifest.json'),
1117
+ `${JSON.stringify({ canvas: rel, slug, deletedAt: new Date().toISOString(), trashed }, null, 2)}\n`
1118
+ );
1119
+
1120
+ return {
1121
+ ok: true,
1122
+ rel,
1123
+ slug,
1124
+ trashed,
1125
+ trashDir: path.relative(paths.repoRoot, trashDir),
1126
+ };
1127
+ }
1128
+
715
1129
  async function saveCanvasState(file: string, state: Record<string, unknown>) {
716
1130
  if (!state || typeof state !== 'object') return;
717
1131
  const safe: Record<string, unknown> = {};
@@ -732,6 +1146,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
732
1146
  async function buildIndexData() {
733
1147
  const groups = [];
734
1148
 
1149
+ // DDR-093 — per-canvas design-system map (repo-relative canvas path → DS
1150
+ // name) so the client's canvasUrl() injects each UI canvas's OWN tokens
1151
+ // instead of unconditionally designSystems[0]. Populated from the canvas
1152
+ // groups below; returned on the payload and folded into the client cfg.
1153
+ const canvasDesignSystems: Record<string, string> = {};
1154
+ const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
1155
+ // A file under `system/<folder>/` belongs to the DS that owns that folder —
1156
+ // path-authoritative, because specimens/ui_kits rarely carry a sidecar
1157
+ // `designSystem`. Returns null for `ui/` canvases (resolved via sidecar).
1158
+ const ownerDsName = (repoRelPath: string): string | null => {
1159
+ let r = repoRelPath;
1160
+ const prefix = `${paths.designRel}/`;
1161
+ if (r.startsWith(prefix)) r = r.slice(prefix.length);
1162
+ const m = r.match(/^system\/([^/]+)\//);
1163
+ if (!m) return null;
1164
+ const folder = m[1];
1165
+ const owner = (cfg.designSystems ?? []).find(
1166
+ (d) => d.path === `system/${folder}` || d.path.endsWith(`/${folder}`)
1167
+ );
1168
+ return owner?.name ?? null;
1169
+ };
1170
+
735
1171
  // PROJECT — top-level .design/ files (README.md, INDEX.md, config.json, …).
736
1172
  // These are non-HTML so they're listed but not openable in the iframe; the
737
1173
  // sidebar can still display them as context (mirrors CV-08 mock).
@@ -777,6 +1213,20 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
777
1213
  const filePaths = isDs
778
1214
  ? await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.md', '.css', '.json'])
779
1215
  : await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.css', '.json']);
1216
+ // DDR-093 — record each `.tsx` canvas's design system. canvasUrl() only
1217
+ // injects tokens for `.tsx`, so skip everything else. Path-owned DS wins
1218
+ // (system/<ds>/…); otherwise the sidecar's `meta.designSystem`, defaulting
1219
+ // to the project default when the canvas declares none.
1220
+ for (const fp of filePaths) {
1221
+ if (!fp.endsWith('.tsx')) continue;
1222
+ let dsName = ownerDsName(fp);
1223
+ if (!dsName) {
1224
+ const meta = await loadCanvasMeta(fp);
1225
+ const declared = meta?.designSystem;
1226
+ dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
1227
+ }
1228
+ canvasDesignSystems[fp] = dsName;
1229
+ }
780
1230
  // Strip down to `g.path` so per-DS folders (`project`, `beta`, …) show
781
1231
  // up as the top-level dirs inside the DS section. Single-DS configs get
782
1232
  // a wrapper folder too — slightly more verbose, but consistent with
@@ -837,6 +1287,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
837
1287
  projectLabel: ctx.projectLabel,
838
1288
  designRoot: paths.designRel,
839
1289
  groups,
1290
+ canvasDesignSystems,
840
1291
  };
841
1292
  }
842
1293
 
@@ -1078,6 +1529,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1078
1529
  patchCanvasMeta,
1079
1530
  loadAnnotations,
1080
1531
  saveAnnotations,
1532
+ saveAsset,
1533
+ createCanvas,
1534
+ deleteCanvas,
1081
1535
  buildIndexData,
1082
1536
  buildSystemData,
1083
1537
  loadExportHistory,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file artboard-activity-overlay.tsx — Phase 13 / DDR-029 overlay.
3
- * @scope plugins/design/dev-server/artboard-activity-overlay.tsx
3
+ * @scope apps/studio/artboard-activity-overlay.tsx
4
4
  * @purpose The "agent works here" chrome for a single artboard: an animated
5
5
  * rim + a top-right `editing — <label>` badge. Rendered by
6
6
  * `DCArtboard` as a world-coord sibling of the `<article>`, so it
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file artboard-marquee.tsx — Task 24 sub-item (Wave 2 follow-up, G8)
3
- * @scope plugins/design/dev-server/artboard-marquee.tsx
3
+ * @scope apps/studio/artboard-marquee.tsx
4
4
  * @purpose Drag-to-lasso multi-select for artboards. Bare-button pointerdown
5
5
  * on empty world (NOT inside any artboard, NOT on floating chrome)
6
6
  * starts a marquee. 4 px drag threshold suppresses click jitter.