@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,172 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ type CubicBezierHandles,
4
+ easeBezier,
5
+ LINEAR,
6
+ lerpValue,
7
+ lerpVertices,
8
+ parallel,
9
+ resolveStagger,
10
+ sampleTrack,
11
+ sequence,
12
+ shiftTrack,
13
+ stagger,
14
+ type Track,
15
+ timeline,
16
+ track,
17
+ trackSpan,
18
+ } from '../animate.ts';
19
+
20
+ describe('easeBezier', () => {
21
+ test('linear is the identity', () => {
22
+ for (const x of [0, 0.25, 0.5, 0.75, 1]) {
23
+ expect(easeBezier(LINEAR, x)).toBeCloseTo(x, 6);
24
+ }
25
+ });
26
+ test('clamps the endpoints exactly', () => {
27
+ const e: CubicBezierHandles = [0.42, 0, 0.58, 1];
28
+ expect(easeBezier(e, 0)).toBe(0);
29
+ expect(easeBezier(e, 1)).toBe(1);
30
+ expect(easeBezier(e, -0.5)).toBe(0);
31
+ expect(easeBezier(e, 1.5)).toBe(1);
32
+ });
33
+ test('ease-in-out is symmetric about the midpoint and monotone', () => {
34
+ const e: CubicBezierHandles = [0.42, 0, 0.58, 1];
35
+ expect(easeBezier(e, 0.5)).toBeCloseTo(0.5, 4);
36
+ let prev = -1;
37
+ for (let i = 0; i <= 20; i++) {
38
+ const y = easeBezier(e, i / 20);
39
+ expect(y).toBeGreaterThanOrEqual(prev - 1e-9);
40
+ prev = y;
41
+ }
42
+ });
43
+ test('preserves overshoot (y > 1) for a snap easing', () => {
44
+ const snap: CubicBezierHandles = [0.34, 1.42, 0.64, 1];
45
+ let maxY = 0;
46
+ for (let i = 0; i <= 100; i++) maxY = Math.max(maxY, easeBezier(snap, i / 100));
47
+ expect(maxY).toBeGreaterThan(1); // the overshoot the snap depends on
48
+ });
49
+ test('round-trips a known x→t→y solve (ease-out)', () => {
50
+ // cubic-bezier(0,0,0.58,1): solving x then evaluating y must land in (x,1).
51
+ const e: CubicBezierHandles = [0, 0, 0.58, 1];
52
+ const y = easeBezier(e, 0.5);
53
+ expect(y).toBeGreaterThan(0.5); // ease-out is ahead of linear at the midpoint
54
+ expect(y).toBeLessThan(1);
55
+ });
56
+ });
57
+
58
+ describe('lerpValue / lerpVertices', () => {
59
+ test('numbers lerp', () => {
60
+ expect(lerpValue(0, 10, 0.3)).toBeCloseTo(3, 6);
61
+ });
62
+ test('number arrays lerp element-wise', () => {
63
+ expect(lerpValue([0, 10], [10, 0], 0.5)).toEqual([5, 5]);
64
+ });
65
+ test('vertex arrays lerp by component', () => {
66
+ const out = lerpVertices([{ x: 0, y: 0 }], [{ x: 4, y: 8 }], 0.25);
67
+ expect(out).toEqual([{ x: 1, y: 2 }]);
68
+ });
69
+ test('strings are a discrete hold (no interpolation)', () => {
70
+ expect(lerpValue('rotate(0)', 'rotate(90)', 0.5)).toBe('rotate(0)');
71
+ expect(lerpValue('rotate(0)', 'rotate(90)', 1)).toBe('rotate(90)');
72
+ });
73
+ test('vertex count mismatch throws (interpolability constraint)', () => {
74
+ expect(() => lerpVertices([{ x: 0, y: 0 }], [], 0.5)).toThrow(/vertex count mismatch/);
75
+ });
76
+ });
77
+
78
+ describe('sampleTrack', () => {
79
+ const tr = track('opacity', [
80
+ { t: 0, value: 0, ease: LINEAR },
81
+ { t: 1, value: 1 },
82
+ ]);
83
+ test('holds before the first and after the last keyframe', () => {
84
+ expect(sampleTrack(tr, -1)).toBe(0);
85
+ expect(sampleTrack(tr, 5)).toBe(1);
86
+ });
87
+ test('linear interpolation mid-segment', () => {
88
+ expect(sampleTrack(tr, 0.5)).toBeCloseTo(0.5, 6);
89
+ });
90
+ test('applies the leading keyframe easing', () => {
91
+ const eased = track('opacity', [
92
+ { t: 0, value: 0, ease: [0, 0, 0.58, 1] }, // ease-out
93
+ { t: 1, value: 1 },
94
+ ]);
95
+ expect(sampleTrack(eased, 0.5) as number).toBeGreaterThan(0.5);
96
+ });
97
+ test('empty track samples undefined', () => {
98
+ expect(sampleTrack(track('opacity', []), 0)).toBeUndefined();
99
+ });
100
+ });
101
+
102
+ describe('combinators', () => {
103
+ const a = (): Track =>
104
+ track(
105
+ 'opacity',
106
+ [
107
+ { t: 0, value: 0 },
108
+ { t: 1, value: 1 },
109
+ ],
110
+ 'a'
111
+ );
112
+ const b = (): Track =>
113
+ track(
114
+ 'opacity',
115
+ [
116
+ { t: 0, value: 1 },
117
+ { t: 2, value: 0 },
118
+ ],
119
+ 'b'
120
+ );
121
+
122
+ test('trackSpan is the latest keyframe time', () => {
123
+ expect(trackSpan([a(), b()])).toBe(2);
124
+ });
125
+
126
+ test('shiftTrack offsets every keyframe', () => {
127
+ const s = shiftTrack(a(), 3);
128
+ expect(s.keyframes.map((k) => k.t)).toEqual([3, 4]);
129
+ expect(a().keyframes[0].t).toBe(0); // immutable
130
+ });
131
+
132
+ test('stagger offsets track i by i·delay', () => {
133
+ const out = stagger([a(), b(), a()], 0.5);
134
+ expect(out[0].keyframes[0].t).toBe(0);
135
+ expect(out[1].keyframes[0].t).toBe(0.5);
136
+ expect(out[2].keyframes[0].t).toBe(1);
137
+ });
138
+
139
+ test('resolveStagger bakes the declarative field and grows dur', () => {
140
+ const tl = timeline({ tracks: [a(), b()], dur: 2, stagger: 0.5 });
141
+ const r = resolveStagger(tl);
142
+ expect(r.stagger).toBeUndefined();
143
+ expect(r.tracks[1].keyframes[0].t).toBe(0.5);
144
+ expect(r.dur).toBe(2.5); // b ended at 2 → shifted to 2.5
145
+ });
146
+
147
+ test('parallel unions tracks and takes the max duration', () => {
148
+ const tl = parallel(
149
+ timeline({ tracks: [a()], dur: 1 }),
150
+ timeline({ tracks: [b()], dur: 2, begin: 0.5 })
151
+ );
152
+ expect(tl.tracks.length).toBe(2);
153
+ expect(tl.dur).toBe(2.5); // 0.5 begin + 2 dur
154
+ expect(tl.tracks[1].keyframes[0].t).toBe(0.5); // b shifted by its begin
155
+ });
156
+
157
+ test('sequence chains timelines end-to-start and sums durations', () => {
158
+ const tl = sequence(timeline({ tracks: [a()], dur: 1 }), timeline({ tracks: [b()], dur: 2 }));
159
+ expect(tl.dur).toBe(3);
160
+ expect(tl.tracks[0].keyframes.map((k) => k.t)).toEqual([0, 1]);
161
+ expect(tl.tracks[1].keyframes.map((k) => k.t)).toEqual([1, 3]); // shifted by first dur
162
+ });
163
+
164
+ test('sequence honors a per-timeline begin gap', () => {
165
+ const tl = sequence(
166
+ timeline({ tracks: [a()], dur: 1 }),
167
+ timeline({ tracks: [a()], dur: 1, begin: 0.5 })
168
+ );
169
+ expect(tl.tracks[1].keyframes[0].t).toBe(1.5); // 1 (cursor) + 0.5 (gap)
170
+ expect(tl.dur).toBe(2.5);
171
+ });
172
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { sampleTrack } from '../animate.ts';
3
+ import { blobPath } from '../geometry.ts';
4
+ import { morphVariants, parseMorphPath, templateToPath } from '../morph.ts';
5
+ import type { Point } from '../primitives.ts';
6
+
7
+ describe('parseMorphPath', () => {
8
+ test('extracts vertices + a command template (M…C…Z)', () => {
9
+ const { template, vertices } = parseMorphPath('M0 0C1 1 2 2 3 3Z');
10
+ expect(template.commands.map((c) => c.cmd)).toEqual(['M', 'C', 'Z']);
11
+ expect(template.vertexCount).toBe(4); // M=1 + C=3
12
+ expect(vertices).toEqual([
13
+ { x: 0, y: 0 },
14
+ { x: 1, y: 1 },
15
+ { x: 2, y: 2 },
16
+ { x: 3, y: 3 },
17
+ ]);
18
+ });
19
+ test('round-trips through templateToPath', () => {
20
+ const d = 'M0 0L10 0L10 10L0 10Z';
21
+ const { template, vertices } = parseMorphPath(d);
22
+ expect(templateToPath(template, vertices)).toBe(d);
23
+ });
24
+ test('parses negative + decimal + exponent numbers', () => {
25
+ const { vertices } = parseMorphPath('M-1.5 2e1L3 -4.25');
26
+ expect(vertices).toEqual([
27
+ { x: -1.5, y: 20 },
28
+ { x: 3, y: -4.25 },
29
+ ]);
30
+ });
31
+ test('rejects non-morph-safe commands (H/V/A)', () => {
32
+ expect(() => parseMorphPath('M0 0H10')).toThrow(/morph-safe/);
33
+ expect(() => parseMorphPath('M0 0A5 5 0 0 1 10 10')).toThrow(/morph-safe/);
34
+ });
35
+ test('rejects relative (lowercase) commands', () => {
36
+ expect(() => parseMorphPath('M0 0l10 10')).toThrow(/morph-safe/);
37
+ });
38
+ test('templateToPath enforces the vertex-count invariant', () => {
39
+ const { template } = parseMorphPath('M0 0L1 1');
40
+ expect(() => templateToPath(template, [{ x: 0, y: 0 }])).toThrow(/vertex count/);
41
+ });
42
+ });
43
+
44
+ describe('morphVariants', () => {
45
+ const base = blobPath(50, 50, 30, { lobes: 7, seed: 3 });
46
+
47
+ test('every keyframe shares the fixed vertex count (interpolability)', () => {
48
+ const { track, template } = morphVariants(base, { n: 4, jitter: 3, seed: 7 });
49
+ for (const kf of track.keyframes) {
50
+ expect((kf.value as Point[]).length).toBe(template.vertexCount);
51
+ }
52
+ });
53
+
54
+ test('is deterministic for a given seed', () => {
55
+ const a = morphVariants(base, { n: 3, jitter: 4, seed: 9 });
56
+ const b = morphVariants(base, { n: 3, jitter: 4, seed: 9 });
57
+ expect(a.track.keyframes).toEqual(b.track.keyframes);
58
+ });
59
+
60
+ test('different seeds produce different jitter', () => {
61
+ const a = morphVariants(base, { n: 3, jitter: 4, seed: 1 });
62
+ const b = morphVariants(base, { n: 3, jitter: 4, seed: 2 });
63
+ expect(a.track.keyframes[1].value).not.toEqual(b.track.keyframes[1].value);
64
+ });
65
+
66
+ test('jitter 0 keeps every keyframe equal to the base shape', () => {
67
+ const { track } = morphVariants(base, { n: 3, jitter: 0, seed: 5 });
68
+ const first = track.keyframes[0].value;
69
+ for (const kf of track.keyframes) expect(kf.value).toEqual(first);
70
+ });
71
+
72
+ test('loop (default) returns to the base on the final keyframe', () => {
73
+ const { track } = morphVariants(base, { n: 3, jitter: 5, seed: 5 });
74
+ const kfs = track.keyframes;
75
+ expect(kfs[kfs.length - 1].value).toEqual(kfs[0].value); // seamless repeat
76
+ });
77
+
78
+ test('loop:false drops the closing base keyframe', () => {
79
+ const loop = morphVariants(base, { n: 3, jitter: 5, loop: true });
80
+ const open = morphVariants(base, { n: 3, jitter: 5, loop: false });
81
+ expect(open.track.keyframes.length).toBe(loop.track.keyframes.length - 1);
82
+ });
83
+
84
+ test('keyframes span [0, dur] evenly', () => {
85
+ const { track } = morphVariants(base, { n: 2, dur: 2 });
86
+ const ts = track.keyframes.map((k) => k.t);
87
+ expect(ts[0]).toBe(0);
88
+ expect(ts[ts.length - 1]).toBe(2);
89
+ });
90
+
91
+ test('sampling + toPath yields a valid morph-safe d at mid-timeline', () => {
92
+ const { track, toPath } = morphVariants(base, { n: 3, jitter: 4, dur: 1, seed: 2 });
93
+ const mid = sampleTrack(track, 0.5) as Point[];
94
+ const d = toPath(mid);
95
+ expect(d.startsWith('M')).toBe(true);
96
+ // Re-parsing the rendered frame must keep the same vertex count.
97
+ expect(() => parseMorphPath(d)).not.toThrow();
98
+ });
99
+
100
+ test('ease is applied to all but the last keyframe', () => {
101
+ const ease = [0.34, 1.42, 0.64, 1] as const;
102
+ const { track } = morphVariants(base, { n: 2, ease });
103
+ const kfs = track.keyframes;
104
+ for (let i = 0; i < kfs.length - 1; i++) expect(kfs[i].ease).toEqual(ease);
105
+ expect(kfs[kfs.length - 1].ease).toBeUndefined();
106
+ });
107
+ });
@@ -0,0 +1,142 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { parallel, type Timeline, timeline, track } from '../animate.ts';
3
+ import { morphVariants } from '../morph.ts';
4
+ import { circle, type DrawPrimitive, path } from '../primitives.ts';
5
+ import { buildAnimPlan, toAnimatedJsx, toAnimatedSvg } from '../serialize-animate.ts';
6
+
7
+ // An animated mark: a morphing blob + a blinking eye + a rotating blob.
8
+ const blob = morphVariants('M10 50L50 10L90 50L50 90Z', {
9
+ n: 3,
10
+ jitter: 5,
11
+ seed: 4,
12
+ dur: 1.2,
13
+ ease: [0.34, 1.42, 0.64, 1], // snap with overshoot
14
+ target: 'blob',
15
+ });
16
+
17
+ const PRIMS: DrawPrimitive[] = [
18
+ path({ d: 'M10 50L50 10L90 50L50 90Z', id: 'blob', fill: '#e94' }),
19
+ circle({ cx: 50, cy: 45, r: 6, id: 'eye', fill: '#222' }),
20
+ ];
21
+
22
+ const TL: Timeline = parallel(
23
+ timeline({ tracks: [blob.track], repeat: 'indefinite' }),
24
+ timeline({
25
+ tracks: [
26
+ track(
27
+ 'opacity',
28
+ [
29
+ { t: 0, value: 1, ease: [0.42, 0, 0.58, 1] },
30
+ { t: 0.5, value: 0.1 },
31
+ { t: 1, value: 1 },
32
+ ],
33
+ 'eye'
34
+ ),
35
+ track(
36
+ 'rotate',
37
+ [
38
+ { t: 0, value: 0 },
39
+ { t: 1.2, value: 360 },
40
+ ],
41
+ 'blob'
42
+ ),
43
+ ],
44
+ repeat: 'indefinite',
45
+ })
46
+ );
47
+
48
+ const OPTS = { viewBox: '0 0 100 100', pathRenderers: { blob: blob.toPath } };
49
+
50
+ describe('buildAnimPlan', () => {
51
+ const plan = buildAnimPlan(TL, OPTS);
52
+ test('one entry per targeted, ≥2-keyframe track', () => {
53
+ expect(plan.length).toBe(3);
54
+ });
55
+ test('d-morph maps to <animate attributeName=d>', () => {
56
+ const d = plan.find((e) => e.property === 'd');
57
+ expect(d?.kind).toBe('animate');
58
+ expect(d?.attr).toBe('d');
59
+ expect(d?.values[0].startsWith('M')).toBe(true);
60
+ });
61
+ test('rotate maps to animateTransform', () => {
62
+ const r = plan.find((e) => e.property === 'rotate');
63
+ expect(r?.kind).toBe('animateTransform');
64
+ expect(r?.attr).toBe('rotate');
65
+ });
66
+ test('keyTimes are normalized 0→1', () => {
67
+ for (const e of plan) {
68
+ expect(e.keyTimes[0]).toBe(0);
69
+ expect(e.keyTimes[e.keyTimes.length - 1]).toBe(1);
70
+ }
71
+ });
72
+ test('eased track carries n-1 keySplines', () => {
73
+ const op = plan.find((e) => e.property === 'opacity');
74
+ expect(op).toBeDefined();
75
+ expect(op?.keySplines?.length).toBe((op?.values.length ?? 0) - 1);
76
+ });
77
+ });
78
+
79
+ describe('toAnimatedSvg (SMIL)', () => {
80
+ const svg = toAnimatedSvg(PRIMS, TL, OPTS);
81
+ test('injects SMIL d-morph with spline easing', () => {
82
+ expect(svg).toContain('<animate attributeName="d"');
83
+ expect(svg).toContain('calcMode="spline"');
84
+ expect(svg).toContain('keySplines=');
85
+ });
86
+ test('rotate is an additive animateTransform (no static-transform clobber)', () => {
87
+ expect(svg).toMatch(/<animateTransform[^>]*type="rotate"[^>]*additive="sum"/);
88
+ });
89
+ test('preserves the overshoot handle verbatim in keySplines', () => {
90
+ expect(svg).toContain('0.34 1.42 0.64 1');
91
+ });
92
+ test('emits a prefers-reduced-motion gate', () => {
93
+ expect(svg).toContain('prefers-reduced-motion: reduce');
94
+ });
95
+ test('is a complete SVG document', () => {
96
+ expect(svg).toContain('xmlns="http://www.w3.org/2000/svg"');
97
+ expect(svg).toContain('repeatCount="indefinite"');
98
+ });
99
+ });
100
+
101
+ describe('toAnimatedJsx (single-source parity)', () => {
102
+ const svg = toAnimatedSvg(PRIMS, TL, OPTS);
103
+ const jsx = toAnimatedJsx(PRIMS, TL, OPTS);
104
+ const count = (s: string, re: RegExp) => (s.match(re) ?? []).length;
105
+
106
+ test('same count of <animate> elements as the SVG form', () => {
107
+ expect(count(jsx, /<animate /g)).toBe(count(svg, /<animate /g));
108
+ });
109
+ test('same count of <animateTransform> elements as the SVG form', () => {
110
+ expect(count(jsx, /<animateTransform /g)).toBe(count(svg, /<animateTransform /g));
111
+ });
112
+ test('omits the xmlns document marker', () => {
113
+ expect(jsx).not.toContain('xmlns=');
114
+ });
115
+ test('keeps the SMIL animation attributes (host renders SMIL)', () => {
116
+ expect(jsx).toContain('attributeName="d"');
117
+ expect(jsx).toContain('keyTimes=');
118
+ });
119
+ });
120
+
121
+ describe('fail-loud', () => {
122
+ test('a track targeting a missing id throws', () => {
123
+ const bad = timeline({
124
+ tracks: [
125
+ track(
126
+ 'opacity',
127
+ [
128
+ { t: 0, value: 0 },
129
+ { t: 1, value: 1 },
130
+ ],
131
+ 'ghost'
132
+ ),
133
+ ],
134
+ repeat: 1,
135
+ });
136
+ expect(() => toAnimatedSvg(PRIMS, bad, { viewBox: '0 0 100 100' })).toThrow(/not found/);
137
+ });
138
+ test('a d-morph track without a pathRenderer throws', () => {
139
+ const tl = timeline({ tracks: [blob.track], repeat: 1 });
140
+ expect(() => toAnimatedSvg(PRIMS, tl, { viewBox: '0 0 100 100' })).toThrow(/pathRenderer/);
141
+ });
142
+ });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file equal-spacing-detector.ts — T27 (Wave 3)
3
- * @scope plugins/design/dev-server/equal-spacing-detector.ts
3
+ * @scope apps/studio/equal-spacing-detector.ts
4
4
  * @purpose Pure detector for Figma "Smart Selection" pink-dot affordance
5
5
  * (Rasmus Andersson 2018). Given 3+ rects on a single axis,
6
6
  * returns the equal gap + the screen-coord midpoints between
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file equal-spacing-handles.tsx — T27 (Wave 3)
3
- * @scope plugins/design/dev-server/equal-spacing-handles.tsx
3
+ * @scope apps/studio/equal-spacing-handles.tsx
4
4
  * @purpose Figma Smart Selection pink-dot affordance. When 3+ elements are
5
5
  * selected AND their bounding rects are equally distributed on an
6
6
  * axis, render small pink dots at the midpoints between adjacent
@@ -14,11 +14,8 @@
14
14
 
15
15
  import { useEffect, useRef, useState } from 'react';
16
16
 
17
- import {
18
- detectEqualSpacing,
19
- type EqualSpacingResult,
20
- type SpacingAxis,
21
- } from './equal-spacing-detector.ts';
17
+ import { resolveSelectionEl } from './dom-selection.ts';
18
+ import { detectEqualSpacing, type EqualSpacingResult } from './equal-spacing-detector.ts';
22
19
  import { useSelectionSet } from './use-selection-set.tsx';
23
20
 
24
21
  const HOVER_PADDING_PX = 40;
@@ -77,25 +74,6 @@ function ensureStyles(): void {
77
74
  document.head.appendChild(s);
78
75
  }
79
76
 
80
- function cssEscape(s: string): string {
81
- // CSS.escape is available everywhere we care; manual fallback handles the
82
- // jsdom test path where it may be missing.
83
- // biome-ignore lint/suspicious/noExplicitAny: feature detect
84
- const ce = (typeof CSS !== 'undefined' ? (CSS as any).escape : null) as
85
- | ((v: string) => string)
86
- | null;
87
- if (ce) return ce(s);
88
- return s.replace(/(["\\\]])/g, '\\$1');
89
- }
90
-
91
- function safeQuery(selector: string): Element | null {
92
- try {
93
- return document.querySelector(selector);
94
- } catch {
95
- return null;
96
- }
97
- }
98
-
99
77
  interface ScreenRect {
100
78
  x: number;
101
79
  y: number;
@@ -103,12 +81,12 @@ interface ScreenRect {
103
81
  h: number;
104
82
  }
105
83
 
106
- function resolveSelectionRects(sels: Array<{ id?: string; selector: string }>): ScreenRect[] {
84
+ function resolveSelectionRects(
85
+ sels: Array<{ id?: string; selector: string; artboardId?: string | null }>
86
+ ): ScreenRect[] {
107
87
  const rects: ScreenRect[] = [];
108
88
  for (const s of sels) {
109
- const el = s.id
110
- ? document.querySelector(`[data-cd-id="${cssEscape(s.id)}"]`)
111
- : safeQuery(s.selector);
89
+ const el = resolveSelectionEl(document, s);
112
90
  if (!el) continue;
113
91
  const b = (el as HTMLElement).getBoundingClientRect();
114
92
  if (b.width === 0 && b.height === 0) continue;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file export-dialog.tsx — Phase 6.5 T8 export dialog.
3
- * @scope plugins/design/dev-server/export-dialog.tsx
3
+ * @scope apps/studio/export-dialog.tsx
4
4
  * @purpose Native `<dialog>`-based export modal. Three controls — format,
5
5
  * scope, per-format options — plus a Recent tab populated by
6
6
  * `/_api/export-history`. Submit fires `POST /_api/export`, the
@@ -339,6 +339,18 @@ export function ExportDialogProvider({ children }: { children: ReactNode }): Rea
339
339
  useEffect(() => {
340
340
  function onCustom(e: Event) {
341
341
  const detail = (e as CustomEvent<{ scope?: Scope; format?: Format }>).detail ?? {};
342
+ // Plan C — route to the UNIFIED shell Export dialog (parent / main origin)
343
+ // so the in-canvas toolbar + context menu open the SAME modal as the
344
+ // menubar. Only fall back to this local dialog for a standalone canvas
345
+ // (handoff / exported embed) with no shell parent.
346
+ if (window.parent && window.parent !== window) {
347
+ try {
348
+ window.parent.postMessage({ dgn: 'open-export', detail }, '*');
349
+ return;
350
+ } catch {
351
+ /* fall through to local */
352
+ }
353
+ }
342
354
  open(detail);
343
355
  }
344
356
  window.addEventListener('maude:open-export', onCustom as EventListener);
@@ -20,6 +20,7 @@ import type { Inspect } from './inspect.ts';
20
20
  import { canvasSlug, writeLocator } from './locator.ts';
21
21
  import { DEV_SERVER_ROOT } from './paths.ts';
22
22
  import { getRuntimeBundle, packageForSlug, RUNTIME_PACKAGES, slugFor } from './runtime-bundle.ts';
23
+ import { loadWhatsNew } from './whats-new.ts';
23
24
 
24
25
  // Real disk install root — never the virtual `/$bunfs/root` of compiled bins.
25
26
  // See paths.ts for the resolution logic + Phase 19.1 / v0.18.1 rationale.
@@ -131,7 +132,10 @@ function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
131
132
  // and inside the standalone binary's embedded FS in --compile mode.
132
133
  const DIST_DIR = join(HERE, 'dist');
133
134
  const CLIENT_DIR = join(HERE, 'client');
134
- const TEMPLATES_DIR = join(HERE, '..', 'templates');
135
+ // Templates stayed under the design *plugin* (plugins/design/templates) when the
136
+ // dev-server moved to apps/studio (DDR-095) — both ship in the npm tarball, so
137
+ // from DEV_SERVER_ROOT (apps/studio) reach across with ../../plugins/design.
138
+ const TEMPLATES_DIR = join(HERE, '..', '..', 'plugins', 'design', 'templates');
135
139
 
136
140
  // In-memory transpile cache. Key = absolute canvas path. Repeat GETs against an
137
141
  // unchanged source skip the parse + ID-injection + Bun.Transpiler + Bun.build
@@ -424,6 +428,12 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
424
428
 
425
429
  '/_config': () => Response.json({ ...ctx.cfg, canvasOrigin: ctx.canvasOrigin }),
426
430
 
431
+ // What's New feed (DDR-A) — read-only product-update list surfaced in the
432
+ // Maude UI chrome. Main-origin only by omission from the canvas-origin
433
+ // allowlist below (the untrusted canvas iframe never needs it).
434
+ '/_api/whats-new': () =>
435
+ Response.json(loadWhatsNew(), { headers: { 'Cache-Control': 'no-store' } }),
436
+
427
437
  '/_index-data': async () =>
428
438
  Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
429
439
 
@@ -582,6 +592,91 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
582
592
  return new Response('Method not allowed', { status: 405 });
583
593
  },
584
594
 
595
+ '/_api/canvas': async (req: Request) => {
596
+ // Phase 22 — create + soft-delete a canvas from the browser file tree.
597
+ // POST body { name, kind?: "brief-board", group? } → 201 { file, rel, slug }
598
+ // DELETE ?file=<rel> → 200 { rel, slug, trashed[] }
599
+ // MAIN ORIGIN ONLY: this route is intentionally absent from
600
+ // startCanvasServer's allowlist (DDR-054) — the untrusted canvas iframe
601
+ // origin must never reach a file-write/-delete endpoint. Validation lives in
602
+ // api.createCanvas / api.deleteCanvas (containment + group allowlist).
603
+ if (req.method === 'DELETE') {
604
+ const file = new URL(req.url).searchParams.get('file');
605
+ const result = await api.deleteCanvas({ file });
606
+ if (!result.ok) {
607
+ return Response.json(
608
+ { ok: false, error: result.error },
609
+ { status: result.status, headers: { 'Cache-Control': 'no-store' } }
610
+ );
611
+ }
612
+ return Response.json(
613
+ {
614
+ ok: true,
615
+ rel: result.rel,
616
+ slug: result.slug,
617
+ trashed: result.trashed,
618
+ trashDir: result.trashDir,
619
+ },
620
+ { status: 200, headers: { 'Cache-Control': 'no-store' } }
621
+ );
622
+ }
623
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
624
+ const body = await readJson<{ name?: unknown; kind?: unknown; group?: unknown }>(
625
+ req,
626
+ 8 * 1024
627
+ );
628
+ if (!body) return new Response('body required', { status: 400 });
629
+ const result = await api.createCanvas(body);
630
+ if (!result.ok) {
631
+ return Response.json(
632
+ { ok: false, error: result.error },
633
+ { status: result.status, headers: { 'Cache-Control': 'no-store' } }
634
+ );
635
+ }
636
+ return Response.json(
637
+ { ok: true, file: result.file, rel: result.rel, slug: result.slug },
638
+ { status: 201, headers: { 'Cache-Control': 'no-store' } }
639
+ );
640
+ },
641
+
642
+ '/_api/asset': async (req: Request) => {
643
+ // Phase 23 — binary image upload from the canvas (drag-drop / paste / the
644
+ // a11y file picker). POST raw image bytes → content-addressed write under
645
+ // <designRoot>/assets/<sha8>.<ext>, returns 201 { path }. This route is on
646
+ // the canvas-origin allowlist (CANVAS_SAFE_API below) — a bigger grant than
647
+ // the inert annotation-SVG write (binary, disk) — so the caps in
648
+ // api.saveAsset (magic-byte sniff, 10 MB ceiling, content-addressed name,
649
+ // traversal guard, no-SVG, dedupe) are the load-bearing trust mitigation,
650
+ // NOT optional. See DDR (Task 9).
651
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
652
+ // Early reject on a declared oversize body (the post-read length check in
653
+ // saveAsset is the authoritative gate — Content-Length can be omitted/lied).
654
+ const declared = Number(req.headers.get('content-length') || '0');
655
+ if (Number.isFinite(declared) && declared > 10 * 1024 * 1024) {
656
+ return Response.json(
657
+ { ok: false, error: 'asset exceeds the 10 MB cap' },
658
+ { status: 413, headers: { 'Cache-Control': 'no-store' } }
659
+ );
660
+ }
661
+ let bytes: Uint8Array;
662
+ try {
663
+ bytes = new Uint8Array(await req.arrayBuffer());
664
+ } catch {
665
+ return new Response('could not read request body', { status: 400 });
666
+ }
667
+ const result = await api.saveAsset(bytes);
668
+ if (!result.ok) {
669
+ return Response.json(
670
+ { ok: false, error: result.error },
671
+ { status: result.status ?? 400, headers: { 'Cache-Control': 'no-store' } }
672
+ );
673
+ }
674
+ return Response.json(
675
+ { path: result.path },
676
+ { status: 201, headers: { 'Cache-Control': 'no-store' } }
677
+ );
678
+ },
679
+
585
680
  '/_api/export-history': async (req: Request) => {
586
681
  // Phase 6.5 T10 — read-only recent-exports feed for the dialog's
587
682
  // Recent tab. Writes happen as a side-effect of `/_api/export`.
@@ -791,6 +886,11 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
791
886
  headers: {
792
887
  'Content-Type': MIME[e] || 'application/octet-stream',
793
888
  'Cache-Control': 'no-store',
889
+ // DDR-088 follow-up — never let a browser MIME-sniff a served file
890
+ // (e.g. an uploaded GIF/WEBP polyglot) into a richer type. Assets are
891
+ // only referenced via <image href> + the canvas CSP blocks script, so
892
+ // this is defense-in-depth on the static lane.
893
+ 'X-Content-Type-Options': 'nosniff',
794
894
  },
795
895
  });
796
896
  } catch (err) {
@@ -837,6 +937,7 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
837
937
  '/_api/git-user', // presence display name
838
938
  '/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
839
939
  '/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
940
+ '/_api/asset', // Phase 23 — capped binary image upload (sniff+10MB+sha8 name+no-SVG)
840
941
  '/_api/git-committers', // @mention autocomplete
841
942
  '/_api/ai', // AI-activity banner
842
943
  '/_comments', // per-file comment list (renders pins)