@1agh/maude 0.28.1 → 0.30.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 (372) hide show
  1. package/README.md +3 -3
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +4537 -0
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
  11. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  12. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  13. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
  15. package/apps/studio/bin/annotate.mjs +732 -0
  16. package/apps/studio/bin/annotate.sh +17 -0
  17. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  18. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  19. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  20. package/apps/studio/bin/read-annotations.mjs +573 -0
  21. package/apps/studio/bin/read-annotations.sh +11 -0
  22. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  24. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  25. package/apps/studio/bin/to-lottie-verify.html +68 -0
  26. package/apps/studio/bin/to-lottie.sh +128 -0
  27. package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
  28. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
  29. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  30. package/apps/studio/canvas-create.ts +104 -0
  31. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
  32. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
  33. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
  34. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  35. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  36. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  37. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
  38. package/apps/studio/client/app.jsx +6742 -0
  39. package/apps/studio/client/canvas-url.js +81 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  41. package/apps/studio/client/index.html +16 -0
  42. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  43. package/apps/studio/client/styles/3-shell-maude.css +1161 -0
  44. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  45. package/apps/studio/client/styles/4-components-maude.css +74 -0
  46. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
  47. package/apps/studio/client/styles/5-maude-overrides.css +137 -0
  48. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  49. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  50. package/apps/studio/client/tour/overlay.jsx +279 -0
  51. package/apps/studio/client/tour/usage-tour.js +48 -0
  52. package/apps/studio/client/whats-new-seen.js +52 -0
  53. package/apps/studio/client/whats-new.jsx +250 -0
  54. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
  55. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  56. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
  57. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
  58. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
  59. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
  60. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
  61. package/apps/studio/dist/client.bundle.js +25545 -0
  62. package/apps/studio/dist/comment-mount.js +2046 -0
  63. package/apps/studio/dist/styles.css +8018 -0
  64. package/apps/studio/dom-selection.ts +342 -0
  65. package/apps/studio/draw/animate.ts +333 -0
  66. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  67. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  68. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  69. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  70. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  71. package/apps/studio/draw/morph.ts +204 -0
  72. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  73. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  74. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  75. package/apps/studio/draw/serialize-animate.ts +324 -0
  76. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  77. package/apps/studio/draw/test/animate.test.ts +172 -0
  78. package/apps/studio/draw/test/morph.test.ts +107 -0
  79. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  80. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  82. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
  83. package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
  84. package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
  85. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
  86. package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
  87. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  88. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
  89. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  90. package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
  91. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
  92. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
  93. package/apps/studio/sync/cold-start.ts +158 -0
  94. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
  95. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
  96. package/apps/studio/sync/journal.ts +190 -0
  97. package/apps/studio/sync/migrate-seed.ts +321 -0
  98. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
  99. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
  100. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
  101. package/apps/studio/test/annotate-write.test.ts +184 -0
  102. package/apps/studio/test/annotations-align.test.ts +88 -0
  103. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  104. package/apps/studio/test/annotations-groups.test.ts +231 -0
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  107. package/apps/studio/test/annotations-snap.test.ts +79 -0
  108. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  109. package/apps/studio/test/asset-api.test.ts +149 -0
  110. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  111. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  112. package/apps/studio/test/canvas-edit.test.ts +319 -0
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  114. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
  116. package/apps/studio/test/canvas-url.test.ts +99 -0
  117. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
  118. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
  119. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
  120. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  121. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  122. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  123. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  124. package/apps/studio/test/motion-sample.test.ts +35 -0
  125. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  126. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  127. package/apps/studio/test/read-annotations.test.ts +409 -0
  128. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  130. package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
  131. package/apps/studio/test/sync-agent.test.ts +575 -0
  132. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
  133. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  134. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
  135. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
  136. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
  137. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  138. package/apps/studio/test/sync-journal.test.ts +176 -0
  139. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
  140. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
  141. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  142. package/apps/studio/test/use-annotation-resize.test.ts +402 -0
  143. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
  144. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  145. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
  146. package/apps/studio/test/whats-new.test.ts +101 -0
  147. package/apps/studio/text-imports.d.ts +9 -0
  148. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
  149. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
  150. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  151. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
  152. package/apps/studio/use-annotation-resize.tsx +912 -0
  153. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
  154. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  156. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
  157. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  158. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
  159. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  161. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
  162. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  163. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
  164. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  165. package/apps/studio/whats-new.json +264 -0
  166. package/apps/studio/whats-new.schema.json +86 -0
  167. package/apps/studio/whats-new.ts +87 -0
  168. package/cli/bin/maude.mjs +10 -0
  169. package/cli/commands/design-link.test.mjs +84 -0
  170. package/cli/commands/design.mjs +16 -7
  171. package/cli/commands/design.test.mjs +40 -0
  172. package/cli/commands/help.mjs +8 -2
  173. package/cli/commands/hub.mjs +3 -3
  174. package/cli/commands/hub.test.mjs +1 -1
  175. package/cli/commands/scenario-report.mjs +2 -9
  176. package/cli/lib/design-link.mjs +51 -1
  177. package/cli/lib/flow-design-integration.test.mjs +1 -1
  178. package/cli/lib/gitignore-block.mjs +1 -0
  179. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  180. package/cli/lib/preflight.mjs +1 -1
  181. package/package.json +15 -15
  182. package/plugins/design/templates/_shell.html +28 -4
  183. package/plugins/design/templates/brief-board.tsx.template +61 -0
  184. package/plugins/design/templates/canvas.tsx.template +1 -1
  185. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  186. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  187. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  188. package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
  189. package/plugins/design/dev-server/client/app.jsx +0 -2985
  190. package/plugins/design/dev-server/client/index.html +0 -15
  191. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  192. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  193. package/plugins/design/dev-server/dist/styles.css +0 -2710
  194. package/plugins/design/dev-server/dom-selection.ts +0 -156
  195. package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
  196. package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
  197. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
  198. package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
  199. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
  200. package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
  201. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  340. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  341. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  342. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  343. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  344. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  345. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  346. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  347. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  348. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  349. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  350. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  351. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  352. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  353. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  354. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  355. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  356. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  357. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  358. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  359. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  362. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  363. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  364. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  365. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  366. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  367. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  368. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  369. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  370. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  371. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  372. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -3,9 +3,16 @@
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';
10
+ import {
11
+ CanvasEditError,
12
+ editAttribute,
13
+ removeAttribute,
14
+ editText as runEditText,
15
+ } from './canvas-edit.ts';
9
16
  import type { Context } from './context.ts';
10
17
 
11
18
  const SKIP_DIRS = new Set([
@@ -95,6 +102,9 @@ export interface Comment {
95
102
  id: string;
96
103
  file: string;
97
104
  selector: string;
105
+ /** Occurrence index among `querySelectorAll(selector)` — disambiguates a
106
+ * component repeated within one artboard. Absent on legacy comments. */
107
+ index?: number;
98
108
  dom_path: string[];
99
109
  tag: string;
100
110
  classes: string;
@@ -129,6 +139,19 @@ export interface ExportHistoryEntry {
129
139
  at: string;
130
140
  }
131
141
 
142
+ export type CreateCanvasResult =
143
+ | { ok: true; file: string; rel: string; slug: string }
144
+ | { ok: false; status: number; error: string };
145
+
146
+ export type DeleteCanvasResult =
147
+ | { ok: true; rel: string; slug: string; trashed: string[]; trashDir: string }
148
+ | { ok: false; status: number; error: string };
149
+
150
+ /** Phase 12 — result of an in-canvas direct edit (`editCss` / `editText`). */
151
+ export type EditOpResult =
152
+ | { ok: true; delta: number }
153
+ | { ok: false; status: number; error: string };
154
+
132
155
  export interface Api {
133
156
  // File tree
134
157
  fileSlug(file: string): string;
@@ -168,6 +191,35 @@ export interface Api {
168
191
  // Annotations sidecar (Phase 5 — .design/<slug>.annotations.svg)
169
192
  loadAnnotations(file: string): Promise<string | null>;
170
193
  saveAnnotations(file: string, svg: string): Promise<boolean>;
194
+ // Phase 23 — content-addressed binary image write (drag-drop / paste / picker)
195
+ saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
196
+ // Create a blank brief board from the browser (Phase 22 — POST /_api/canvas)
197
+ createCanvas(input: {
198
+ name?: unknown;
199
+ kind?: unknown;
200
+ group?: unknown;
201
+ }): Promise<CreateCanvasResult>;
202
+ // Soft-delete a canvas from the browser (Phase 22 — DELETE /_api/canvas)
203
+ deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult>;
204
+ // Phase 12 (DDR-103) — single-property inline CSS edit (POST /_api/edit-css).
205
+ // Main-origin only: writes one key into the element's inline `style={{}}` object.
206
+ editCss(input: {
207
+ canvas?: unknown;
208
+ id?: unknown;
209
+ property?: unknown;
210
+ value?: unknown;
211
+ }): Promise<EditOpResult>;
212
+ // Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
213
+ editText(input: { canvas?: unknown; id?: unknown; text?: unknown }): Promise<EditOpResult>;
214
+ // Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
215
+ // only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
216
+ // writes a plain JSX attribute via editAttribute's non-`style.` path.
217
+ editAttr(input: {
218
+ canvas?: unknown;
219
+ id?: unknown;
220
+ attr?: unknown;
221
+ value?: unknown;
222
+ }): Promise<EditOpResult>;
171
223
  // Aggregate data
172
224
  buildIndexData(): Promise<unknown>;
173
225
  buildSystemData(dsName?: string | null): Promise<unknown>;
@@ -182,80 +234,92 @@ export interface ApiHooks {
182
234
  onAnnotationsChanged?: (file: string, svg: string) => void;
183
235
  }
184
236
 
237
+ // FigJam v3 — the annotation sanitizer moved to annotations-model.ts (the
238
+ // schema owner; the allowlist guards exactly that vocabulary, and the
239
+ // headless `maude design annotate` write verb needs it without pulling the
240
+ // server modules). Re-exported here so every existing `from './api.ts'`
241
+ // import keeps working unchanged.
242
+ export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.ts';
243
+
244
+ import { sanitizeAnnotationSvg } from './annotations-model.ts';
245
+
246
+ /** Phase 23 — hard ceiling on a single uploaded asset (10 MB). */
247
+ export const ASSET_MAX_BYTES = 10 * 1024 * 1024;
248
+
185
249
  /**
186
- * Elements the annotation tool legitimately emits (`strokesToSvg` in
187
- * annotations-layer.tsx). This is the WHOLE vocabulary purely presentational.
250
+ * Phase 23 security review (DDR-088 follow-up) aggregate per-server-instance
251
+ * write budget for `/_api/asset`. Content-addressing dedupes IDENTICAL bytes,
252
+ * but a one-byte mutation (a PNG `tEXt` chunk / a single pixel) yields a fresh
253
+ * sha8 each time, so dedup is NOT a disk-fill defense. This caps total bytes a
254
+ * single dev-server instance will ever write to `assets/` — generous for real
255
+ * reference material, but bounds a scripted loop from the untrusted canvas
256
+ * origin. Overridable via `MAUDE_ASSET_SESSION_BUDGET` (bytes) for power users.
188
257
  */
189
- const ANNOTATION_SVG_ELEMENTS = new Set([
190
- 'svg',
191
- 'g',
192
- 'path',
193
- 'rect',
194
- 'ellipse',
195
- 'line',
196
- 'polyline',
197
- // Phase 24 — `polygon` (diamond / triangle shape primitives + closed arrowhead
198
- // outlines + diamond heads) and `circle` (circle arrowhead). Inert shape
199
- // elements with no script capability — same safety class as the rest.
200
- 'polygon',
201
- 'circle',
202
- 'text',
203
- ]);
258
+ export const ASSET_SESSION_BUDGET = (() => {
259
+ const env = Number(process.env.MAUDE_ASSET_SESSION_BUDGET);
260
+ return Number.isFinite(env) && env > 0 ? env : 256 * 1024 * 1024;
261
+ })();
204
262
 
205
263
  /**
206
- * A3 (DDR-060 F1 re-audit) sanitize an annotation SVG before it is persisted /
207
- * synced / mirrored into the collab Y.Map. ALLOWLIST, not denylist (the F1
208
- * confirming pass showed a denylist loses the race: `<svg:script>` via namespace,
209
- * `javascript&#58;` via HTML entity, `<style>@import url()>`, CSS `url(javascript:)`
210
- * all slipped a tag-name denylist). Two rules, both keyed to the fixed legit
211
- * vocabulary so they're zero-regression on real annotations:
212
- *
213
- * 1. Element allowlist — drop the markup of any tag whose LOCAL name (namespace
214
- * stripped, so `svg:script` → `script`) isn't in ANNOTATION_SVG_ELEMENTS.
215
- * Dropped-tag text content survives as inert text (not in a script/style
216
- * context), which is harmless.
217
- * 2. Attribute denylist on survivors — the legit vocabulary uses none of
218
- * `on*=`, `style=`, or any `href`, so stripping them closes inline handlers,
219
- * CSS `url(javascript:)`, and entity-encoded `xlink:href` in one pass.
220
- *
221
- * The current consumer (`svgToStrokes` → DOMParser image/svg+xml → structured
222
- * strokes → React) never raw-renders this string, so the live XSS risk is already
223
- * nil; this keeps "inert" TRUE for any future raw-render consumer and for the
224
- * synced file a peer / Claude-context ingests (defense-in-depth, DDR-054 §3).
264
+ * Phase 23 content-type sniff from the first bytes (magic numbers). The
265
+ * declared name / extension / Content-Type is NEVER trusted the bytes decide
266
+ * the stored extension (a `.png` name carrying GIF bytes is stored as `.gif`).
267
+ * SVG (XML/text) matches nothing here returns null → rejected, so a
268
+ * script-bearing vector can't ride in through the image route. See DDR (Task 9).
225
269
  */
226
- 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
- )
258
- );
270
+ export function sniffImageType(bytes: Uint8Array): 'png' | 'jpg' | 'gif' | 'webp' | null {
271
+ const b = bytes;
272
+ // PNG — 89 50 4E 47 0D 0A 1A 0A
273
+ if (
274
+ b.length >= 8 &&
275
+ b[0] === 0x89 &&
276
+ b[1] === 0x50 &&
277
+ b[2] === 0x4e &&
278
+ b[3] === 0x47 &&
279
+ b[4] === 0x0d &&
280
+ b[5] === 0x0a &&
281
+ b[6] === 0x1a &&
282
+ b[7] === 0x0a
283
+ ) {
284
+ return 'png';
285
+ }
286
+ // JPEG FF D8 FF
287
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpg';
288
+ // GIF — "GIF87a" / "GIF89a"
289
+ if (
290
+ b.length >= 6 &&
291
+ b[0] === 0x47 &&
292
+ b[1] === 0x49 &&
293
+ b[2] === 0x46 &&
294
+ b[3] === 0x38 &&
295
+ (b[4] === 0x37 || b[4] === 0x39) &&
296
+ b[5] === 0x61
297
+ ) {
298
+ return 'gif';
299
+ }
300
+ // WEBP — "RIFF"????"WEBP"
301
+ if (
302
+ b.length >= 12 &&
303
+ b[0] === 0x52 &&
304
+ b[1] === 0x49 &&
305
+ b[2] === 0x46 &&
306
+ b[3] === 0x46 &&
307
+ b[8] === 0x57 &&
308
+ b[9] === 0x45 &&
309
+ b[10] === 0x42 &&
310
+ b[11] === 0x50
311
+ ) {
312
+ return 'webp';
313
+ }
314
+ return null;
315
+ }
316
+
317
+ export interface SaveAssetResult {
318
+ ok: boolean;
319
+ status?: number;
320
+ error?: string;
321
+ /** Relative `assets/<sha8>.<ext>` path on success. */
322
+ path?: string;
259
323
  }
260
324
 
261
325
  export function createApi(ctx: Context, hooks: ApiHooks): Api {
@@ -471,6 +535,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
471
535
  id: newCommentId(),
472
536
  file: payload.file,
473
537
  selector: String(payload.selector || ''),
538
+ index: typeof payload.index === 'number' ? payload.index : undefined,
474
539
  dom_path: Array.isArray(payload.dom_path) ? payload.dom_path.slice(0, 16) : [],
475
540
  tag: String(payload.tag || ''),
476
541
  classes: String(payload.classes || ''),
@@ -712,6 +777,444 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
712
777
  return true;
713
778
  }
714
779
 
780
+ // Phase 23 — content-addressed asset write. Reachable from the (potentially
781
+ // untrusted, DDR-054) canvas origin, so every cap is load-bearing, NOT
782
+ // optional (DDR Task 9):
783
+ // • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
784
+ // SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
785
+ // • ≤ 10 MB ceiling (assets get their OWN cap; never routed through the 1 MB
786
+ // SVG-text gate in saveAnnotations).
787
+ // • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
788
+ // dedupe → a malicious canvas can't fill the disk with N copies of one
789
+ // image, and orphan-on-delete is safe (shared content survives).
790
+ // • resolved-path containment assert (defense-in-depth; the name carries no
791
+ // user input, but a poisoned designRoot must still not escape).
792
+ // Running total of bytes this server instance has actually written (post-dedupe).
793
+ let assetBytesWritten = 0;
794
+ async function saveAsset(bytes: Uint8Array): Promise<SaveAssetResult> {
795
+ if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
796
+ if (bytes.length > ASSET_MAX_BYTES) {
797
+ return { ok: false, status: 413, error: 'asset exceeds the 10 MB cap' };
798
+ }
799
+ const kind = sniffImageType(bytes);
800
+ if (!kind) {
801
+ return {
802
+ ok: false,
803
+ status: 415,
804
+ error: 'unsupported image type — png/jpeg/gif/webp only (SVG rejected)',
805
+ };
806
+ }
807
+ const sha8 = crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 8);
808
+ const name = `${sha8}.${kind}`;
809
+ const assetsDir = path.join(paths.designRoot, 'assets');
810
+ const fileAbs = path.join(assetsDir, name);
811
+ // Containment backstop — the name is content-addressed (sha8 hex + sniffed
812
+ // ext), so there is no user-controlled path segment, but assert anyway.
813
+ const resolved = path.resolve(fileAbs);
814
+ const assetsResolved = path.resolve(assetsDir);
815
+ if (resolved !== path.join(assetsResolved, name)) {
816
+ return { ok: false, status: 400, error: 'resolved asset path escapes assets dir' };
817
+ }
818
+ try {
819
+ // Dedupe — identical bytes hash to the same name; skip the write if present.
820
+ if (!(await Bun.file(fileAbs).exists())) {
821
+ // Aggregate write budget (DDR-088 follow-up) — bounds a scripted
822
+ // one-byte-mutation disk-fill loop from the untrusted canvas origin.
823
+ // Only a genuinely NEW file counts (a dedupe hit is free).
824
+ if (assetBytesWritten + bytes.length > ASSET_SESSION_BUDGET) {
825
+ return {
826
+ ok: false,
827
+ status: 429,
828
+ error: 'asset write budget exceeded for this server session',
829
+ };
830
+ }
831
+ await mkdir(assetsDir, { recursive: true });
832
+ await Bun.write(fileAbs, bytes);
833
+ assetBytesWritten += bytes.length;
834
+ }
835
+ } catch (err) {
836
+ return { ok: false, status: 500, error: err instanceof Error ? err.message : 'write failed' };
837
+ }
838
+ return { ok: true, path: `assets/${name}` };
839
+ }
840
+
841
+ // Phase 22 — create a blank brief board from the browser file tree. Wired ONLY
842
+ // on the main origin (server.ts startMainServer); the segregated canvas origin
843
+ // (DDR-054) never exposes this — an untrusted canvas iframe must not be able to
844
+ // write arbitrary `.tsx` files. The single user-controlled value (`name`) is
845
+ // gated by `validateCanvasName` (path + JSX + JSON injection boundary); `group`
846
+ // is allowlisted to the configured canvas groups; a `resolve()` containment
847
+ // assert is the defense-in-depth backstop.
848
+ async function createCanvas(input: {
849
+ name?: unknown;
850
+ kind?: unknown;
851
+ group?: unknown;
852
+ }): Promise<CreateCanvasResult> {
853
+ // v1 only stamps blank boards — generation stays with `/design:new` (Claude).
854
+ const kind = input.kind == null || input.kind === '' ? 'brief-board' : input.kind;
855
+ if (kind !== 'brief-board') {
856
+ return { ok: false, status: 400, error: 'only kind "brief-board" is supported' };
857
+ }
858
+ const v = validateCanvasName(input.name);
859
+ if (!v.ok || !v.name || !v.componentName) {
860
+ return { ok: false, status: 400, error: v.error ?? 'invalid name' };
861
+ }
862
+
863
+ // Group must be one of the configured canvas groups (+ the new-canvas dir).
864
+ const allowed = new Set<string>(cfg.canvasGroups.map((g) => g.path));
865
+ allowed.add(cfg.newCanvasDir || 'ui');
866
+ const group =
867
+ input.group == null || input.group === '' ? cfg.newCanvasDir || 'ui' : String(input.group);
868
+ if (!allowed.has(group)) {
869
+ return { ok: false, status: 400, error: `group must be one of: ${[...allowed].join(', ')}` };
870
+ }
871
+
872
+ const groupAbs = path.join(paths.designRoot, group);
873
+ const fileAbs = path.join(groupAbs, `${v.name}.tsx`);
874
+ // Containment backstop, two layers (Phase 22 security review F2). The name
875
+ // regex already forbids traversal, but `group` comes from config
876
+ // (canvasGroups[].path / newCanvasDir), which `normalizeConfig` does NOT
877
+ // validate for `..`. So assert (1) the GROUP resolves inside designRoot —
878
+ // a config-supplied `../../etc` must not relocate the write out of the
879
+ // project — and (2) the file resolves inside that group. Without (1) a
880
+ // poisoned config would be an arbitrary-directory `.tsx` write (latent today
881
+ // because config isn't sync-scoped; closed now so a future config-sync can't
882
+ // re-open it).
883
+ const resolvedDesignRoot = path.resolve(paths.designRoot);
884
+ const resolvedGroup = path.resolve(groupAbs);
885
+ if (
886
+ resolvedGroup !== resolvedDesignRoot &&
887
+ !resolvedGroup.startsWith(`${resolvedDesignRoot}${path.sep}`)
888
+ ) {
889
+ return { ok: false, status: 400, error: 'canvas group resolves outside the design root' };
890
+ }
891
+ if (
892
+ path.resolve(fileAbs) !== path.join(resolvedGroup, `${v.name}.tsx`) ||
893
+ !path.resolve(fileAbs).startsWith(`${resolvedGroup}${path.sep}`)
894
+ ) {
895
+ return { ok: false, status: 400, error: 'resolved path escapes the canvas group' };
896
+ }
897
+ if (await Bun.file(fileAbs).exists()) {
898
+ return {
899
+ ok: false,
900
+ status: 409,
901
+ error: `a canvas named "${v.name}" already exists in ${group}`,
902
+ };
903
+ }
904
+
905
+ const rel = path.posix.join(group, `${v.name}.tsx`);
906
+ const slug = fileSlug(rel);
907
+ const dsName = cfg.defaultDesignSystem || 'project';
908
+ const platform = 'desktop';
909
+ const tsx = renderBriefBoard({
910
+ name: v.name,
911
+ componentName: v.componentName,
912
+ dsName,
913
+ platform,
914
+ seedHint: 'Empty brief board — annotate me',
915
+ historyDir: path.posix.join(paths.designRel, '_history', slug),
916
+ });
917
+ const now = new Date().toISOString();
918
+ const meta = {
919
+ kind: 'brief-board',
920
+ title: v.name,
921
+ subtitle: 'brief board',
922
+ brief: v.name,
923
+ designSystem: dsName,
924
+ platform,
925
+ created: now,
926
+ last_modified: now,
927
+ };
928
+ await Bun.write(fileAbs, tsx);
929
+ await Bun.write(
930
+ path.join(groupAbs, `${v.name}.meta.json`),
931
+ `${JSON.stringify(meta, null, 2)}\n`
932
+ );
933
+ // designRel-prefixed path — matches the file-tree `file.path` shape so the
934
+ // client can open it directly after reloadTree().
935
+ return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
936
+ }
937
+
938
+ // Phase 22 — SOFT-delete a canvas (DELETE /_api/canvas). Same trust boundary as
939
+ // createCanvas: main-origin-only, never the untrusted canvas iframe origin
940
+ // (DDR-054). Destructive, so it MOVES the whole sidecar set to
941
+ // `<designRoot>/_trash/<stamp>__<slug>/` (recoverable locally) instead of a hard
942
+ // rm — pairs with the existing `_history/` + /design:rollback model. Hub
943
+ // propagation of the delete is deliberately out of scope (Phase 26 consent).
944
+ async function deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult> {
945
+ const raw = input?.file;
946
+ if (typeof raw !== 'string' || !raw.trim()) {
947
+ return { ok: false, status: 400, error: 'file is required' };
948
+ }
949
+ let rel = raw.trim();
950
+ try {
951
+ rel = decodeURIComponent(rel);
952
+ } catch {
953
+ /* leave as-is */
954
+ }
955
+ rel = rel.replace(/^\/+/, '');
956
+ const prefix = `${paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
957
+ if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
958
+
959
+ // Only a real `.tsx` canvas, no traversal.
960
+ if (rel.includes('..')) return { ok: false, status: 400, error: 'invalid path' };
961
+ if (!/\.tsx$/i.test(rel)) {
962
+ return { ok: false, status: 400, error: 'only .tsx canvases can be deleted' };
963
+ }
964
+
965
+ const fileAbs = path.join(paths.designRoot, rel);
966
+ const resolvedDesignRoot = path.resolve(paths.designRoot);
967
+ const resolvedFile = path.resolve(fileAbs);
968
+ if (
969
+ resolvedFile !== resolvedDesignRoot &&
970
+ !resolvedFile.startsWith(`${resolvedDesignRoot}${path.sep}`)
971
+ ) {
972
+ return { ok: false, status: 400, error: 'path escapes the design root' };
973
+ }
974
+
975
+ // Must live under a NON-DS canvas group — never the design system (`system`),
976
+ // never a loose root file (config.json, README). Deleting DS sources is not a
977
+ // canvas operation.
978
+ const deletable = cfg.canvasGroups.filter(
979
+ (g) => g.label !== 'Design system' && !/^system(\/|$)/.test(g.path)
980
+ );
981
+ const inGroup = deletable.some((g) => {
982
+ const gAbs = path.resolve(path.join(paths.designRoot, g.path));
983
+ return resolvedFile.startsWith(`${gAbs}${path.sep}`);
984
+ });
985
+ if (!inGroup) {
986
+ return {
987
+ ok: false,
988
+ status: 400,
989
+ error: 'only canvases under a managed canvas group can be deleted',
990
+ };
991
+ }
992
+
993
+ if (!(await Bun.file(fileAbs).exists())) {
994
+ return { ok: false, status: 404, error: 'canvas not found' };
995
+ }
996
+
997
+ const slug = fileSlug(rel);
998
+ const base = path.basename(rel).replace(/\.tsx$/i, '');
999
+ const groupDir = path.dirname(fileAbs);
1000
+ // Filesystem-safe wall-clock stamp (no `:`) so repeated deletes of the same
1001
+ // canvas don't collide in _trash.
1002
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1003
+ const trashDir = path.join(paths.designRoot, '_trash', `${stamp}__${slug}`);
1004
+ await mkdir(trashDir, { recursive: true });
1005
+
1006
+ const trashed: string[] = [];
1007
+ const moveIfExists = async (src: string, destName: string) => {
1008
+ try {
1009
+ await statp(src); // throws if absent (works for files AND dirs)
1010
+ } catch {
1011
+ return;
1012
+ }
1013
+ try {
1014
+ await rename(src, path.join(trashDir, destName));
1015
+ trashed.push(path.relative(paths.designRoot, src));
1016
+ } catch {
1017
+ /* best-effort — a sidecar that can't move shouldn't abort the delete */
1018
+ }
1019
+ };
1020
+
1021
+ // Primary + the full sidecar set (annotations, meta, history, canvas-state,
1022
+ // comments). Flattened names so the trash dir is a self-contained bundle.
1023
+ await moveIfExists(fileAbs, `${base}.tsx`);
1024
+ await moveIfExists(path.join(groupDir, `${base}.meta.json`), `${base}.meta.json`);
1025
+ await moveIfExists(
1026
+ path.join(paths.designRoot, `${slug}.annotations.svg`),
1027
+ `${slug}.annotations.svg`
1028
+ );
1029
+ await moveIfExists(path.join(paths.designRoot, '_history', slug), `_history__${slug}`);
1030
+ await moveIfExists(
1031
+ path.join(paths.canvasStateDir, `${slug}.json`),
1032
+ `_canvas-state__${slug}.json`
1033
+ );
1034
+ await moveIfExists(path.join(paths.commentsDir, `${slug}.json`), `_comments__${slug}.json`);
1035
+
1036
+ await Bun.write(
1037
+ path.join(trashDir, '_trash-manifest.json'),
1038
+ `${JSON.stringify({ canvas: rel, slug, deletedAt: new Date().toISOString(), trashed }, null, 2)}\n`
1039
+ );
1040
+
1041
+ return {
1042
+ ok: true,
1043
+ rel,
1044
+ slug,
1045
+ trashed,
1046
+ trashDir: path.relative(paths.repoRoot, trashDir),
1047
+ };
1048
+ }
1049
+
1050
+ // Phase 12 (DDR-103) — resolve a v2 canvas slug (`selected.canvas`: POSIX,
1051
+ // extension-less, designRoot-relative — matches `_locator.json` keys + the
1052
+ // `canvasSlug()` shape) to its absolute `.tsx` path, with a containment
1053
+ // backstop. Same main-origin-only trust boundary as createCanvas: the
1054
+ // untrusted canvas iframe origin never reaches a source-write endpoint.
1055
+ function resolveCanvasAbs(
1056
+ slugRaw: unknown
1057
+ ): { ok: true; abs: string } | { ok: false; status: number; error: string } {
1058
+ if (typeof slugRaw !== 'string' || !slugRaw.trim()) {
1059
+ return { ok: false, status: 400, error: 'canvas (slug) required' };
1060
+ }
1061
+ let slug = slugRaw.replace(/^\/+|\/+$/g, '').replace(/\.(tsx|html)$/i, '');
1062
+ // Accept either the bare slug (`ui/Foo`) or the designRel-prefixed file path
1063
+ // the client `selected.file` carries (`.design/ui/Foo.tsx`) — strip a leading
1064
+ // designRel so both shapes resolve to the same canvas (mirrors deriveCanvasSlug).
1065
+ const dr = paths.designRel.replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
1066
+ if (dr && slug.startsWith(`${dr}/`)) slug = slug.slice(dr.length + 1);
1067
+ if (
1068
+ !slug ||
1069
+ path.isAbsolute(slug) ||
1070
+ slug.split('/').some((seg) => seg === '..' || seg === '.' || seg === '')
1071
+ ) {
1072
+ return { ok: false, status: 400, error: 'invalid canvas slug' };
1073
+ }
1074
+ const abs = `${path.join(paths.designRoot, ...slug.split('/'))}.tsx`;
1075
+ const resolvedRoot = path.resolve(paths.designRoot);
1076
+ const resolved = path.resolve(abs);
1077
+ if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
1078
+ return { ok: false, status: 400, error: 'canvas resolves outside the design root' };
1079
+ }
1080
+ return { ok: true, abs };
1081
+ }
1082
+
1083
+ // 8-hex lowercase, the shape `computeId` (canvas-edit.ts) stamps on data-cd-id.
1084
+ const CD_ID_RE = /^[0-9a-f]{8}$/;
1085
+
1086
+ async function editCss(input: {
1087
+ canvas?: unknown;
1088
+ id?: unknown;
1089
+ property?: unknown;
1090
+ value?: unknown;
1091
+ reset?: unknown;
1092
+ }): Promise<EditOpResult> {
1093
+ const r = resolveCanvasAbs(input.canvas);
1094
+ if (!r.ok) return r;
1095
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
1096
+ if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
1097
+ const property = typeof input.property === 'string' ? input.property.trim() : '';
1098
+ // CSS property names are ASCII letters + hyphens only (optionally a leading
1099
+ // `-` for vendor prefixes) — reject anything that could smuggle a second key
1100
+ // or an expression into the inline style object.
1101
+ if (!property || !/^-?[a-z][a-z-]*$/.test(property)) {
1102
+ return { ok: false, status: 400, error: 'invalid css property' };
1103
+ }
1104
+ // kebab → camelCase JSX style key.
1105
+ const camel = property.replace(/-([a-z])/g, (_m, c: string) => c.toUpperCase());
1106
+ // Phase 12.3 — `reset: true` REMOVES the inline property (back to class /
1107
+ // inherited). No value needed; a missing key is a no-op (delta 0).
1108
+ if (input.reset === true) {
1109
+ try {
1110
+ const res = await removeAttribute(r.abs, id, `style.${camel}`);
1111
+ return { ok: true, delta: res.delta };
1112
+ } catch (err) {
1113
+ return {
1114
+ ok: false,
1115
+ status: 422,
1116
+ error: err instanceof CanvasEditError ? err.message : 'reset failed',
1117
+ };
1118
+ }
1119
+ }
1120
+ const value = typeof input.value === 'string' ? input.value : '';
1121
+ if (!value.trim()) return { ok: false, status: 400, error: 'value required' };
1122
+ if (value.length > 256) return { ok: false, status: 413, error: 'value too long' };
1123
+ // The value is always written as a JS STRING literal: JSON.stringify escapes
1124
+ // quotes/backslashes/newlines so it can never break out of the string, and
1125
+ // React accepts string values for every style prop — so `var(--accent)`,
1126
+ // `#fff`, `8px`, `700`, `1.5` all ride verbatim.
1127
+ try {
1128
+ const res = await editAttribute(r.abs, id, `style.${camel}`, JSON.stringify(value));
1129
+ return { ok: true, delta: res.delta };
1130
+ } catch (err) {
1131
+ return {
1132
+ ok: false,
1133
+ status: 422,
1134
+ error: err instanceof CanvasEditError ? err.message : 'edit failed',
1135
+ };
1136
+ }
1137
+ }
1138
+
1139
+ async function editText(input: {
1140
+ canvas?: unknown;
1141
+ id?: unknown;
1142
+ text?: unknown;
1143
+ }): Promise<EditOpResult> {
1144
+ const r = resolveCanvasAbs(input.canvas);
1145
+ if (!r.ok) return r;
1146
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
1147
+ if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
1148
+ if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
1149
+ if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
1150
+ try {
1151
+ const res = await runEditText(r.abs, id, input.text);
1152
+ return { ok: true, delta: res.delta };
1153
+ } catch (err) {
1154
+ return {
1155
+ ok: false,
1156
+ status: 422,
1157
+ error: err instanceof CanvasEditError ? err.message : 'edit failed',
1158
+ };
1159
+ }
1160
+ }
1161
+
1162
+ async function editAttr(input: {
1163
+ canvas?: unknown;
1164
+ id?: unknown;
1165
+ attr?: unknown;
1166
+ value?: unknown;
1167
+ reset?: unknown;
1168
+ }): Promise<EditOpResult> {
1169
+ const r = resolveCanvasAbs(input.canvas);
1170
+ if (!r.ok) return r;
1171
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
1172
+ if (!CD_ID_RE.test(id)) return { ok: false, status: 400, error: 'invalid data-cd-id' };
1173
+ const attr = typeof input.attr === 'string' ? input.attr.trim() : '';
1174
+ // Plain HTML attributes only — data-*, aria-*, role, title, id, lang, dir…
1175
+ // Reject `style*` (that's /_api/edit-css), `data-cd-id` (pipeline-owned, also
1176
+ // refused downstream by editAttribute), and anything that isn't a bare html
1177
+ // attribute name. Digits allowed mid-name (e.g. data-2x).
1178
+ if (
1179
+ !attr ||
1180
+ !/^[a-z][a-z0-9-]*$/.test(attr) ||
1181
+ attr === 'data-cd-id' ||
1182
+ attr.startsWith('style')
1183
+ ) {
1184
+ return { ok: false, status: 400, error: 'invalid attribute' };
1185
+ }
1186
+ // Phase 12.3 — `reset: true` REMOVES the custom attribute. No-op if absent.
1187
+ if (input.reset === true) {
1188
+ try {
1189
+ const res = await removeAttribute(r.abs, id, attr);
1190
+ return { ok: true, delta: res.delta };
1191
+ } catch (err) {
1192
+ return {
1193
+ ok: false,
1194
+ status: 422,
1195
+ error: err instanceof CanvasEditError ? err.message : 'reset failed',
1196
+ };
1197
+ }
1198
+ }
1199
+ const value = typeof input.value === 'string' ? input.value : '';
1200
+ if (!value.trim()) return { ok: false, status: 400, error: 'value required' };
1201
+ if (value.length > 256) return { ok: false, status: 413, error: 'value too long' };
1202
+ try {
1203
+ // Non-`style.` attr name → editAttribute writes a plain quoted JSX attribute.
1204
+ // Pass the value RAW: editStringAttr quotes/escapes it itself (JSON.stringify
1205
+ // on replace, escapeAttr on insert) — pre-stringifying here double-encoded
1206
+ // the value (`data-x="\"ok\""`; knob-smoke finding, 2026-06-12).
1207
+ const res = await editAttribute(r.abs, id, attr, value);
1208
+ return { ok: true, delta: res.delta };
1209
+ } catch (err) {
1210
+ return {
1211
+ ok: false,
1212
+ status: 422,
1213
+ error: err instanceof CanvasEditError ? err.message : 'edit failed',
1214
+ };
1215
+ }
1216
+ }
1217
+
715
1218
  async function saveCanvasState(file: string, state: Record<string, unknown>) {
716
1219
  if (!state || typeof state !== 'object') return;
717
1220
  const safe: Record<string, unknown> = {};
@@ -732,6 +1235,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
732
1235
  async function buildIndexData() {
733
1236
  const groups = [];
734
1237
 
1238
+ // DDR-093 — per-canvas design-system map (repo-relative canvas path → DS
1239
+ // name) so the client's canvasUrl() injects each UI canvas's OWN tokens
1240
+ // instead of unconditionally designSystems[0]. Populated from the canvas
1241
+ // groups below; returned on the payload and folded into the client cfg.
1242
+ const canvasDesignSystems: Record<string, string> = {};
1243
+ const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
1244
+ // A file under `system/<folder>/` belongs to the DS that owns that folder —
1245
+ // path-authoritative, because specimens/ui_kits rarely carry a sidecar
1246
+ // `designSystem`. Returns null for `ui/` canvases (resolved via sidecar).
1247
+ const ownerDsName = (repoRelPath: string): string | null => {
1248
+ let r = repoRelPath;
1249
+ const prefix = `${paths.designRel}/`;
1250
+ if (r.startsWith(prefix)) r = r.slice(prefix.length);
1251
+ const m = r.match(/^system\/([^/]+)\//);
1252
+ if (!m) return null;
1253
+ const folder = m[1];
1254
+ const owner = (cfg.designSystems ?? []).find(
1255
+ (d) => d.path === `system/${folder}` || d.path.endsWith(`/${folder}`)
1256
+ );
1257
+ return owner?.name ?? null;
1258
+ };
1259
+
735
1260
  // PROJECT — top-level .design/ files (README.md, INDEX.md, config.json, …).
736
1261
  // These are non-HTML so they're listed but not openable in the iframe; the
737
1262
  // sidebar can still display them as context (mirrors CV-08 mock).
@@ -777,6 +1302,20 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
777
1302
  const filePaths = isDs
778
1303
  ? await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.md', '.css', '.json'])
779
1304
  : await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.css', '.json']);
1305
+ // DDR-093 — record each `.tsx` canvas's design system. canvasUrl() only
1306
+ // injects tokens for `.tsx`, so skip everything else. Path-owned DS wins
1307
+ // (system/<ds>/…); otherwise the sidecar's `meta.designSystem`, defaulting
1308
+ // to the project default when the canvas declares none.
1309
+ for (const fp of filePaths) {
1310
+ if (!fp.endsWith('.tsx')) continue;
1311
+ let dsName = ownerDsName(fp);
1312
+ if (!dsName) {
1313
+ const meta = await loadCanvasMeta(fp);
1314
+ const declared = meta?.designSystem;
1315
+ dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
1316
+ }
1317
+ canvasDesignSystems[fp] = dsName;
1318
+ }
780
1319
  // Strip down to `g.path` so per-DS folders (`project`, `beta`, …) show
781
1320
  // up as the top-level dirs inside the DS section. Single-DS configs get
782
1321
  // a wrapper folder too — slightly more verbose, but consistent with
@@ -837,6 +1376,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
837
1376
  projectLabel: ctx.projectLabel,
838
1377
  designRoot: paths.designRel,
839
1378
  groups,
1379
+ canvasDesignSystems,
840
1380
  };
841
1381
  }
842
1382
 
@@ -1078,6 +1618,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1078
1618
  patchCanvasMeta,
1079
1619
  loadAnnotations,
1080
1620
  saveAnnotations,
1621
+ saveAsset,
1622
+ createCanvas,
1623
+ deleteCanvas,
1624
+ editCss,
1625
+ editText,
1626
+ editAttr,
1081
1627
  buildIndexData,
1082
1628
  buildSystemData,
1083
1629
  loadExportHistory,