@1agh/maude 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/README.md +2 -2
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
  3. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +285 -5
  4. package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1206 -119
  5. package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
  6. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  7. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  8. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  9. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  10. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  11. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  12. package/apps/studio/bin/read-annotations.mjs +438 -0
  13. package/apps/studio/bin/read-annotations.sh +11 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  15. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  16. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  17. package/apps/studio/bin/to-lottie-verify.html +68 -0
  18. package/apps/studio/bin/to-lottie.sh +128 -0
  19. package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
  20. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
  21. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  22. package/apps/studio/canvas-create.ts +104 -0
  23. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +46 -12
  24. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
  25. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  26. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  27. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  28. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +56 -41
  29. package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
  30. package/apps/studio/client/canvas-url.js +81 -0
  31. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  32. package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
  33. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  34. package/apps/studio/client/styles/3-shell-maude.css +459 -0
  35. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  36. package/apps/studio/client/styles/4-components-maude.css +74 -0
  37. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
  38. package/apps/studio/client/styles/5-maude-overrides.css +123 -0
  39. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  41. package/apps/studio/client/tour/overlay.jsx +191 -0
  42. package/apps/studio/client/tour/usage-tour.js +35 -0
  43. package/apps/studio/client/whats-new-seen.js +52 -0
  44. package/apps/studio/client/whats-new.jsx +250 -0
  45. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
  46. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  47. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
  48. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
  49. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +1 -1
  50. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
  51. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
  52. package/apps/studio/dist/client.bundle.js +22825 -0
  53. package/apps/studio/dist/comment-mount.js +1960 -0
  54. package/apps/studio/dist/styles.css +5695 -0
  55. package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
  56. package/apps/studio/draw/animate.ts +333 -0
  57. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  58. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  59. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  60. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  61. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  62. package/apps/studio/draw/morph.ts +204 -0
  63. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  64. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  65. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  66. package/apps/studio/draw/serialize-animate.ts +324 -0
  67. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  68. package/apps/studio/draw/test/animate.test.ts +172 -0
  69. package/apps/studio/draw/test/morph.test.ts +107 -0
  70. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  71. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  72. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  73. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +13 -1
  74. package/{plugins/design/dev-server → apps/studio}/http.ts +102 -1
  75. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
  76. package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
  77. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  78. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
  79. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  80. package/{plugins/design/dev-server → apps/studio}/server.ts +18 -0
  81. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +1 -1
  82. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  83. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  84. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  85. package/apps/studio/test/asset-api.test.ts +149 -0
  86. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  87. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  88. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  89. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  90. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +17 -1
  91. package/apps/studio/test/canvas-url.test.ts +99 -0
  92. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  93. package/apps/studio/test/motion-sample.test.ts +35 -0
  94. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  95. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  96. package/apps/studio/test/read-annotations.test.ts +409 -0
  97. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  98. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  99. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  100. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
  101. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
  102. package/apps/studio/test/whats-new.test.ts +101 -0
  103. package/apps/studio/text-imports.d.ts +9 -0
  104. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
  105. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
  106. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  107. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +1 -1
  108. package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +40 -7
  109. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +1 -1
  110. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  111. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  112. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +1 -1
  113. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  114. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +1 -1
  115. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  116. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  117. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +9 -1
  118. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  119. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +3 -1
  120. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  121. package/apps/studio/whats-new.json +139 -0
  122. package/apps/studio/whats-new.schema.json +70 -0
  123. package/apps/studio/whats-new.ts +87 -0
  124. package/cli/bin/maude.mjs +10 -0
  125. package/cli/commands/design.mjs +15 -7
  126. package/cli/commands/design.test.mjs +36 -0
  127. package/cli/commands/help.mjs +8 -2
  128. package/cli/commands/hub.mjs +3 -3
  129. package/cli/commands/hub.test.mjs +1 -1
  130. package/cli/commands/scenario-report.mjs +2 -9
  131. package/cli/lib/flow-design-integration.test.mjs +1 -1
  132. package/cli/lib/gitignore-block.mjs +1 -0
  133. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  134. package/cli/lib/preflight.mjs +1 -1
  135. package/package.json +15 -15
  136. package/plugins/design/templates/brief-board.tsx.template +61 -0
  137. package/plugins/design/templates/canvas.tsx.template +1 -1
  138. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  139. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  140. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  141. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  142. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  143. package/plugins/design/dev-server/dist/styles.css +0 -2710
  144. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  145. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  146. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  147. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  148. /package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
  149. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  150. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  151. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  152. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  153. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  154. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  155. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  156. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  157. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  158. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  159. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  160. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  161. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  162. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  163. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  164. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  165. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  166. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  167. /package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
  168. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  169. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  170. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  171. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  172. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  173. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  174. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  175. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  176. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  177. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  178. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  179. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  180. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  181. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  182. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  183. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  184. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  185. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  186. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  187. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  188. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  189. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  190. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  191. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  192. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  193. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  194. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  195. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  196. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  197. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  198. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  199. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  200. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  201. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,97 @@
1
+ // DDR-093 — `/_index-data` attaches a per-canvas design-system map so the
2
+ // client's canvasUrl() can inject each UI canvas's OWN tokens instead of always
3
+ // designSystems[0]. This is the server half of the
4
+ // "non-default-DS canvas renders unstyled" fix.
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ import { bootServer, killProc, nextPort } from './_helpers.ts';
12
+
13
+ // A two-DS sandbox: `project` is the default (ds0, rootClass mdcc), `maude` is
14
+ // the non-default one (rootClass maude) the bug broke.
15
+ function makeMultiDsSandbox(): { root: string; designRoot: string } {
16
+ const root = mkdtempSync(join(tmpdir(), 'mdcc-multids-'));
17
+ const designRoot = join(root, '.design');
18
+ mkdirSync(designRoot, { recursive: true });
19
+ writeFileSync(
20
+ join(designRoot, 'config.json'),
21
+ JSON.stringify(
22
+ {
23
+ name: 'multi-ds-test',
24
+ designRoot: '.design',
25
+ canvasGroups: [
26
+ { label: 'Design system', path: 'system' },
27
+ { label: 'UI kit', path: 'ui' },
28
+ ],
29
+ designSystems: [
30
+ { name: 'project', path: 'system/project', rootClass: 'mdcc' },
31
+ { name: 'maude', path: 'system/maude', rootClass: 'maude' },
32
+ ],
33
+ defaultDesignSystem: 'project',
34
+ },
35
+ null,
36
+ 2
37
+ )
38
+ );
39
+
40
+ const tsx = 'export default function C(){return <main/>}\n';
41
+
42
+ // UI canvas authored under the NON-default DS — declares it in its sidecar.
43
+ mkdirSync(join(designRoot, 'ui'), { recursive: true });
44
+ writeFileSync(join(designRoot, 'ui', 'Studio.tsx'), tsx);
45
+ writeFileSync(
46
+ join(designRoot, 'ui', 'Studio.meta.json'),
47
+ JSON.stringify({ designSystem: 'maude' })
48
+ );
49
+
50
+ // UI canvas with a sidecar that omits designSystem → resolves to the default.
51
+ writeFileSync(join(designRoot, 'ui', 'Plain.tsx'), tsx);
52
+ writeFileSync(join(designRoot, 'ui', 'Plain.meta.json'), JSON.stringify({ title: 'Plain' }));
53
+
54
+ // UI canvas with NO sidecar at all → also defaults.
55
+ writeFileSync(join(designRoot, 'ui', 'Bare.tsx'), tsx);
56
+
57
+ // Specimens — DS resolved path-authoritatively (no sidecar needed).
58
+ mkdirSync(join(designRoot, 'system', 'maude', 'preview'), { recursive: true });
59
+ writeFileSync(join(designRoot, 'system', 'maude', 'preview', 'Buttons.tsx'), tsx);
60
+ mkdirSync(join(designRoot, 'system', 'project', 'preview'), { recursive: true });
61
+ writeFileSync(join(designRoot, 'system', 'project', 'preview', 'Cards.tsx'), tsx);
62
+
63
+ return { root, designRoot };
64
+ }
65
+
66
+ interface IndexData {
67
+ canvasDesignSystems?: Record<string, string>;
68
+ groups?: { paths?: string[] }[];
69
+ }
70
+
71
+ describe('/_index-data — per-canvas design-system map', () => {
72
+ test('attaches the right DS for UI canvases (sidecar) and specimens (path)', async () => {
73
+ const { root } = makeMultiDsSandbox();
74
+ const port = nextPort();
75
+ const proc = await bootServer(root, port);
76
+ try {
77
+ const r = await fetch(`http://localhost:${port}/_index-data`);
78
+ expect(r.status).toBe(200);
79
+ const data = (await r.json()) as IndexData;
80
+ const map = data.canvasDesignSystems ?? {};
81
+
82
+ // The bug: this used to be implicitly designSystems[0] ('project'). The
83
+ // sidecar declares 'maude', so the server must surface 'maude'.
84
+ expect(map['.design/ui/Studio.tsx']).toBe('maude');
85
+
86
+ // Sidecar without designSystem, and no sidecar at all → project default.
87
+ expect(map['.design/ui/Plain.tsx']).toBe('project');
88
+ expect(map['.design/ui/Bare.tsx']).toBe('project');
89
+
90
+ // Specimens resolve from their `system/<ds>/` folder.
91
+ expect(map['.design/system/maude/preview/Buttons.tsx']).toBe('maude');
92
+ expect(map['.design/system/project/preview/Cards.tsx']).toBe('project');
93
+ } finally {
94
+ await killProc(proc);
95
+ }
96
+ });
97
+ });
@@ -0,0 +1,35 @@
1
+ // Unit cover for the live-motion proof sampler's pure delta test. The browser
2
+ // drive itself is integration-shape (needs Chromium + a running dev server);
3
+ // here we pin the `signatureChanged` classifier that decides motion-proven vs
4
+ // dead-mechanism, so a regression can't silently turn a freeze-frame "pass"
5
+ // into a false motion confirmation (DDR-094 M1).
6
+
7
+ import { describe, expect, test } from 'bun:test';
8
+ import { signatureChanged } from '../bin/_motion-sample-playwright.mjs';
9
+
10
+ const base = { x: 10, y: 10, width: 80, height: 80, transform: 'none', opacity: 1 };
11
+
12
+ describe('signatureChanged', () => {
13
+ test('identical samples ⇒ no change (dead mechanism)', () => {
14
+ expect(signatureChanged(base, { ...base })).toBe(false);
15
+ });
16
+ test('a bbox shift (morph/scale/translate) ⇒ change', () => {
17
+ expect(signatureChanged(base, { ...base, width: 92 })).toBe(true);
18
+ });
19
+ test('a transform-matrix change ⇒ change (rotate/scale via CSS)', () => {
20
+ expect(signatureChanged(base, { ...base, transform: 'matrix(1,0.2,0,1,0,0)' })).toBe(true);
21
+ });
22
+ test('an opacity fade ⇒ change (no bbox delta)', () => {
23
+ expect(signatureChanged(base, { ...base, opacity: 0.4 })).toBe(true);
24
+ });
25
+ test('sub-epsilon numeric jitter is NOT a change (anti-flake)', () => {
26
+ expect(signatureChanged(base, { ...base, x: 10.1 })).toBe(false);
27
+ expect(signatureChanged(base, { ...base, x: 10.1 }, 0.05)).toBe(true); // tighter eps catches it
28
+ });
29
+ test('the internal __missing marker never counts as a change', () => {
30
+ expect(signatureChanged({ __missing: true }, { __missing: true })).toBe(false);
31
+ });
32
+ test('a null sample is not a change (fail closed)', () => {
33
+ expect(signatureChanged(null, base)).toBe(false);
34
+ });
35
+ });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  // Performance harness for Phase 3.4 budgets.
3
3
  //
4
- // bun run plugins/design/dev-server/test/perf-harness.ts
4
+ // bun run apps/studio/test/perf-harness.ts
5
5
  //
6
6
  // Boots the dev-server against a synthetic .design/ fixture, measures the
7
7
  // budget table from `.ai/plans/phase-3.4-architecture-refactor.md`, and writes
@@ -19,7 +19,9 @@ import { buildCanvasModule } from '../canvas-build.ts';
19
19
  import { transpileCanvasSource } from '../canvas-pipeline.ts';
20
20
  import { emitRegistryItem } from '../handoff.ts';
21
21
 
22
- const REPO_ROOT = path.resolve(import.meta.dir, '../../../..');
22
+ // apps/studio/test → ../../.. = repo root (DDR-095: was ../../../.. at the old
23
+ // plugins/design/dev-server/test depth).
24
+ const REPO_ROOT = path.resolve(import.meta.dir, '../../..');
23
25
  const DESIGN_ROOT = path.join(REPO_ROOT, '.design');
24
26
  const DOCS_SITE = path.join(DESIGN_ROOT, 'ui/Docs Site.tsx');
25
27
  const CANVAS_VIEWPORT = path.join(DESIGN_ROOT, 'ui/Canvas Viewport.tsx');
@@ -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
+ });
@@ -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
+ });