@1agh/maude 0.28.0 → 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 (361) 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 +286 -6
  4. package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1209 -122
  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/scenario-report.mjs +1 -1
  16. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  17. package/apps/studio/bin/server-up.sh +234 -0
  18. package/apps/studio/bin/to-lottie-verify.html +68 -0
  19. package/apps/studio/bin/to-lottie.sh +128 -0
  20. package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +19 -3
  21. package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
  22. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +2 -2
  24. package/apps/studio/canvas-create.ts +104 -0
  25. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +47 -13
  26. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
  27. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  28. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  29. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +41 -33
  30. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +57 -42
  31. package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
  32. package/apps/studio/client/canvas-url.js +81 -0
  33. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  34. package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
  35. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  36. package/apps/studio/client/styles/3-shell-maude.css +459 -0
  37. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  38. package/apps/studio/client/styles/4-components-maude.css +74 -0
  39. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
  40. package/apps/studio/client/styles/5-maude-overrides.css +123 -0
  41. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  42. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  43. package/apps/studio/client/tour/overlay.jsx +191 -0
  44. package/apps/studio/client/tour/usage-tour.js +35 -0
  45. package/apps/studio/client/whats-new-seen.js +52 -0
  46. package/apps/studio/client/whats-new.jsx +250 -0
  47. package/{plugins/design/dev-server → apps/studio}/collab/index.ts +4 -4
  48. package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +1 -1
  49. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
  50. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  51. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
  52. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
  53. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +2 -2
  54. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
  55. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
  56. package/apps/studio/dist/client.bundle.js +22825 -0
  57. package/apps/studio/dist/comment-mount.js +1960 -0
  58. package/apps/studio/dist/runtime/lib0_decoding.js +1 -0
  59. package/apps/studio/dist/runtime/lib0_encoding.js +1 -0
  60. package/apps/studio/dist/runtime/motion.js +4 -0
  61. package/apps/studio/dist/runtime/motion_react.js +12 -0
  62. package/apps/studio/dist/runtime/pixi-js.js +3397 -0
  63. package/apps/studio/dist/runtime/react-dom.js +1 -0
  64. package/apps/studio/dist/runtime/react-dom_client.js +9 -0
  65. package/apps/studio/dist/runtime/react.js +1 -0
  66. package/apps/studio/dist/runtime/react_jsx-dev-runtime.js +1 -0
  67. package/apps/studio/dist/runtime/react_jsx-runtime.js +1 -0
  68. package/apps/studio/dist/runtime/y-protocols_awareness.js +1 -0
  69. package/apps/studio/dist/runtime/y-protocols_sync.js +1 -0
  70. package/apps/studio/dist/runtime/yjs.js +5 -0
  71. package/apps/studio/dist/styles.css +5695 -0
  72. package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
  73. package/apps/studio/draw/animate.ts +333 -0
  74. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +4 -4
  75. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  76. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  77. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +9 -6
  78. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +2 -2
  79. package/apps/studio/draw/morph.ts +204 -0
  80. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  82. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  83. package/apps/studio/draw/serialize-animate.ts +324 -0
  84. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  85. package/apps/studio/draw/test/animate.test.ts +172 -0
  86. package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +1 -1
  87. package/apps/studio/draw/test/morph.test.ts +107 -0
  88. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  89. package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +1 -1
  90. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  91. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  92. package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +1 -2
  93. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +14 -2
  94. package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +2 -2
  95. package/{plugins/design/dev-server → apps/studio}/http.ts +103 -2
  96. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
  97. package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
  98. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +6 -3
  99. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
  100. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  101. package/{plugins/design/dev-server → apps/studio}/server.ts +19 -1
  102. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +6 -6
  103. package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +2 -2
  104. package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +1 -1
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +1 -2
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +69 -6
  107. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +98 -1
  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/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +1 -1
  111. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  112. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +3 -1
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +1 -1
  114. package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +1 -1
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +1 -1
  116. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +2 -3
  117. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  118. package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +1 -2
  119. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +18 -3
  120. package/apps/studio/test/canvas-url.test.ts +99 -0
  121. package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +1 -1
  122. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +1 -1
  123. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +1 -1
  124. package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +1 -1
  125. package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +1 -2
  126. package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +1 -2
  127. package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +1 -2
  128. package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +1 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +1 -1
  130. package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +1 -2
  131. package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +1 -2
  132. package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +1 -2
  133. package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +1 -1
  134. package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +1 -1
  135. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  136. package/apps/studio/test/motion-sample.test.ts +35 -0
  137. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  138. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  139. package/apps/studio/test/read-annotations.test.ts +409 -0
  140. package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +1 -1
  141. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  142. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +2 -2
  143. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +2 -3
  144. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +1 -1
  145. package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +1 -1
  146. package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +2 -3
  147. package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +2 -3
  148. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +1 -1
  149. package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +1 -1
  150. package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +1 -2
  151. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +4 -5
  152. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +1 -2
  153. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +3 -8
  154. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +1 -2
  156. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  157. package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +4 -1
  158. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
  159. package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
  161. package/apps/studio/test/whats-new.test.ts +101 -0
  162. package/apps/studio/text-imports.d.ts +9 -0
  163. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
  164. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
  165. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  166. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +2 -2
  167. package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +41 -12
  168. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +3 -7
  169. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +2 -2
  170. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +2 -2
  171. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +2 -2
  172. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  173. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +4 -5
  174. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  175. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  176. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +10 -2
  177. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  178. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +4 -2
  179. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +5 -6
  180. package/apps/studio/whats-new.json +139 -0
  181. package/apps/studio/whats-new.schema.json +70 -0
  182. package/apps/studio/whats-new.ts +87 -0
  183. package/cli/bin/maude.mjs +10 -0
  184. package/cli/commands/design.mjs +84 -10
  185. package/cli/commands/design.test.mjs +133 -1
  186. package/cli/commands/help.mjs +8 -2
  187. package/cli/commands/hub.mjs +3 -3
  188. package/cli/commands/hub.test.mjs +1 -1
  189. package/cli/commands/scenario-report.mjs +2 -9
  190. package/cli/lib/cache.mjs +1 -1
  191. package/cli/lib/config-lint.mjs +1 -1
  192. package/cli/lib/copy-tree.mjs +1 -1
  193. package/cli/lib/design-link.mjs +1 -1
  194. package/cli/lib/flow-design-integration.test.mjs +2 -2
  195. package/cli/lib/gitignore-block.mjs +1 -0
  196. package/cli/lib/gitignore-block.test.mjs +2 -2
  197. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  198. package/cli/lib/preflight.mjs +1 -1
  199. package/cli/lib/stack-detect.mjs +1 -1
  200. package/package.json +16 -16
  201. package/plugins/design/templates/_shell.html +3 -3
  202. package/plugins/design/templates/brief-board.tsx.template +61 -0
  203. package/plugins/design/templates/canvas.tsx.template +1 -1
  204. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +18 -3
  205. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  206. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  207. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  208. package/plugins/design/dev-server/bin/server-up.sh +0 -135
  209. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  210. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  211. package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +0 -620
  212. package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +0 -604
  213. package/plugins/design/dev-server/dist/runtime/motion.js +0 -4944
  214. package/plugins/design/dev-server/dist/runtime/motion_react.js +0 -10056
  215. package/plugins/design/dev-server/dist/runtime/pixi-js.js +0 -53104
  216. package/plugins/design/dev-server/dist/runtime/react-dom.js +0 -231
  217. package/plugins/design/dev-server/dist/runtime/react-dom_client.js +0 -10357
  218. package/plugins/design/dev-server/dist/runtime/react.js +0 -535
  219. package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +0 -60
  220. package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +0 -85
  221. package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +0 -376
  222. package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +0 -100
  223. package/plugins/design/dev-server/dist/runtime/yjs.js +0 -6687
  224. package/plugins/design/dev-server/dist/styles.css +0 -2710
  225. package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  226. package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  227. package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  228. package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  229. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
  230. package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  231. package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  232. package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  233. package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  234. package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  235. package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  236. package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  237. package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  238. package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  239. package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  240. package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  241. package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  242. package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  243. package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  244. package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  245. package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  246. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
  247. package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  248. package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  249. package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  250. package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  251. package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  252. package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  253. package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  254. package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  255. package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  256. package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  257. package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +1 -1
  258. package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  259. package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  260. package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  261. package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  262. package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  263. package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  264. package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  265. package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  266. package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  267. package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +1 -1
  268. package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  269. package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  270. package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  271. package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +1 -1
  272. package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  273. package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  274. package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  275. package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  276. package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +1 -1
  277. package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +1 -1
  278. package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +1 -1
  279. package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +1 -1
  280. package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  281. package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +1 -1
  282. package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  283. package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  284. package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  285. package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
  286. package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  287. package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  288. package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  289. package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  290. package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  291. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +1 -1
  292. package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  293. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
  294. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
  295. package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  296. package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  297. package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  298. package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +1 -1
  299. package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  300. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +3 -3
  301. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
  302. package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  303. package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  304. package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  305. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
  306. package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  307. package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +2 -2
  308. package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  309. package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  310. package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  311. package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  312. package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +1 -1
  313. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  314. package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  315. package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  316. package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  317. package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  318. package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +2 -2
  319. package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  320. package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  321. package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  322. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
  323. package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  324. package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  325. package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  326. package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  327. package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  328. package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  329. package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  330. package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  331. package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  332. package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  333. package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  334. package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +1 -1
  335. package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  336. package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  337. package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +2 -2
  338. package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +1 -1
  339. package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  340. package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  341. package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  342. package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  343. package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  344. package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  345. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  346. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +1 -1
  347. package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +1 -1
  348. package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  349. package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  350. package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +2 -2
  351. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  352. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +3 -3
  353. package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +2 -2
  354. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  355. package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  356. package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  357. package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +1 -1
  358. package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +2 -2
  359. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,409 @@
1
+ // read-annotations.test.ts — Phase 22. The CONTRACT test that keeps the headless
2
+ // reader (`bin/read-annotations.mjs`) in sync with the browser serializer.
3
+ //
4
+ // Strategy: build strokes → run them through the CANONICAL `strokesToSvg`
5
+ // (annotations-layer.tsx) → write to a temp <designRoot>/<slug>.annotations.svg →
6
+ // invoke the reader as a child process exactly as `maude design read-annotations`
7
+ // does (node + the .mjs) → assert the extracted `{ tool, text, x, y, color }`
8
+ // matches the inputs. If Phase 21/23/24 ever change the SVG shape, this fails
9
+ // loud — the reader is a SEPARATE implementation of the same vocabulary, so the
10
+ // serializer is the single source of truth it must track.
11
+
12
+ import { describe, expect, test } from 'bun:test';
13
+ import { spawnSync } from 'node:child_process';
14
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ import {
19
+ type ArrowStroke,
20
+ type EllipseStroke,
21
+ type PenStroke,
22
+ type PolygonStroke,
23
+ type RectStroke,
24
+ type StickyStroke,
25
+ type Stroke,
26
+ strokesToSvg,
27
+ type TextStroke,
28
+ } from '../annotations-layer.tsx';
29
+
30
+ const READER = fileURLToPath(new URL('../bin/read-annotations.mjs', import.meta.url));
31
+
32
+ interface Annotation {
33
+ tool: string;
34
+ id: string;
35
+ x: number | null;
36
+ y: number | null;
37
+ w: number | null;
38
+ h: number | null;
39
+ text: string | null;
40
+ color: string | null;
41
+ anchorId?: string;
42
+ artboard?: string | null;
43
+ }
44
+
45
+ // Mirror api.ts fileSlug for the common (design-root-relative .tsx) case so the
46
+ // fixture lands where the reader will look.
47
+ function slugFor(relPath: string): string {
48
+ return relPath
49
+ .replace(/\//g, '-')
50
+ .replace(/\s+/g, '_')
51
+ .replace(/\.(tsx|html)$/i, '')
52
+ .replace(/^\.+/, '')
53
+ .toLowerCase();
54
+ }
55
+
56
+ /**
57
+ * Materialize a temp repo with `<root>/.design/<slug>.annotations.svg`, run the
58
+ * reader against it, and return the parsed JSON. `extraFiles` lets a test drop a
59
+ * canvas-state JSON alongside.
60
+ */
61
+ function read(
62
+ relPath: string,
63
+ svg: string,
64
+ extraArgs: string[] = [],
65
+ extraFiles: Record<string, string> = {}
66
+ ): { annotations: Annotation[]; exitCode: number; stderr: string } {
67
+ const root = mkdtempSync(`${tmpdir()}/maude-read-annot-`);
68
+ try {
69
+ mkdirSync(`${root}/.design`, { recursive: true });
70
+ writeFileSync(`${root}/.design/${slugFor(relPath)}.annotations.svg`, svg);
71
+ for (const [name, contents] of Object.entries(extraFiles)) {
72
+ writeFileSync(`${root}/${name}`, contents);
73
+ }
74
+ const res = spawnSync('node', [READER, relPath, '--root', root, ...extraArgs], {
75
+ encoding: 'utf8',
76
+ cwd: root, // a relative --canvas-state resolves against cwd, as in real use
77
+ });
78
+ const stdout = (res.stdout || '').trim();
79
+ return {
80
+ annotations: stdout ? (JSON.parse(stdout) as Annotation[]) : [],
81
+ exitCode: res.status ?? 1,
82
+ stderr: res.stderr || '',
83
+ };
84
+ } finally {
85
+ rmSync(root, { recursive: true, force: true });
86
+ }
87
+ }
88
+
89
+ const REL = 'ui/Board.tsx';
90
+
91
+ describe('read-annotations / standalone + anchored text', () => {
92
+ test('standalone text yields its world (x, y), color, and verbatim string', () => {
93
+ const t: TextStroke = {
94
+ id: 't-std',
95
+ tool: 'text',
96
+ color: '#1a1a1a',
97
+ fontSize: 20,
98
+ text: 'hero needs a bigger CTA',
99
+ x: 120,
100
+ y: 80,
101
+ };
102
+ const { annotations, exitCode } = read(REL, strokesToSvg([t]));
103
+ expect(exitCode).toBe(0);
104
+ expect(annotations).toHaveLength(1);
105
+ const [a] = annotations;
106
+ expect(a?.tool).toBe('text');
107
+ expect(a?.id).toBe('t-std');
108
+ expect(a?.text).toBe('hero needs a bigger CTA');
109
+ expect(a?.x).toBe(120);
110
+ expect(a?.y).toBe(80);
111
+ expect(a?.color).toBe('#1a1a1a');
112
+ expect(a?.anchorId).toBeUndefined();
113
+ });
114
+
115
+ test('anchored text carries its anchor id and null world coords', () => {
116
+ const t: TextStroke = {
117
+ id: 't-anc',
118
+ tool: 'text',
119
+ color: '#1a1a1a',
120
+ fontSize: 14,
121
+ text: 'label',
122
+ anchorId: 'r-host',
123
+ };
124
+ const { annotations } = read(REL, strokesToSvg([t]));
125
+ const [a] = annotations;
126
+ expect(a?.anchorId).toBe('r-host');
127
+ expect(a?.x).toBeNull();
128
+ expect(a?.y).toBeNull();
129
+ expect(a?.text).toBe('label');
130
+ });
131
+ });
132
+
133
+ describe('read-annotations / sticky', () => {
134
+ const sticky: StickyStroke = {
135
+ id: 'st1',
136
+ tool: 'sticky',
137
+ color: '#fce8a6',
138
+ x: 40,
139
+ y: 50,
140
+ w: 200,
141
+ h: 160,
142
+ text: 'Step 1: email + password',
143
+ fontSize: 14,
144
+ cornerRadius: 8,
145
+ };
146
+
147
+ test('sticky yields geometry, paper tint, and body text', () => {
148
+ const { annotations } = read(REL, strokesToSvg([sticky]));
149
+ const [a] = annotations;
150
+ expect(a?.tool).toBe('sticky');
151
+ expect(a?.x).toBe(40);
152
+ expect(a?.y).toBe(50);
153
+ expect(a?.w).toBe(200);
154
+ expect(a?.h).toBe(160);
155
+ expect(a?.text).toBe('Step 1: email + password');
156
+ expect(a?.color).toBe('#fce8a6'); // group fill = paper tint, NOT the #1a1a1a body ink
157
+ });
158
+
159
+ test('multi-line sticky text round-trips the newline', () => {
160
+ const multi: StickyStroke = { ...sticky, text: 'line one\nline two' };
161
+ const { annotations } = read(REL, strokesToSvg([multi]));
162
+ expect(annotations[0]?.text).toBe('line one\nline two');
163
+ });
164
+ });
165
+
166
+ describe('read-annotations / position-only strokes carry text:null + a bbox', () => {
167
+ test('pen → text:null with a bounding box from its path', () => {
168
+ const pen: PenStroke = {
169
+ id: 'p1',
170
+ tool: 'pen',
171
+ color: '#e5484d',
172
+ width: 3,
173
+ points: [
174
+ [10, 20],
175
+ [60, 80],
176
+ [30, 50],
177
+ ],
178
+ };
179
+ const [a] = read(REL, strokesToSvg([pen])).annotations;
180
+ expect(a?.tool).toBe('pen');
181
+ expect(a?.text).toBeNull();
182
+ expect(a?.x).toBe(10);
183
+ expect(a?.y).toBe(20);
184
+ expect(a?.w).toBe(50); // 60 - 10
185
+ expect(a?.h).toBe(60); // 80 - 20
186
+ expect(a?.color).toBe('#e5484d');
187
+ });
188
+
189
+ test('rect → text:null with its declared geometry', () => {
190
+ const rect: RectStroke = {
191
+ id: 'r1',
192
+ tool: 'rect',
193
+ color: '#3b82f6',
194
+ width: 2,
195
+ x: 5,
196
+ y: 6,
197
+ w: 100,
198
+ h: 40,
199
+ fill: null,
200
+ };
201
+ const [a] = read(REL, strokesToSvg([rect])).annotations;
202
+ expect(a?.tool).toBe('rect');
203
+ expect(a?.text).toBeNull();
204
+ expect(a?.x).toBe(5);
205
+ expect(a?.y).toBe(6);
206
+ expect(a?.w).toBe(100);
207
+ expect(a?.h).toBe(40);
208
+ });
209
+
210
+ test('arrow → text:null with a bbox recovered from its endpoints', () => {
211
+ const arrow: ArrowStroke = {
212
+ id: 'a1',
213
+ tool: 'arrow',
214
+ color: '#30a46c',
215
+ width: 2,
216
+ x1: 200,
217
+ y1: 100,
218
+ x2: 120,
219
+ y2: 160,
220
+ };
221
+ const [a] = read(REL, strokesToSvg([arrow])).annotations;
222
+ expect(a?.tool).toBe('arrow');
223
+ expect(a?.text).toBeNull();
224
+ expect(a?.x).toBe(120); // min(200,120)
225
+ expect(a?.y).toBe(100); // min(100,160)
226
+ expect(a?.w).toBe(80); // |200-120|
227
+ expect(a?.h).toBe(60); // |160-100|
228
+ });
229
+
230
+ test('ellipse → bbox from centre + radii', () => {
231
+ const ell: EllipseStroke = {
232
+ id: 'e1',
233
+ tool: 'ellipse',
234
+ color: '#8b5cf6',
235
+ width: 2,
236
+ cx: 100,
237
+ cy: 100,
238
+ rx: 30,
239
+ ry: 20,
240
+ fill: null,
241
+ };
242
+ const [a] = read(REL, strokesToSvg([ell])).annotations;
243
+ expect(a?.tool).toBe('ellipse');
244
+ expect(a?.x).toBe(70);
245
+ expect(a?.y).toBe(80);
246
+ expect(a?.w).toBe(60);
247
+ expect(a?.h).toBe(40);
248
+ expect(a?.text).toBeNull();
249
+ });
250
+
251
+ test('polygon → bbox from its points', () => {
252
+ const poly: PolygonStroke = {
253
+ id: 'pg1',
254
+ tool: 'polygon',
255
+ shape: 'diamond',
256
+ color: '#e5484d',
257
+ width: 3,
258
+ x: 10,
259
+ y: 20,
260
+ w: 80,
261
+ h: 60,
262
+ };
263
+ const [a] = read(REL, strokesToSvg([poly])).annotations;
264
+ expect(a?.tool).toBe('polygon');
265
+ expect(a?.text).toBeNull();
266
+ expect(a?.x).toBeCloseTo(10, 4);
267
+ expect(a?.y).toBeCloseTo(20, 4);
268
+ expect(a?.w).toBeCloseTo(80, 4);
269
+ expect(a?.h).toBeCloseTo(60, 4);
270
+ });
271
+ });
272
+
273
+ describe('read-annotations / empty + missing', () => {
274
+ test('an empty annotation SVG yields []', () => {
275
+ const { annotations, exitCode } = read(REL, strokesToSvg([]));
276
+ expect(exitCode).toBe(0);
277
+ expect(annotations).toEqual([]);
278
+ });
279
+
280
+ test('a missing annotation file yields [] (a board with no notes is valid)', () => {
281
+ // Point at a canvas whose slug has NO file on disk.
282
+ const root = mkdtempSync(`${tmpdir()}/maude-read-annot-`);
283
+ try {
284
+ mkdirSync(`${root}/.design`, { recursive: true });
285
+ const res = spawnSync('node', [READER, 'ui/Nope.tsx', '--root', root], {
286
+ encoding: 'utf8',
287
+ });
288
+ expect(res.status).toBe(0);
289
+ expect(JSON.parse((res.stdout || '').trim())).toEqual([]);
290
+ } finally {
291
+ rmSync(root, { recursive: true, force: true });
292
+ }
293
+ });
294
+ });
295
+
296
+ describe('read-annotations / entity decoding', () => {
297
+ test('an entity-laden string comes back exactly as typed', () => {
298
+ const t: TextStroke = {
299
+ id: 't-ent',
300
+ tool: 'text',
301
+ color: '#1a1a1a',
302
+ fontSize: 16,
303
+ text: 'A & B < C "quoted"',
304
+ x: 0,
305
+ y: 0,
306
+ };
307
+ const svg = strokesToSvg([t]);
308
+ // Sanity: the serializer DID escape the special characters on disk.
309
+ expect(svg).toContain('&amp;');
310
+ expect(svg).toContain('&lt;');
311
+ expect(svg).toContain('&quot;');
312
+ const [a] = read(REL, svg).annotations;
313
+ expect(a?.text).toBe('A & B < C "quoted"');
314
+ });
315
+ });
316
+
317
+ describe('read-annotations / document order + mixed scene', () => {
318
+ test('a mixed scene preserves document order and tags each tool', () => {
319
+ const scene: Stroke[] = [
320
+ {
321
+ id: 's-sticky',
322
+ tool: 'sticky',
323
+ color: '#bfe3c0',
324
+ x: 0,
325
+ y: 0,
326
+ w: 200,
327
+ h: 200,
328
+ text: 'first',
329
+ fontSize: 14,
330
+ },
331
+ { id: 's-text', tool: 'text', color: '#000', fontSize: 14, text: 'second', x: 300, y: 10 },
332
+ {
333
+ id: 's-rect',
334
+ tool: 'rect',
335
+ color: '#222',
336
+ width: 2,
337
+ x: 0,
338
+ y: 300,
339
+ w: 50,
340
+ h: 50,
341
+ fill: null,
342
+ },
343
+ { id: 's-arrow', tool: 'arrow', color: '#111', width: 2, x1: 0, y1: 0, x2: 40, y2: 40 },
344
+ ];
345
+ const { annotations } = read(REL, strokesToSvg(scene));
346
+ expect(annotations.map((a) => a.id)).toEqual(['s-sticky', 's-text', 's-rect', 's-arrow']);
347
+ expect(annotations.map((a) => a.tool)).toEqual(['sticky', 'text', 'rect', 'arrow']);
348
+ // Only the word-carrying strokes have text.
349
+ expect(annotations.map((a) => a.text)).toEqual(['first', 'second', null, null]);
350
+ });
351
+ });
352
+
353
+ describe('read-annotations / --canvas-state overlap tagging', () => {
354
+ test('each annotation is tagged with the artboard it overlaps', () => {
355
+ const scene: Stroke[] = [
356
+ // sits inside artboard "hero" (0,0,400,300)
357
+ { id: 'in-hero', tool: 'text', color: '#000', fontSize: 14, text: 'on hero', x: 50, y: 60 },
358
+ // sits inside artboard "list" (500,0,400,300)
359
+ {
360
+ id: 'in-list',
361
+ tool: 'sticky',
362
+ color: '#fce8a6',
363
+ x: 520,
364
+ y: 40,
365
+ w: 100,
366
+ h: 80,
367
+ text: 'on list',
368
+ fontSize: 14,
369
+ },
370
+ // sits in empty space — no artboard
371
+ {
372
+ id: 'floating',
373
+ tool: 'text',
374
+ color: '#000',
375
+ fontSize: 14,
376
+ text: 'nowhere',
377
+ x: 2000,
378
+ y: 2000,
379
+ },
380
+ ];
381
+ const canvasState = JSON.stringify({
382
+ artboards: [
383
+ { id: 'hero', x: 0, y: 0, w: 400, h: 300 },
384
+ { id: 'list', x: 500, y: 0, w: 400, h: 300 },
385
+ ],
386
+ });
387
+ const { annotations } = read(REL, strokesToSvg(scene), ['--canvas-state', 'state.json'], {
388
+ 'state.json': canvasState,
389
+ });
390
+ const byId = Object.fromEntries(annotations.map((a) => [a.id, a.artboard]));
391
+ expect(byId['in-hero']).toBe('hero');
392
+ expect(byId['in-list']).toBe('list');
393
+ expect(byId.floating).toBeNull();
394
+ });
395
+
396
+ test('without --canvas-state no artboard field is emitted', () => {
397
+ const t: TextStroke = {
398
+ id: 't',
399
+ tool: 'text',
400
+ color: '#000',
401
+ fontSize: 14,
402
+ text: 'x',
403
+ x: 0,
404
+ y: 0,
405
+ };
406
+ const [a] = read(REL, strokesToSvg([t])).annotations;
407
+ expect(a && 'artboard' in a).toBe(false);
408
+ });
409
+ });
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { describe, expect, test } from 'bun:test';
10
10
 
11
- import { RUNTIME_PACKAGES, getRuntimeBundle, packageForSlug, slugFor } from '../runtime-bundle.ts';
11
+ import { getRuntimeBundle, packageForSlug, RUNTIME_PACKAGES, slugFor } from '../runtime-bundle.ts';
12
12
 
13
13
  describe('runtime-bundle', () => {
14
14
  test('builds all four sub-bundles successfully', async () => {
@@ -48,15 +48,20 @@ describe('sanitizeAnnotationSvg — A3', () => {
48
48
  expect(out).toContain('width="9"');
49
49
  });
50
50
 
51
- test('strips <image>/<use> and javascript: URLs', () => {
51
+ // Phase 23 — `<image>` is now an allowlisted element, but ONLY a relative
52
+ // assets/<sha8>.<ext> href may survive on it. A javascript:/external href is
53
+ // stripped (the inert href-less <image> may remain — it fetches nothing);
54
+ // <use> stays fully dropped.
55
+ test('strips javascript:/external href on <image>; drops <use>', () => {
52
56
  const dirty =
53
57
  '<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1">' +
54
58
  '<image href="javascript:alert(1)"/><use href="#x"/>' +
55
59
  '</svg>';
56
60
  const out = sanitizeAnnotationSvg(dirty);
57
- expect(out.toLowerCase()).not.toContain('<image');
58
61
  expect(out.toLowerCase()).not.toContain('<use');
59
62
  expect(out.toLowerCase()).not.toContain('javascript:');
63
+ // The <image> may survive as an inert element, but with NO href at all.
64
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
60
65
  });
61
66
 
62
67
  // Bypasses the F1 confirming pass found against the prior denylist — the
@@ -130,3 +135,104 @@ describe('sanitizeAnnotationSvg — A3', () => {
130
135
  expect(sanitizeAnnotationSvg(phase24)).toBe(phase24);
131
136
  });
132
137
  });
138
+
139
+ // ── Phase 23 — <image> assets-href allow/deny matrix (Task 4, pairs with DDR) ──
140
+ describe('sanitizeAnnotationSvg — Phase 23 <image> href allowlist', () => {
141
+ const wrap = (inner: string) =>
142
+ `<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1">${inner}</svg>`;
143
+
144
+ test('KEEPS a valid assets/<sha8>.<ext> href on <image> (all four exts)', () => {
145
+ for (const name of [
146
+ 'a1b2c3d4.png',
147
+ 'deadbeef.jpg',
148
+ 'cafe1234.jpeg',
149
+ 'f00dface.webp',
150
+ '0badf00d.gif',
151
+ ]) {
152
+ const svg = wrap(
153
+ `<image data-id="im" data-tool="image" x="0" y="0" width="40" height="40" href="assets/${name}" preserveAspectRatio="xMidYMid meet"/>`
154
+ );
155
+ expect(sanitizeAnnotationSvg(svg)).toBe(svg);
156
+ }
157
+ });
158
+
159
+ test('STRIPS an external https href (no off-origin fetch)', () => {
160
+ const out = sanitizeAnnotationSvg(
161
+ wrap(
162
+ '<image data-tool="image" x="0" y="0" width="9" height="9" href="https://evil.example/x.png"/>'
163
+ )
164
+ );
165
+ expect(out).not.toContain('https://evil.example');
166
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
167
+ });
168
+
169
+ test('STRIPS a data: URL href (no base64 smuggle)', () => {
170
+ const out = sanitizeAnnotationSvg(
171
+ wrap('<image data-tool="image" href="data:image/svg+xml;base64,PHN2Zz4="/>')
172
+ );
173
+ expect(out.toLowerCase()).not.toContain('data:image');
174
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
175
+ });
176
+
177
+ test('STRIPS a path-traversal href (single-segment guard)', () => {
178
+ for (const bad of [
179
+ 'assets/../../etc/passwd',
180
+ 'assets/../secret.png',
181
+ 'assets/sub/dir/x.png',
182
+ 'assets/x.svg',
183
+ ]) {
184
+ const out = sanitizeAnnotationSvg(wrap(`<image data-tool="image" href="${bad}"/>`));
185
+ expect(out).not.toContain(bad);
186
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
187
+ }
188
+ });
189
+
190
+ test('STRIPS an href on a NON-image element (rect href is never legit)', () => {
191
+ const out = sanitizeAnnotationSvg(
192
+ wrap('<rect data-tool="rect" x="0" y="0" width="9" height="9" href="assets/x.png"/>')
193
+ );
194
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
195
+ expect(out).toContain('<rect');
196
+ });
197
+
198
+ test('STRIPS xlink:href (entity-encoded / namespaced) external on <image>', () => {
199
+ const out = sanitizeAnnotationSvg(
200
+ wrap('<image data-tool="image" xlink:href="javascript:alert(1)"/>')
201
+ );
202
+ expect(out.toLowerCase()).not.toContain('javascript:');
203
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
204
+ });
205
+
206
+ test('CANNOT be tricked by a forged data-mdcc-asset marker carrying a scheme', () => {
207
+ // A hub-pushed SVG that pre-seeds the internal marker with a scheme must NOT
208
+ // round-trip back to an executable href (the post-pass re-validates).
209
+ const out = sanitizeAnnotationSvg(
210
+ wrap('<image data-tool="image" data-mdcc-asset="javascript:alert(1)"/>')
211
+ );
212
+ expect(out.toLowerCase()).not.toContain('javascript:');
213
+ expect(out.toLowerCase()).not.toContain('data-mdcc-asset');
214
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
215
+ });
216
+
217
+ test('an input-supplied data-mdcc-asset marker is neutralized (only a real href hoists)', () => {
218
+ // The marker is an INTERNAL hop minted by the pre-pass from a real href. Any
219
+ // input-supplied marker — even one carrying a valid assets value — is
220
+ // stripped up front, so it can never become a surviving href on its own.
221
+ const out = sanitizeAnnotationSvg(
222
+ wrap('<image data-tool="image" data-mdcc-asset="assets/abcd1234.png"/>')
223
+ );
224
+ expect(out.toLowerCase()).not.toContain('data-mdcc-asset');
225
+ expect(out.toLowerCase()).not.toMatch(/href\s*=/);
226
+ });
227
+
228
+ test('still drops <script>/<svg:script> alongside a valid image (allowlist holds)', () => {
229
+ const out = sanitizeAnnotationSvg(
230
+ wrap(
231
+ '<image data-tool="image" href="assets/ok123456.png"/><svg:script>alert(1)</svg:script><script>x()</script>'
232
+ )
233
+ );
234
+ expect(out).toContain('href="assets/ok123456.png"');
235
+ expect(out.toLowerCase()).not.toContain('script');
236
+ expect(out).not.toContain('alert(1)');
237
+ });
238
+ });
@@ -16,7 +16,7 @@
16
16
  // In-process (no hub process): a relay models Hocuspocus by re-broadcasting each
17
17
  // doc update to every other peer (Yjs updates are commutative/idempotent, so a
18
18
  // re-broadcast relay IS the hub's convergence contract). The real-hub N-peer
19
- // soak lives in plugins/design/hub/test/stress-integration.test.mjs + the live
19
+ // soak lives in apps/hub/test/stress-integration.test.mjs + the live
20
20
  // cross-machine manual run; this suite is the deterministic property gate.
21
21
 
22
22
  import { describe, expect, test } from 'bun:test';
@@ -24,11 +24,11 @@ import * as Y from 'yjs';
24
24
 
25
25
  import { Y_TYPES } from '../collab/persistence.ts';
26
26
  import {
27
- Y_SYNC_TYPES,
28
27
  applyCommentsToDoc,
29
28
  applyCssToDoc,
30
29
  applyHtmlToDoc,
31
30
  applyMetaToDoc,
31
+ Y_SYNC_TYPES,
32
32
  } from '../sync/codec.ts';
33
33
  import { createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
34
34
  import { materialize, materializeCanonical } from '../sync/materialize.ts';
@@ -4,14 +4,13 @@
4
4
  // populated from ONE source. These tests prove migrateSeed never duplicates
5
5
  // comments and is idempotent across re-runs (server restarts).
6
6
 
7
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
7
8
  import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
8
9
  import { tmpdir } from 'node:os';
9
10
  import { join } from 'node:path';
10
-
11
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
12
11
  import * as Y from 'yjs';
13
12
 
14
- import { Y_TYPES, createPersistence } from '../collab/persistence.ts';
13
+ import { createPersistence, Y_TYPES } from '../collab/persistence.ts';
15
14
  import type { Context } from '../context.ts';
16
15
  import { applyCommentsToDoc, applyHtmlToDoc } from '../sync/codec.ts';
17
16
  import { docIsEmpty, migrateSeed } from '../sync/migrate-seed.ts';
@@ -22,7 +22,7 @@ import { describe, expect, test } from 'bun:test';
22
22
  import * as Y from 'yjs';
23
23
 
24
24
  import { Y_TYPES } from '../collab/persistence.ts';
25
- import { MAX_HTML_BYTES, applyHtmlToDoc } from '../sync/codec.ts';
25
+ import { applyHtmlToDoc, MAX_HTML_BYTES } from '../sync/codec.ts';
26
26
  import { createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
27
27
  import { ORIGINS } from '../sync/origins.ts';
28
28
  import { createDocProjection } from '../sync/projection.ts';
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, expect, test } from 'bun:test';
7
7
 
8
- import { type Rect, type SnapOptions, computeSnap } from '../use-snap-guides.tsx';
8
+ import { computeSnap, type Rect, type SnapOptions } from '../use-snap-guides.tsx';
9
9
 
10
10
  const DEFAULTS: SnapOptions = { gridSize: 40, tolerance: 8, disabled: false };
11
11
  const rect = (x: number, y: number, w = 100, h = 80): Rect => ({ x, y, w, h });
@@ -10,15 +10,14 @@
10
10
  // This lets us validate the whole bidi loop, echo prevention, and the
11
11
  // 100-event stress scenario from the plan without booting a real hub.
12
12
 
13
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
13
14
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
15
  import { tmpdir } from 'node:os';
15
16
  import { join } from 'node:path';
16
-
17
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
18
17
  import * as Y from 'yjs';
19
18
 
20
19
  import { type CanvasSyncAgent, createCanvasSyncAgent } from '../sync/agent.ts';
21
- import { Y_SYNC_TYPES, applyHtmlToDoc, htmlFromDoc } from '../sync/codec.ts';
20
+ import { applyHtmlToDoc, htmlFromDoc, Y_SYNC_TYPES } from '../sync/codec.ts';
22
21
  import { createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
23
22
  import { createFsReader } from '../sync/fs-mirror.ts';
24
23
 
@@ -1,11 +1,10 @@
1
1
  // Atomic-write unit tests — Phase 9 Task 4.
2
2
 
3
- import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
4
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
 
7
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
8
-
9
8
  import { atomicWrite } from '../sync/atomic-write.ts';
10
9
 
11
10
  let dir: string;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, expect, test } from 'bun:test';
7
7
 
8
- import { type SyncStatusSnapshot, createConnectionMonitor } from '../sync/connection-state.ts';
8
+ import { createConnectionMonitor, type SyncStatusSnapshot } from '../sync/connection-state.ts';
9
9
 
10
10
  /** A controllable timer queue: setTimer enqueues, advance() fires due timers. */
11
11
  function fakeClock() {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { describe, expect, test } from 'bun:test';
4
4
 
5
- import { ECHO_TTL_MS, createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
5
+ import { createEchoGuard, ECHO_TTL_MS, hashBytes } from '../sync/echo-guard.ts';
6
6
 
7
7
  describe('hashBytes', () => {
8
8
  test('stable hex SHA-256 of bytes', () => {
@@ -1,11 +1,10 @@
1
1
  // fs-mirror unit tests — Phase 9 Task 4.
2
2
 
3
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
4
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
 
7
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
8
-
9
8
  import { hashBytes } from '../sync/echo-guard.ts';
10
9
  import { createFsReader } from '../sync/fs-mirror.ts';
11
10