@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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file input-router.tsx — canvas pointer/keyboard classifier + hook
3
- * @scope plugins/design/dev-server/input-router.tsx
3
+ * @scope apps/studio/input-router.tsx
4
4
  * @purpose Owned by canvas-lib's DesignCanvas. Classifies the NON-WHEEL
5
5
  * subset of pointer + key events into discrete router actions.
6
6
  * `useViewportController` keeps owning wheel + middle-mouse +
@@ -68,6 +68,10 @@ export type Tool =
68
68
  | 'hand'
69
69
  | 'comment'
70
70
  | 'pen'
71
+ // Annotation polish (item 8) — FigJam-style highlighter. A `pen`-shaped tool
72
+ // that produces a translucent wide multiply stroke (a PenStroke with
73
+ // `highlighter:true`), not a new stroke type.
74
+ | 'highlighter'
71
75
  // Phase 24 — `shape` is the single active draw tool that produces rect /
72
76
  // ellipse / polygon strokes (the kind is chosen via the palette popover).
73
77
  // `rect` / `ellipse` stay in the union as the stroke discriminants they
@@ -82,6 +86,7 @@ export type Tool =
82
86
 
83
87
  const ANNOTATION_TOOLS = new Set<Tool>([
84
88
  'pen',
89
+ 'highlighter',
85
90
  'shape',
86
91
  'rect',
87
92
  'ellipse',
@@ -167,6 +172,8 @@ export function classify(input: ClassifyInput): RouterAction {
167
172
  if (k === 'h') return { kind: 'tool', tool: 'hand' };
168
173
  if (k === 'c') return { kind: 'tool', tool: 'comment' };
169
174
  if (k === 'b') return { kind: 'tool', tool: 'pen' };
175
+ // I = hIghlighter (a free bare letter; 'H' is taken by Hand).
176
+ if (k === 'i') return { kind: 'tool', tool: 'highlighter' };
170
177
  // Phase 24 — R (and legacy O) both arm the single Shape tool; the specific
171
178
  // primitive is picked from the palette's shape-kind popover.
172
179
  if (k === 'r' || k === 'o') return { kind: 'tool', tool: 'shape' };
@@ -8,6 +8,9 @@ export interface SelectedElement {
8
8
  file: string;
9
9
  /** CSS-selector path (v1 anchor). Always present for backwards-compat + legacy HTML canvases. */
10
10
  selector: string;
11
+ /** Occurrence index among `querySelectorAll(selector)` — disambiguates a
12
+ * component repeated within one artboard. Absent → first match. */
13
+ index?: number;
11
14
  tag: string;
12
15
  classes: string;
13
16
  text: string;
@@ -140,6 +143,7 @@ export function createInspect(
140
143
  return {
141
144
  file,
142
145
  selector: String(sel.selector || ''),
146
+ index: typeof sel.index === 'number' ? sel.index : undefined,
143
147
  tag: String(sel.tag || ''),
144
148
  classes: String(sel.classes || ''),
145
149
  text: String(sel.text || '').slice(0, 240),
@@ -281,7 +285,7 @@ const INSPECTOR_SCRIPT = `
281
285
  var withSelector = commentsCache.filter(function(c) { return c && c.selector; });
282
286
  withSelector.forEach(function(c, i) {
283
287
  var target = null;
284
- try { target = document.querySelector(c.selector); } catch (e) {}
288
+ try { var _all = document.querySelectorAll(c.selector); var _i = (typeof c.index === 'number' && c.index > 0 && c.index < _all.length) ? c.index : 0; target = _all[_i] || _all[0] || null; } catch (e) {}
285
289
  var pin = document.createElement('button');
286
290
  pin.className = 'dgn-pin' + (c.status === 'resolved' ? ' resolved' : '') + (c.id === focusedPinId ? ' focused' : '');
287
291
  pin.textContent = String(i + 1);
@@ -309,7 +313,7 @@ const INSPECTOR_SCRIPT = `
309
313
  var node = pinNodes[i];
310
314
  var x, y, hidden = false;
311
315
  if (!node.target || !node.target.isConnected) {
312
- try { node.target = document.querySelector(node.comment.selector); } catch (e) {}
316
+ try { var _nc = node.comment; var _na = document.querySelectorAll(_nc.selector); var _ni = (typeof _nc.index === 'number' && _nc.index > 0 && _nc.index < _na.length) ? _nc.index : 0; node.target = _na[_ni] || _na[0] || null; } catch (e) {}
313
317
  }
314
318
  if (node.target) {
315
319
  var r = node.target.getBoundingClientRect();
@@ -352,6 +356,19 @@ const INSPECTOR_SCRIPT = `
352
356
  new MutationObserver(function(){ startTick(); }).observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['style', 'class'], childList: true });
353
357
  }
354
358
 
359
+ // ⌘K / Ctrl+K command palette — keydown fired inside the canvas iframe never
360
+ // reaches the shell's window-scoped listener (iframe keyboard isolation), so
361
+ // the palette wouldn't open while focus is in the canvas. Forward the chord to
362
+ // the parent; the shell toggles the palette (mirrors its own handler, which
363
+ // fires "even in inputs"). Capture phase so canvas-lib's pan/zoom keydown
364
+ // handler can't swallow it first.
365
+ document.addEventListener('keydown', function(e) {
366
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
367
+ e.preventDefault();
368
+ try { window.parent.postMessage({ dgn: 'toggle-palette' }, '*'); } catch (err) {}
369
+ }
370
+ }, true);
371
+
355
372
  window.addEventListener('message', function(e) {
356
373
  var m = e.data;
357
374
  if (!m || typeof m !== 'object' || !m.dgn) return;
@@ -368,7 +385,7 @@ const INSPECTOR_SCRIPT = `
368
385
  var c = commentsCache.find(function(x){ return x && x.id === m.id; });
369
386
  if (c && c.selector) {
370
387
  try {
371
- var t = document.querySelector(c.selector);
388
+ var _ta = document.querySelectorAll(c.selector); var _ti = (typeof c.index === 'number' && c.index > 0 && c.index < _ta.length) ? c.index : 0; var t = _ta[_ti] || _ta[0] || null;
372
389
  if (t) t.scrollIntoView({ behavior: 'smooth', block: 'center' });
373
390
  } catch (e) {}
374
391
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file marquee-overlay.tsx — T26 (Wave 3)
3
- * @scope plugins/design/dev-server/marquee-overlay.tsx
3
+ * @scope apps/studio/marquee-overlay.tsx
4
4
  * @purpose Element-level marquee selection. Drag from empty padding inside
5
5
  * an artboard body (NOT on top of a `[data-cd-id]` element) to
6
6
  * lasso multiple stamped elements. Aseprite modifier vocabulary:
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { useEffect, useRef } from 'react';
24
24
 
25
+ import { scopedCdSelector, selectorIndex } from './dom-selection.ts';
25
26
  import { DRAG_THRESHOLD_PX } from './input-router.tsx';
26
27
  import { type Selection, useSelectionSet } from './use-selection-set.tsx';
27
28
  import { useToolMode } from './use-tool-mode.tsx';
@@ -103,10 +104,12 @@ function elementToSelection(el: Element, bodyAncestor: Element): Selection | nul
103
104
  const artboardEl = bodyAncestor.closest('[data-dc-screen]');
104
105
  const artboardId = artboardEl?.getAttribute('data-dc-screen') ?? null;
105
106
  const rect = (el as HTMLElement).getBoundingClientRect();
107
+ const marqueeSelector = scopedCdSelector(cdId, artboardId);
106
108
  return {
107
109
  id: cdId,
108
- selector: `[data-cd-id="${cdId}"]`,
110
+ selector: marqueeSelector,
109
111
  artboardId,
112
+ index: selectorIndex(document, marqueeSelector, el),
110
113
  tag: el.tagName.toLowerCase(),
111
114
  classes: (el.getAttribute('class') ?? '')
112
115
  .trim()
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file participants-chrome.tsx — top-right avatar stack of live peers
3
- * @scope plugins/design/dev-server/participants-chrome.tsx
3
+ * @scope apps/studio/participants-chrome.tsx
4
4
  * @purpose Phase 8 Task 6 — show who's currently on the canvas. Colored
5
5
  * initials in a circle per peer, with a popover for full name +
6
6
  * "Follow" button. Follow mode publishes `followTarget` on my
@@ -8,10 +8,14 @@
8
8
  // is the virtual `/$bunfs/root` (bun --compile
9
9
  // embedded fs) — NOT a real disk path.
10
10
  // Walk up from process.execPath until we find
11
- // the real plugins/design/dev-server/ dir.
12
- // 3. Compiled binary, marketplace cache: similar to (2) but lives at
13
- // ~/.claude/plugins/cache/maude/design/<v>/
14
- // dev-server/. Same walk-up logic finds it.
11
+ // the real apps/studio/ dir.
12
+ // 3. Compiled binary, marketplace cache: the marketplace clone ships only the
13
+ // plugin markdown (plugins/design/) — the
14
+ // dev-server ships via npm (package.json files),
15
+ // so the runtime is resolved from the npm
16
+ // package layout, never the marketplace cache.
17
+ // Same walk-up logic anchors on http.ts. See
18
+ // DDR-095 for the apps/studio relocation.
15
19
  //
16
20
  // Why this matters: Phase 19 v0.18.0 used `dirname(fileURLToPath(import.meta.url))`
17
21
  // universally. In the compiled binary that's `/$bunfs/root` — a virtual path —
@@ -28,7 +32,7 @@ import { fileURLToPath } from 'node:url';
28
32
 
29
33
  /**
30
34
  * Real disk path to the dev-server install dir
31
- * (`plugins/design/dev-server/` inside whatever package layout we're in).
35
+ * (`apps/studio/` inside whatever package layout we're in).
32
36
  *
33
37
  * Always a directory that contains `http.ts` + `dist/` + (optionally)
34
38
  * `node_modules/` and `client/`. Never a virtual `/$bunfs/*` path.
@@ -86,16 +90,16 @@ function resolveDevServerRoot(): string {
86
90
  }
87
91
 
88
92
  // (2 + 3) Compiled binary: walk up from process.execPath until we find a dir
89
- // that *contains* `plugins/design/dev-server/<canonical files>`. Match both
90
- // npm install layout (binary at @1agh/maude-<plat>/maude → walk up 4 levels
91
- // to @1agh/maude/) AND marketplace cache layout (binary somewhere under
92
- // ~/.claude/plugins/cache/maude/design/<v>/dev-server/dist/).
93
+ // that *contains* `apps/studio/<canonical files>`. Match the npm install
94
+ // layout (binary at @1agh/maude-<plat>/maude → walk up to @1agh/maude/, which
95
+ // ships apps/studio/ via package.json files). The dev-server is NOT shipped via
96
+ // the marketplace clone, so there is no marketplace-cache anchor (DDR-095).
93
97
  let cur = dirname(process.execPath);
94
98
  for (let i = 0; i < 10; i++) {
95
99
  // Check if cur itself is the dev-server root.
96
100
  if (isDevServerDir(cur)) return cur;
97
- // Check if cur contains plugins/design/dev-server/.
98
- const nested = join(cur, 'plugins', 'design', 'dev-server');
101
+ // Check if cur contains apps/studio/.
102
+ const nested = join(cur, 'apps', 'studio');
99
103
  if (isDevServerDir(nested)) return nested;
100
104
  const parent = dirname(cur);
101
105
  if (parent === cur) break;
@@ -101,11 +101,20 @@ const { port: BASE_PORT, explicit: PORT_EXPLICIT } = resolvePort();
101
101
 
102
102
  type BunServer = ReturnType<typeof Bun.serve<WsData, never>>;
103
103
 
104
+ // Phase 23 security review (DDR-088 follow-up) — hard ceiling on a buffered
105
+ // request body. Without it Bun's 128 MB default applies, so `POST /_api/asset`
106
+ // would buffer up to 128 MB into RAM BEFORE api.saveAsset's 10 MB check runs
107
+ // (memory amplification from the untrusted canvas origin). 16 MB covers every
108
+ // legit body (10 MB asset + 1 MB annotation SVG + small JSON) with headroom and
109
+ // bounds the pre-handler buffer; the authoritative per-route caps still apply.
110
+ const MAX_REQUEST_BODY = 16 * 1024 * 1024;
111
+
104
112
  function startServer(port: number): BunServer {
105
113
  return Bun.serve<WsData, never>({
106
114
  port,
107
115
  hostname: '127.0.0.1',
108
116
  development: process.env.NODE_ENV !== 'production',
117
+ maxRequestBodySize: MAX_REQUEST_BODY,
109
118
  routes: http.routes,
110
119
  async fetch(req, srv) {
111
120
  const pathname = new URL(req.url).pathname;
@@ -167,6 +176,9 @@ function startCanvasServer(port: number): BunServer {
167
176
  port,
168
177
  hostname: '127.0.0.1',
169
178
  development: process.env.NODE_ENV !== 'production',
179
+ // DDR-088 follow-up — bound the pre-handler request buffer on the UNTRUSTED
180
+ // canvas origin (the asset upload lives here). See MAX_REQUEST_BODY.
181
+ maxRequestBodySize: MAX_REQUEST_BODY,
170
182
  // Hard allowlist of route-table endpoints (Bun matches `routes` before
171
183
  // `fetch`). Only the collab/display-data endpoints the canvas runtime needs
172
184
  // — see http.isCanvasSafeRoute for the trust rationale. The dynamic
@@ -176,6 +188,12 @@ function startCanvasServer(port: number): BunServer {
176
188
  '/_api/git-user': http.routes['/_api/git-user'],
177
189
  '/_api/canvas-meta': http.routes['/_api/canvas-meta'],
178
190
  '/_api/annotations': http.routes['/_api/annotations'],
191
+ // Phase 23 — capped binary image upload (magic-byte sniff + 10 MB +
192
+ // content-addressed name + traversal guard + no-SVG, in api.saveAsset).
193
+ // Bun matches `routes` BEFORE `fetch`, so the route must be listed here
194
+ // explicitly — the CANVAS_SAFE_API entry alone only opens the fetch
195
+ // fall-through (which serves files, not route handlers). See DDR (Task 9).
196
+ '/_api/asset': http.routes['/_api/asset'],
179
197
  '/_api/git-committers': http.routes['/_api/git-committers'],
180
198
  '/_api/ai': http.routes['/_api/ai'],
181
199
  '/_comments': http.routes['/_comments'],
@@ -824,7 +824,7 @@ async function defaultProviderFactory(args: {
824
824
  mod = await import('@hocuspocus/provider');
825
825
  } catch (err) {
826
826
  throw new Error(
827
- `@hocuspocus/provider unavailable — install it under plugins/design/dev-server/. (${err instanceof Error ? err.message : String(err)})`
827
+ `@hocuspocus/provider unavailable — install it under apps/studio/. (${err instanceof Error ? err.message : String(err)})`
828
828
  );
829
829
  }
830
830
  // Hocuspocus accepts ws:// or wss://; the linked URL is http(s)://, so swap
@@ -13,6 +13,8 @@ import {
13
13
  type ArrowStroke,
14
14
  arrowHeadPoints,
15
15
  type EllipseStroke,
16
+ type ImageStroke,
17
+ type LinkStroke,
16
18
  type PenStroke,
17
19
  type PolygonStroke,
18
20
  penPathD,
@@ -696,6 +698,67 @@ describe('annotations-layer / Phase 21 sticky + standalone-text geometry', () =>
696
698
  });
697
699
  });
698
700
 
701
+ describe('annotations-layer / Phase 23 image + link geometry', () => {
702
+ const image: ImageStroke = {
703
+ id: 'im',
704
+ tool: 'image',
705
+ x: 30,
706
+ y: 40,
707
+ w: 200,
708
+ h: 150,
709
+ href: 'assets/deadbeef.png',
710
+ };
711
+ const link: LinkStroke = {
712
+ id: 'lk',
713
+ tool: 'link',
714
+ x: 30,
715
+ y: 40,
716
+ w: 260,
717
+ h: 76,
718
+ url: 'https://example.com',
719
+ title: 'Example',
720
+ domain: 'example.com',
721
+ };
722
+
723
+ test('image bbox is its rect extent (normalizes negative extent)', () => {
724
+ expect(strokeBBox(image)).toEqual({ x: 30, y: 40, w: 200, h: 150 });
725
+ expect(strokeBBox({ ...image, x: 230, y: 190, w: -200, h: -150 })).toEqual({
726
+ x: 30,
727
+ y: 40,
728
+ w: 200,
729
+ h: 150,
730
+ });
731
+ });
732
+
733
+ test('link bbox is its card extent', () => {
734
+ expect(strokeBBox(link)).toEqual({ x: 30, y: 40, w: 260, h: 76 });
735
+ });
736
+
737
+ test('image + link are filled-card hits anywhere inside', () => {
738
+ expect(strokeHitTest(image, 100, 100, 4)).toBe(true); // interior
739
+ expect(strokeHitTest(image, 30, 40, 4)).toBe(true); // corner
740
+ expect(strokeHitTest(image, 999, 999, 4)).toBe(false); // far outside
741
+ expect(strokeHitTest(link, 100, 60, 4)).toBe(true);
742
+ expect(strokeHitTest(link, 999, 999, 4)).toBe(false);
743
+ });
744
+
745
+ test('image serializes a same-origin <image> with preserveAspectRatio', () => {
746
+ const svg = strokesToSvg([image]);
747
+ expect(svg).toContain('<image');
748
+ expect(svg).toContain('data-tool="image"');
749
+ expect(svg).toContain('preserveAspectRatio="xMidYMid meet"');
750
+ expect(svg).toContain('href="assets/deadbeef.png"');
751
+ });
752
+
753
+ test('link serializes a card <g> carrying its data-* payload', () => {
754
+ const svg = strokesToSvg([link]);
755
+ expect(svg).toContain('data-tool="link"');
756
+ expect(svg).toContain('data-url="https://example.com"');
757
+ expect(svg).toContain('data-domain="example.com"');
758
+ expect(svg).toContain('<rect');
759
+ });
760
+ });
761
+
699
762
  describe('annotations-layer / Phase 24 polygon geometry', () => {
700
763
  test('polygonVertices span the full bbox for every shape', () => {
701
764
  for (const shape of ['diamond', 'triangle', 'triangle-down'] as const) {
@@ -14,6 +14,8 @@ import { GlobalRegistrator } from '@happy-dom/global-registrator';
14
14
 
15
15
  import {
16
16
  type ArrowStroke,
17
+ type ImageStroke,
18
+ type LinkStroke,
17
19
  type RectStroke,
18
20
  type StickyStroke,
19
21
  type Stroke,
@@ -217,6 +219,101 @@ describe('annotations round-trip / arrow heads + dash (4 dirs × 2 dash)', () =>
217
219
  }
218
220
  });
219
221
 
222
+ describe('annotations round-trip / image (Phase 23)', () => {
223
+ const image: ImageStroke = {
224
+ id: 'im1',
225
+ tool: 'image',
226
+ x: 120,
227
+ y: 80,
228
+ w: 320,
229
+ h: 240,
230
+ href: 'assets/a1b2c3d4.png',
231
+ };
232
+
233
+ test('image survives serialize → parse with all fields intact', () => {
234
+ const [parsed] = svgToStrokes(strokesToSvg([image])) as ImageStroke[];
235
+ expect(parsed).toEqual(image);
236
+ });
237
+
238
+ test('image alt text round-trips via data-alt', () => {
239
+ const withAlt: ImageStroke = { ...image, alt: 'competitor pricing page' };
240
+ const svg = strokesToSvg([withAlt]);
241
+ expect(svg).toContain('data-alt="competitor pricing page"');
242
+ const [parsed] = svgToStrokes(svg) as ImageStroke[];
243
+ expect(parsed?.alt).toBe('competitor pricing page');
244
+ });
245
+
246
+ test('image with no alt omits data-alt (no phantom default)', () => {
247
+ const svg = strokesToSvg([image]);
248
+ expect(svg).not.toContain('data-alt');
249
+ const [parsed] = svgToStrokes(svg) as ImageStroke[];
250
+ expect(parsed?.alt).toBeUndefined();
251
+ });
252
+
253
+ test('image persists a relative assets href (never a data: URL)', () => {
254
+ const svg = strokesToSvg([image]);
255
+ expect(svg).toContain('href="assets/a1b2c3d4.png"');
256
+ expect(svg).not.toContain('data:');
257
+ });
258
+
259
+ test('image serialize → parse is idempotent', () => {
260
+ const once = strokesToSvg([{ ...image, alt: 'x' }]);
261
+ expect(reparse(once)).toBe(once);
262
+ });
263
+
264
+ test('image SVG survives sanitizeAnnotationSvg byte-intact (PUT-path)', () => {
265
+ // The assets href must SURVIVE the sanitizer (Task 4 relaxation), so the
266
+ // persisted form comes back unchanged on every PUT.
267
+ const svg = strokesToSvg([image]);
268
+ expect(sanitizeAnnotationSvg(svg)).toBe(svg);
269
+ });
270
+ });
271
+
272
+ describe('annotations round-trip / link chip (Phase 23)', () => {
273
+ const link: LinkStroke = {
274
+ id: 'lk1',
275
+ tool: 'link',
276
+ x: 60,
277
+ y: 40,
278
+ w: 260,
279
+ h: 76,
280
+ url: 'https://example.com/blog/post?ref=maude',
281
+ title: 'A reference post worth reacting to',
282
+ domain: 'example.com',
283
+ };
284
+
285
+ test('link survives serialize → parse with all fields intact', () => {
286
+ const [parsed] = svgToStrokes(strokesToSvg([link])) as LinkStroke[];
287
+ expect(parsed).toEqual(link);
288
+ });
289
+
290
+ test('link never persists an <a href> (click-to-open is client-only)', () => {
291
+ const svg = strokesToSvg([link]);
292
+ expect(svg).not.toContain('<a ');
293
+ expect(svg).toContain('data-url="https://example.com/blog/post?ref=maude"');
294
+ });
295
+
296
+ test('link serialize → parse is idempotent (even with a long title)', () => {
297
+ const once = strokesToSvg([{ ...link, title: 'x'.repeat(200) }]);
298
+ expect(reparse(once)).toBe(once);
299
+ });
300
+
301
+ test('link SVG survives sanitizeAnnotationSvg byte-intact (PUT-path)', () => {
302
+ // rect/svg/path/text/data-* are all allowlisted; no on*/style/href → unchanged.
303
+ const svg = strokesToSvg([link]);
304
+ expect(sanitizeAnnotationSvg(svg)).toBe(svg);
305
+ });
306
+
307
+ test('link missing data-title falls back to the domain', () => {
308
+ const svg =
309
+ '<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1">' +
310
+ '<g data-id="lk2" data-tool="link" data-url="https://x.io" data-domain="x.io">' +
311
+ '<rect x="0" y="0" width="260" height="76" rx="8" ry="8"/></g></svg>';
312
+ const [parsed] = svgToStrokes(svg) as LinkStroke[];
313
+ expect(parsed?.title).toBe('x.io');
314
+ });
315
+ });
316
+
220
317
  describe('annotations round-trip / standalone vs anchored text', () => {
221
318
  test('standalone text round-trips its world (x, y) and stays unanchored', () => {
222
319
  const t: TextStroke = {