@1agh/maude 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +2 -2
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
  3. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +286 -6
  4. package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1209 -122
  5. package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
  6. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  7. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  8. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  9. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  10. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  11. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  12. package/apps/studio/bin/read-annotations.mjs +438 -0
  13. package/apps/studio/bin/read-annotations.sh +11 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  15. package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +1 -1
  16. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  17. package/apps/studio/bin/server-up.sh +234 -0
  18. package/apps/studio/bin/to-lottie-verify.html +68 -0
  19. package/apps/studio/bin/to-lottie.sh +128 -0
  20. package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +19 -3
  21. package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
  22. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +2 -2
  24. package/apps/studio/canvas-create.ts +104 -0
  25. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +47 -13
  26. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
  27. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  28. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  29. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +41 -33
  30. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +57 -42
  31. package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
  32. package/apps/studio/client/canvas-url.js +81 -0
  33. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  34. package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
  35. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  36. package/apps/studio/client/styles/3-shell-maude.css +459 -0
  37. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  38. package/apps/studio/client/styles/4-components-maude.css +74 -0
  39. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
  40. package/apps/studio/client/styles/5-maude-overrides.css +123 -0
  41. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  42. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  43. package/apps/studio/client/tour/overlay.jsx +191 -0
  44. package/apps/studio/client/tour/usage-tour.js +35 -0
  45. package/apps/studio/client/whats-new-seen.js +52 -0
  46. package/apps/studio/client/whats-new.jsx +250 -0
  47. package/{plugins/design/dev-server → apps/studio}/collab/index.ts +4 -4
  48. package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +1 -1
  49. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
  50. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  51. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
  52. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
  53. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +2 -2
  54. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
  55. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
  56. package/apps/studio/dist/client.bundle.js +22825 -0
  57. package/apps/studio/dist/comment-mount.js +1960 -0
  58. package/apps/studio/dist/runtime/lib0_decoding.js +1 -0
  59. package/apps/studio/dist/runtime/lib0_encoding.js +1 -0
  60. package/apps/studio/dist/runtime/motion.js +4 -0
  61. package/apps/studio/dist/runtime/motion_react.js +12 -0
  62. package/apps/studio/dist/runtime/pixi-js.js +3397 -0
  63. package/apps/studio/dist/runtime/react-dom.js +1 -0
  64. package/apps/studio/dist/runtime/react-dom_client.js +9 -0
  65. package/apps/studio/dist/runtime/react.js +1 -0
  66. package/apps/studio/dist/runtime/react_jsx-dev-runtime.js +1 -0
  67. package/apps/studio/dist/runtime/react_jsx-runtime.js +1 -0
  68. package/apps/studio/dist/runtime/y-protocols_awareness.js +1 -0
  69. package/apps/studio/dist/runtime/y-protocols_sync.js +1 -0
  70. package/apps/studio/dist/runtime/yjs.js +5 -0
  71. package/apps/studio/dist/styles.css +5695 -0
  72. package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
  73. package/apps/studio/draw/animate.ts +333 -0
  74. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +4 -4
  75. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  76. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  77. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +9 -6
  78. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +2 -2
  79. package/apps/studio/draw/morph.ts +204 -0
  80. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  82. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  83. package/apps/studio/draw/serialize-animate.ts +324 -0
  84. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  85. package/apps/studio/draw/test/animate.test.ts +172 -0
  86. package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +1 -1
  87. package/apps/studio/draw/test/morph.test.ts +107 -0
  88. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  89. package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +1 -1
  90. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  91. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  92. package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +1 -2
  93. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +14 -2
  94. package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +2 -2
  95. package/{plugins/design/dev-server → apps/studio}/http.ts +103 -2
  96. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
  97. package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
  98. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +6 -3
  99. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
  100. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  101. package/{plugins/design/dev-server → apps/studio}/server.ts +19 -1
  102. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +6 -6
  103. package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +2 -2
  104. package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +1 -1
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +1 -2
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +69 -6
  107. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +98 -1
  108. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  109. package/apps/studio/test/asset-api.test.ts +149 -0
  110. package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +1 -1
  111. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  112. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +3 -1
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +1 -1
  114. package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +1 -1
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +1 -1
  116. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +2 -3
  117. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  118. package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +1 -2
  119. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +18 -3
  120. package/apps/studio/test/canvas-url.test.ts +99 -0
  121. package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +1 -1
  122. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +1 -1
  123. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +1 -1
  124. package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +1 -1
  125. package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +1 -2
  126. package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +1 -2
  127. package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +1 -2
  128. package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +1 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +1 -1
  130. package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +1 -2
  131. package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +1 -2
  132. package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +1 -2
  133. package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +1 -1
  134. package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +1 -1
  135. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  136. package/apps/studio/test/motion-sample.test.ts +35 -0
  137. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  138. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  139. package/apps/studio/test/read-annotations.test.ts +409 -0
  140. package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +1 -1
  141. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  142. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +2 -2
  143. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +2 -3
  144. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +1 -1
  145. package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +1 -1
  146. package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +2 -3
  147. package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +2 -3
  148. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +1 -1
  149. package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +1 -1
  150. package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +1 -2
  151. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +4 -5
  152. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +1 -2
  153. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +3 -8
  154. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +1 -2
  156. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  157. package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +4 -1
  158. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
  159. package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
  161. package/apps/studio/test/whats-new.test.ts +101 -0
  162. package/apps/studio/text-imports.d.ts +9 -0
  163. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
  164. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
  165. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  166. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +2 -2
  167. package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +41 -12
  168. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +3 -7
  169. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +2 -2
  170. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +2 -2
  171. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +2 -2
  172. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  173. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +4 -5
  174. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  175. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  176. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +10 -2
  177. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  178. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +4 -2
  179. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +5 -6
  180. package/apps/studio/whats-new.json +139 -0
  181. package/apps/studio/whats-new.schema.json +70 -0
  182. package/apps/studio/whats-new.ts +87 -0
  183. package/cli/bin/maude.mjs +10 -0
  184. package/cli/commands/design.mjs +84 -10
  185. package/cli/commands/design.test.mjs +133 -1
  186. package/cli/commands/help.mjs +8 -2
  187. package/cli/commands/hub.mjs +3 -3
  188. package/cli/commands/hub.test.mjs +1 -1
  189. package/cli/commands/scenario-report.mjs +2 -9
  190. package/cli/lib/cache.mjs +1 -1
  191. package/cli/lib/config-lint.mjs +1 -1
  192. package/cli/lib/copy-tree.mjs +1 -1
  193. package/cli/lib/design-link.mjs +1 -1
  194. package/cli/lib/flow-design-integration.test.mjs +2 -2
  195. package/cli/lib/gitignore-block.mjs +1 -0
  196. package/cli/lib/gitignore-block.test.mjs +2 -2
  197. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  198. package/cli/lib/preflight.mjs +1 -1
  199. package/cli/lib/stack-detect.mjs +1 -1
  200. package/package.json +16 -16
  201. package/plugins/design/templates/_shell.html +3 -3
  202. package/plugins/design/templates/brief-board.tsx.template +61 -0
  203. package/plugins/design/templates/canvas.tsx.template +1 -1
  204. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +18 -3
  205. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  206. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  207. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  208. package/plugins/design/dev-server/bin/server-up.sh +0 -135
  209. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  210. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  211. package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +0 -620
  212. package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +0 -604
  213. package/plugins/design/dev-server/dist/runtime/motion.js +0 -4944
  214. package/plugins/design/dev-server/dist/runtime/motion_react.js +0 -10056
  215. package/plugins/design/dev-server/dist/runtime/pixi-js.js +0 -53104
  216. package/plugins/design/dev-server/dist/runtime/react-dom.js +0 -231
  217. package/plugins/design/dev-server/dist/runtime/react-dom_client.js +0 -10357
  218. package/plugins/design/dev-server/dist/runtime/react.js +0 -535
  219. package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +0 -60
  220. package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +0 -85
  221. package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +0 -376
  222. package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +0 -100
  223. package/plugins/design/dev-server/dist/runtime/yjs.js +0 -6687
  224. package/plugins/design/dev-server/dist/styles.css +0 -2710
  225. package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  226. package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  227. package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  228. package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  229. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
  230. package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  231. package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  232. package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  233. package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  234. package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  235. package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  236. package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  237. package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  238. package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  239. package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  240. package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  241. package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  242. package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  243. package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  244. package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  245. package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  246. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
  247. package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  248. package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  249. package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  250. package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  251. package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  252. package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  253. package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  254. package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  255. package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  256. package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  257. package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +1 -1
  258. package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  259. package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  260. package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  261. package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  262. package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  263. package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  264. package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  265. package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  266. package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  267. package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +1 -1
  268. package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  269. package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  270. package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  271. package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +1 -1
  272. package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  273. package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  274. package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  275. package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  276. package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +1 -1
  277. package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +1 -1
  278. package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +1 -1
  279. package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +1 -1
  280. package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  281. package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +1 -1
  282. package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  283. package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  284. package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  285. package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
  286. package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  287. package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  288. package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  289. package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  290. package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  291. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +1 -1
  292. package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  293. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
  294. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
  295. package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  296. package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  297. package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  298. package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +1 -1
  299. package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  300. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +3 -3
  301. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
  302. package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  303. package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  304. package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  305. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
  306. package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  307. package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +2 -2
  308. package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  309. package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  310. package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  311. package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  312. package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +1 -1
  313. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  314. package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  315. package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  316. package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  317. package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  318. package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +2 -2
  319. package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  320. package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  321. package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  322. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
  323. package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  324. package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  325. package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  326. package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  327. package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  328. package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  329. package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  330. package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  331. package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  332. package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  333. package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  334. package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +1 -1
  335. package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  336. package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  337. package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +2 -2
  338. package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +1 -1
  339. package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  340. package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  341. package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  342. package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  343. package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  344. package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  345. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  346. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +1 -1
  347. package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +1 -1
  348. package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  349. package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  350. package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +2 -2
  351. package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  352. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +3 -3
  353. package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +2 -2
  354. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  355. package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  356. package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  357. package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +1 -1
  358. package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +2 -2
  359. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @file draw/morph.ts — deterministic shape-morph producer for the IR
3
+ * @scope apps/studio/draw/morph.ts
4
+ * @purpose Turn ONE base `d` path into an animatable `d`-morph {@link Track}
5
+ * of VERTEX ARRAYS — the cross-renderer interpolable form. Every
6
+ * keyframe shares an identical command template + a fixed vertex
7
+ * count, which is the hard constraint that lets web (SMIL/motion)
8
+ * AND Lottie tween the morph without reparsing geometry per frame.
9
+ *
10
+ * This is the engine replacement for the studyfi-v3 "Python
11
+ * perturbation outside the engine" anti-pattern (DDR-094 Findings):
12
+ * `morphVariants(base, { n, jitter, seed })` is PURE + deterministic
13
+ * (a seeded LCG, no `Math.random`), so a morph is reproducible and
14
+ * reviewable as code.
15
+ *
16
+ * Constraint: morph paths must use ABSOLUTE, pair-coordinate
17
+ * commands only (`M L C S Q T Z`). `H`/`V`/`A` and relative commands
18
+ * throw — they break the "fixed vertex count" interpolability
19
+ * invariant. The engine's `blobPath` (M…C…Z) already conforms.
20
+ *
21
+ * React-free (DDR-067); depends only on `animate.ts` + the `Point`
22
+ * type.
23
+ */
24
+
25
+ import type { CubicBezierHandles, Keyframe, Track } from './animate.ts';
26
+ import type { Point } from './primitives.ts';
27
+
28
+ /** One command of a parsed morph path + how many (x,y) pairs it consumes. */
29
+ export interface MorphCommand {
30
+ cmd: 'M' | 'L' | 'C' | 'S' | 'Q' | 'T' | 'Z';
31
+ /** Number of (x,y) vertex pairs this command consumes (`Z` = 0). */
32
+ nPairs: number;
33
+ }
34
+
35
+ /** A shared path template: the command skeleton every morph keyframe reuses. */
36
+ export interface PathTemplate {
37
+ commands: MorphCommand[];
38
+ /** Total vertex count = Σ nPairs. The fixed-length morph invariant. */
39
+ vertexCount: number;
40
+ }
41
+
42
+ /** A parsed morph path: the reusable template + its base vertex array. */
43
+ export interface ParsedMorph {
44
+ template: PathTemplate;
45
+ vertices: Point[];
46
+ }
47
+
48
+ const PAIRS_PER_CMD: Record<MorphCommand['cmd'], number> = {
49
+ M: 1,
50
+ L: 1,
51
+ C: 3,
52
+ S: 2,
53
+ Q: 2,
54
+ T: 1,
55
+ Z: 0,
56
+ };
57
+
58
+ const NUM_RE = /-?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
59
+
60
+ /**
61
+ * Parse an absolute, pair-coordinate `d` path into a {@link PathTemplate} plus
62
+ * its flat vertex array (vertices in document order). Throws on relative or
63
+ * `H`/`V`/`A` commands — they violate the fixed-vertex-count morph constraint.
64
+ */
65
+ export function parseMorphPath(d: string): ParsedMorph {
66
+ const commands: MorphCommand[] = [];
67
+ const vertices: Point[] = [];
68
+ // Split on PATH-command letters only — NOT every letter — so the `e`/`E` of an
69
+ // exponent (`2e1`) stays inside its number rather than being read as a command.
70
+ const re = /([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)/g;
71
+ let m: RegExpExecArray | null = re.exec(d);
72
+ for (; m !== null; m = re.exec(d)) {
73
+ const letter = m[1];
74
+ if (letter === 'z' || letter === 'Z') {
75
+ commands.push({ cmd: 'Z', nPairs: 0 });
76
+ continue;
77
+ }
78
+ if (!(letter in PAIRS_PER_CMD)) {
79
+ throw new Error(
80
+ `parseMorphPath: command "${letter}" is not morph-safe — use only ` +
81
+ 'absolute pair-coordinate commands (M L C S Q T Z); the blobPath form conforms'
82
+ );
83
+ }
84
+ const cmd = letter as MorphCommand['cmd'];
85
+ const nPairs = PAIRS_PER_CMD[cmd];
86
+ const nums = (m[2].match(NUM_RE) ?? []).map(Number);
87
+ if (nums.length !== nPairs * 2) {
88
+ throw new Error(
89
+ `parseMorphPath: command "${cmd}" expected ${nPairs * 2} numbers, got ${nums.length} ` +
90
+ '(repeated/implicit commands are not supported — emit one explicit command per segment)'
91
+ );
92
+ }
93
+ commands.push({ cmd, nPairs });
94
+ for (let i = 0; i < nums.length; i += 2) vertices.push({ x: nums[i], y: nums[i + 1] });
95
+ }
96
+ return { template: { commands, vertexCount: vertices.length }, vertices };
97
+ }
98
+
99
+ /** Format a number the way the rest of the engine does (no float noise). */
100
+ function fmt(n: number): string {
101
+ return Number.isInteger(n) ? String(n) : String(Math.round(n * 1e4) / 1e4);
102
+ }
103
+
104
+ /**
105
+ * Reassemble a `d` string from a template + a vertex array (the inverse of
106
+ * {@link parseMorphPath}). Throws if the vertex count doesn't match the
107
+ * template — the morph invariant, checked at the seam.
108
+ */
109
+ export function templateToPath(template: PathTemplate, vertices: Point[]): string {
110
+ if (vertices.length !== template.vertexCount) {
111
+ throw new Error(
112
+ `templateToPath: vertex count ${vertices.length} ≠ template ${template.vertexCount}`
113
+ );
114
+ }
115
+ const parts: string[] = [];
116
+ let vi = 0;
117
+ for (const c of template.commands) {
118
+ if (c.cmd === 'Z') {
119
+ parts.push('Z');
120
+ continue;
121
+ }
122
+ const coords: string[] = [];
123
+ for (let p = 0; p < c.nPairs; p++) {
124
+ const v = vertices[vi++];
125
+ coords.push(`${fmt(v.x)} ${fmt(v.y)}`);
126
+ }
127
+ parts.push(`${c.cmd}${coords.join(' ')}`);
128
+ }
129
+ return parts.join('');
130
+ }
131
+
132
+ /** Deterministic LCG in [0,1) — same generator the geometry layer uses. */
133
+ function lcg(seed: number): () => number {
134
+ let s = seed >>> 0 || 1;
135
+ return () => {
136
+ s = (s * 1664525 + 1013904223) >>> 0;
137
+ return s / 4294967296;
138
+ };
139
+ }
140
+
141
+ export interface MorphVariantsOpts {
142
+ /** Number of distinct jittered variants between the base endpoints (default 3). */
143
+ n?: number;
144
+ /** Max per-vertex displacement in user units (default 2). 0 ⇒ no motion. */
145
+ jitter?: number;
146
+ /** Integer seed for the deterministic jitter (default 1). */
147
+ seed?: number;
148
+ /** Total morph duration in seconds (default 1). */
149
+ dur?: number;
150
+ /** Per-segment easing (default linear). */
151
+ ease?: CubicBezierHandles;
152
+ /** Node id this track drives. */
153
+ target?: string;
154
+ /**
155
+ * Close the loop by returning to the base shape on the final keyframe
156
+ * (default true) — a seamless `repeat` morph.
157
+ */
158
+ loop?: boolean;
159
+ }
160
+
161
+ /** The morph producer's output: the IR track + the template to render it. */
162
+ export interface MorphResult {
163
+ track: Track<Point[]>;
164
+ template: PathTemplate;
165
+ /** Render any keyframe's vertex array back to a `d` string. */
166
+ toPath: (vertices: Point[]) => string;
167
+ }
168
+
169
+ /**
170
+ * Produce a deterministic `d`-morph track from a base path. Every keyframe is a
171
+ * vertex array of the SAME length (the base's), jittered by a seeded LCG, evenly
172
+ * spaced over `dur`. With `loop` (default) the last keyframe is the base again,
173
+ * so a repeating timeline morphs seamlessly. Pure + reproducible — the engine
174
+ * replacement for ad-hoc out-of-engine perturbation.
175
+ */
176
+ export function morphVariants(basePath: string, opts: MorphVariantsOpts = {}): MorphResult {
177
+ const n = Math.max(1, opts.n ?? 3);
178
+ const jitter = opts.jitter ?? 2;
179
+ const dur = opts.dur ?? 1;
180
+ const loop = opts.loop ?? true;
181
+ const { template, vertices: base } = parseMorphPath(basePath);
182
+ const rnd = lcg(opts.seed ?? 1);
183
+
184
+ // Keyframe times: base at 0, n jittered variants, optional base again at dur.
185
+ const stops = loop ? n + 2 : n + 1; // includes the base endpoint(s)
186
+ const keyframes: Keyframe<Point[]>[] = [];
187
+ for (let k = 0; k < stops; k++) {
188
+ const t = (k / (stops - 1)) * dur;
189
+ const isBase = k === 0 || (loop && k === stops - 1);
190
+ const value: Point[] = isBase
191
+ ? base.map((p) => ({ ...p }))
192
+ : base.map((p) => ({
193
+ x: p.x + (rnd() * 2 - 1) * jitter,
194
+ y: p.y + (rnd() * 2 - 1) * jitter,
195
+ }));
196
+ const kf: Keyframe<Point[]> = { t, value };
197
+ if (opts.ease && k < stops - 1) kf.ease = opts.ease;
198
+ keyframes.push(kf);
199
+ }
200
+
201
+ const track: Track<Point[]> = { property: 'd', keyframes };
202
+ if (opts.target !== undefined) track.target = opts.target;
203
+ return { track, template, toPath: (v) => templateToPath(template, v) };
204
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file draw/optimize.ts — Phase 25 geometry-engine SVGO wrapper
3
- * @scope plugins/design/dev-server/draw/optimize.ts
3
+ * @scope apps/studio/draw/optimize.ts
4
4
  * @purpose The single external dependency (SVGO — DDR-071). Two jobs:
5
5
  * 1. Minify the final on-disk SVG asset (multipass, 2-decimal
6
6
  * precision) without stripping the a11y/scaling contract.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file draw/palette.ts — Phase 25 geometry-engine color layer
3
- * @scope plugins/design/dev-server/draw/palette.ts
3
+ * @scope apps/studio/draw/palette.ts
4
4
  * @purpose Color correctness the draw-agent verifies from SOURCE, never from
5
5
  * the vision model (VLMs misread color confidently):
6
6
  * • WCAG 2.1 relative-luminance + contrast ratio (4.5 / 3 / 7);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file draw/primitives.ts — Phase 25 geometry-engine primitive layer
3
- * @scope plugins/design/dev-server/draw/primitives.ts
3
+ * @scope apps/studio/draw/primitives.ts
4
4
  * @purpose The `DrawPrimitive` union + grid-snapped constructors + transform
5
5
  * composition + viewBox presets. This is the "intent" layer the
6
6
  * draw-agent emits: the LLM names shapes and where they go; the
@@ -0,0 +1,324 @@
1
+ /**
2
+ * @file draw/serialize-animate.ts — IR Timeline → animated SVG/JSX
3
+ * @scope apps/studio/draw/serialize-animate.ts
4
+ * @purpose Time-generalize the DDR-067 single-source invariant: ONE node tree
5
+ * + ONE {@link Timeline} emits BOTH an animated SVG (on-disk `.svg`
6
+ * form) AND an animated JSX string (inline canvas-preview form). Both
7
+ * render the SAME tree — `primitivesToNodes` from `serialize.ts` with
8
+ * SMIL animation children injected onto each track's target — so the
9
+ * two forms can never structurally drift (the test asserts this).
10
+ *
11
+ * Mechanism (per the DDR-094 conversion rules + `_draw-motion-rules`):
12
+ * • `d` shape morph → `<animate attributeName="d" calcMode="spline">`
13
+ * (NEVER CSS `d:path()`, which doesn't animate live).
14
+ * • opacity / fill / stroke → `<animate>`.
15
+ * • translate / scale / rotate → `<animateTransform additive="sum">`
16
+ * — the position-vs-animation split: animated transforms compose
17
+ * with (never clobber) a static `transform=` attribute.
18
+ * • Per-segment easing → `keySplines` + `calcMode="spline"`, taken
19
+ * verbatim from the IR `cubic-bezier` handles (overshoot survives).
20
+ *
21
+ * Reduced motion: emitted as a `<style>` `prefers-reduced-motion`
22
+ * gate for the SVG form (CSS-driven host). SMIL itself ignores the
23
+ * CSS catch-all (a re-discovered gotcha — DDR-094 background); the
24
+ * durable RM story is the host wrapper (`useReducedMotion`) for JSX
25
+ * and the Lottie host for production. Documented in `_draw-motion-rules.md`.
26
+ *
27
+ * Production delivery is Lottie (DDR-094) — this is the maude
28
+ * authoring/preview form. React-free (DDR-067): JSX is a STRING.
29
+ */
30
+
31
+ import type { CubicBezierHandles, Repeat, Timeline, Track, TrackValue } from './animate.ts';
32
+ import { resolveStagger } from './animate.ts';
33
+ import type { DrawPrimitive, Point } from './primitives.ts';
34
+ import {
35
+ type Dialect,
36
+ primitivesToNodes,
37
+ renderNode,
38
+ type SerializeOpts,
39
+ type SvgNode,
40
+ } from './serialize.ts';
41
+
42
+ /** Per-target renderer for a `d`-morph track's vertex arrays → a path string. */
43
+ export type PathRenderer = (vertices: Point[]) => string;
44
+
45
+ export interface AnimateOpts extends SerializeOpts {
46
+ /**
47
+ * Per-target path renderers for `d`-morph tracks (a {@link Track} on `d`
48
+ * carries vertex arrays; rendering them to `d` strings needs the shared
49
+ * command template). Wire `{ [targetId]: morphResult.toPath }`. A `d` track
50
+ * whose target has no renderer throws.
51
+ */
52
+ pathRenderers?: Record<string, PathRenderer>;
53
+ }
54
+
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+ // Animation plan — the dialect-neutral intermediate both forms share
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+
59
+ /** Properties driven through `<animateTransform>` rather than `<animate>`. */
60
+ const TRANSFORM_TYPES: Record<string, 'translate' | 'scale' | 'rotate'> = {
61
+ translate: 'translate',
62
+ scale: 'scale',
63
+ rotate: 'rotate',
64
+ };
65
+
66
+ /** Logical property → SMIL `attributeName` (transforms handled separately). */
67
+ const ATTR_NAME: Record<string, string> = {
68
+ d: 'd',
69
+ opacity: 'opacity',
70
+ fillOpacity: 'fill-opacity',
71
+ strokeOpacity: 'stroke-opacity',
72
+ fill: 'fill',
73
+ stroke: 'stroke',
74
+ };
75
+
76
+ /** One resolved, normalized animation (a single SMIL element's worth). */
77
+ export interface AnimEntry {
78
+ target: string;
79
+ property: string;
80
+ /** SMIL element to emit. */
81
+ kind: 'animate' | 'animateTransform';
82
+ /** `attributeName` (animate) or transform `type` (animateTransform). */
83
+ attr: string;
84
+ /** Serialized keyframe values (`;`-joined at render). */
85
+ values: string[];
86
+ /** Normalized 0–1 keyframe times (first 0, last 1). */
87
+ keyTimes: number[];
88
+ /** `n-1` `"x1 y1 x2 y2"` splines when any segment eases; else undefined. */
89
+ keySplines?: string[];
90
+ /** Animation window start (seconds). */
91
+ begin: number;
92
+ /** Animation window duration (seconds). */
93
+ dur: number;
94
+ repeat: Repeat;
95
+ }
96
+
97
+ function num(n: number): string {
98
+ return Number.isInteger(n) ? String(n) : String(Math.round(n * 1e4) / 1e4);
99
+ }
100
+
101
+ function serializeValue(
102
+ property: string,
103
+ v: TrackValue,
104
+ pathRenderers: Record<string, PathRenderer>,
105
+ target: string
106
+ ): string {
107
+ if (property === 'd') {
108
+ const r = pathRenderers[target];
109
+ if (!r) {
110
+ throw new Error(
111
+ `serialize-animate: d-morph track on "${target}" needs a pathRenderer ` +
112
+ '(pass { pathRenderers: { [target]: morphResult.toPath } })'
113
+ );
114
+ }
115
+ return r(v as Point[]);
116
+ }
117
+ if (typeof v === 'string') return v;
118
+ if (typeof v === 'number') return num(v);
119
+ if (Array.isArray(v)) {
120
+ // number[] (transform components / stops). Point[] only valid for `d` (above).
121
+ return (v as number[]).map(num).join(' ');
122
+ }
123
+ throw new Error(`serialize-animate: unserializable value for "${property}"`);
124
+ }
125
+
126
+ function splineStr(h: CubicBezierHandles): string {
127
+ return h.map(num).join(' ');
128
+ }
129
+
130
+ /** Resolve one track into an {@link AnimEntry} (normalize times, splines). */
131
+ function planTrack(
132
+ tr: Track,
133
+ pathRenderers: Record<string, PathRenderer>,
134
+ repeat: Repeat
135
+ ): AnimEntry | null {
136
+ const target = tr.target;
137
+ if (!target) return null; // untargeted tracks can't be serialized
138
+ const kfs = tr.keyframes;
139
+ if (kfs.length < 2) return null;
140
+
141
+ const begin = kfs[0].t;
142
+ const dur = kfs[kfs.length - 1].t - begin;
143
+ if (dur <= 0) return null;
144
+
145
+ const keyTimes = kfs.map((k) => Math.min(1, Math.max(0, (k.t - begin) / dur)));
146
+ const values = kfs.map((k) => serializeValue(tr.property, k.value, pathRenderers, target));
147
+
148
+ // keySplines: one per interval. Present only if at least one segment eases.
149
+ const hasEase = kfs.slice(0, -1).some((k) => k.ease);
150
+ let keySplines: string[] | undefined;
151
+ if (hasEase) {
152
+ keySplines = kfs.slice(0, -1).map((k) => splineStr(k.ease ?? [0, 0, 1, 1]));
153
+ }
154
+
155
+ const transformType = TRANSFORM_TYPES[tr.property];
156
+ const entry: AnimEntry = transformType
157
+ ? {
158
+ target,
159
+ property: tr.property,
160
+ kind: 'animateTransform',
161
+ attr: transformType,
162
+ values,
163
+ keyTimes,
164
+ begin,
165
+ dur,
166
+ repeat,
167
+ }
168
+ : {
169
+ target,
170
+ property: tr.property,
171
+ kind: 'animate',
172
+ attr: ATTR_NAME[tr.property] ?? tr.property,
173
+ values,
174
+ keyTimes,
175
+ begin,
176
+ dur,
177
+ repeat,
178
+ };
179
+ if (keySplines) entry.keySplines = keySplines;
180
+ return entry;
181
+ }
182
+
183
+ /**
184
+ * Build the dialect-neutral animation plan from a timeline. Resolves `stagger`,
185
+ * normalizes each track to its own [0,1] window, and maps property → SMIL kind.
186
+ * Both serializers consume this — it IS the single source the parity test checks.
187
+ */
188
+ export function buildAnimPlan(
189
+ timeline: Timeline,
190
+ opts: { pathRenderers?: Record<string, PathRenderer> } = {}
191
+ ): AnimEntry[] {
192
+ const tl = resolveStagger(timeline);
193
+ const repeat: Repeat = tl.repeat ?? 'indefinite';
194
+ const renderers = opts.pathRenderers ?? {};
195
+ const out: AnimEntry[] = [];
196
+ for (const tr of tl.tracks) {
197
+ const e = planTrack(tr, renderers, repeat);
198
+ if (e) out.push(e);
199
+ }
200
+ return out;
201
+ }
202
+
203
+ // ─────────────────────────────────────────────────────────────────────────────
204
+ // SMIL node injection
205
+ // ─────────────────────────────────────────────────────────────────────────────
206
+
207
+ function repeatCount(r: Repeat): string {
208
+ return r === 'indefinite' ? 'indefinite' : String(r);
209
+ }
210
+
211
+ /** An {@link AnimEntry} → a leaf {@link SvgNode} (`<animate>`/`<animateTransform>`). */
212
+ function entryToNode(e: AnimEntry): SvgNode {
213
+ const attrs: Array<[string, string]> = [];
214
+ if (e.kind === 'animateTransform') {
215
+ attrs.push(['attributeName', 'transform']);
216
+ attrs.push(['type', e.attr]);
217
+ attrs.push(['additive', 'sum']); // compose, never clobber a static transform=
218
+ } else {
219
+ attrs.push(['attributeName', e.attr]);
220
+ }
221
+ attrs.push(['values', e.values.join(';')]);
222
+ attrs.push(['keyTimes', e.keyTimes.map(num).join(';')]);
223
+ if (e.keySplines) {
224
+ attrs.push(['calcMode', 'spline']);
225
+ attrs.push(['keySplines', e.keySplines.join(';')]);
226
+ }
227
+ if (e.begin > 0) attrs.push(['begin', `${num(e.begin)}s`]);
228
+ attrs.push(['dur', `${num(e.dur)}s`]);
229
+ attrs.push(['repeatCount', repeatCount(e.repeat)]);
230
+ if (e.repeat !== 'indefinite') attrs.push(['fill', 'freeze']);
231
+ return { tag: e.kind, attrs, children: [] };
232
+ }
233
+
234
+ /** Find a node by `id` (depth-first) and append animation children to it. */
235
+ function injectAnimations(
236
+ root: SvgNode,
237
+ plan: AnimEntry[]
238
+ ): { injected: number; missing: string[] } {
239
+ const byTarget = new Map<string, AnimEntry[]>();
240
+ for (const e of plan) {
241
+ const list = byTarget.get(e.target) ?? [];
242
+ list.push(e);
243
+ byTarget.set(e.target, list);
244
+ }
245
+ const found = new Set<string>();
246
+
247
+ const walk = (node: SvgNode): void => {
248
+ const id = node.attrs.find(([k]) => k === 'id')?.[1];
249
+ if (id && byTarget.has(id)) {
250
+ for (const e of byTarget.get(id) as AnimEntry[]) node.children.push(entryToNode(e));
251
+ found.add(id);
252
+ }
253
+ for (const c of node.children) walk(c);
254
+ };
255
+ walk(root);
256
+
257
+ const missing = [...byTarget.keys()].filter((t) => !found.has(t));
258
+ return { injected: found.size, missing };
259
+ }
260
+
261
+ /** Reduced-motion `<style>` (CSS-host best-effort; SMIL caveat documented). */
262
+ function reducedMotionStyle(): SvgNode {
263
+ const css =
264
+ '@media (prefers-reduced-motion: reduce){' +
265
+ '*{animation-duration:.001ms!important;animation-iteration-count:1!important;' +
266
+ 'transition-duration:.001ms!important}}';
267
+ return { tag: 'style', attrs: [], children: [], text: css };
268
+ }
269
+
270
+ // ─────────────────────────────────────────────────────────────────────────────
271
+ // Public serializers
272
+ // ─────────────────────────────────────────────────────────────────────────────
273
+
274
+ function buildAnimatedTree(
275
+ primitives: DrawPrimitive[],
276
+ timeline: Timeline,
277
+ opts: AnimateOpts,
278
+ dialect: Dialect
279
+ ): { tree: SvgNode; missing: string[] } {
280
+ const tree = primitivesToNodes(primitives, opts);
281
+ const plan = buildAnimPlan(timeline, opts);
282
+ const { missing } = injectAnimations(tree, plan);
283
+ // Only the SVG form gets a live <style> RM gate — a CSS `{}` block can't be a
284
+ // JSX text child without entity-escaping the braces; JSX hosts gate via
285
+ // useReducedMotion (see _draw-motion-rules.md).
286
+ if (dialect === 'svg') tree.children.unshift(reducedMotionStyle());
287
+ return { tree, missing };
288
+ }
289
+
290
+ /**
291
+ * Serialize primitives + a timeline to an animated SVG document string (SMIL +
292
+ * a `prefers-reduced-motion` `<style>` gate). Throws if a track targets an id
293
+ * not present in the tree (fail loud — a typo'd target silently animates nothing).
294
+ */
295
+ export function toAnimatedSvg(
296
+ primitives: DrawPrimitive[],
297
+ timeline: Timeline,
298
+ opts: AnimateOpts
299
+ ): string {
300
+ const { tree, missing } = buildAnimatedTree(primitives, timeline, opts, 'svg');
301
+ if (missing.length) {
302
+ throw new Error(`toAnimatedSvg: track target(s) not found in tree: ${missing.join(', ')}`);
303
+ }
304
+ return renderNode(tree, 'svg', '');
305
+ }
306
+
307
+ /**
308
+ * Serialize primitives + a timeline to an animated JSX string for inline canvas
309
+ * embedding. Renders the SAME SMIL node tree in the JSX dialect (React renders
310
+ * SMIL inside an `<svg>`), so the on-disk and on-canvas animated forms are
311
+ * structurally identical. The reduced-motion gate is the host's responsibility
312
+ * (`useReducedMotion`) — see `_draw-motion-rules.md`.
313
+ */
314
+ export function toAnimatedJsx(
315
+ primitives: DrawPrimitive[],
316
+ timeline: Timeline,
317
+ opts: AnimateOpts
318
+ ): string {
319
+ const { tree, missing } = buildAnimatedTree(primitives, timeline, opts, 'jsx');
320
+ if (missing.length) {
321
+ throw new Error(`toAnimatedJsx: track target(s) not found in tree: ${missing.join(', ')}`);
322
+ }
323
+ return renderNode(tree, 'jsx', '');
324
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file draw/serialize.ts — Phase 25 geometry-engine serializer
3
- * @scope plugins/design/dev-server/draw/serialize.ts
3
+ * @scope apps/studio/draw/serialize.ts
4
4
  * @purpose The dual serializer: ONE `DrawPrimitive[]` produces BOTH an
5
5
  * optimized SVG string (`toSvg`) for on-disk assets AND a JSX string
6
6
  * (`toJsx`) for inline canvas embedding. Both render the SAME
@@ -379,7 +379,7 @@ export function primitivesToNodes(primitives: DrawPrimitive[], opts: SerializeOp
379
379
  // Rendering
380
380
  // ─────────────────────────────────────────────────────────────────────────────
381
381
 
382
- type Dialect = 'svg' | 'jsx';
382
+ export type Dialect = 'svg' | 'jsx';
383
383
 
384
384
  function attrName(key: string, dialect: Dialect): string {
385
385
  const mapped = ATTR_DIALECT[key];
@@ -397,7 +397,13 @@ function escapeAttr(s: string): string {
397
397
  return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
398
398
  }
399
399
 
400
- function renderNode(node: SvgNode, dialect: Dialect, indent: string): string {
400
+ /**
401
+ * Render one {@link SvgNode} tree to a dialect string. Exported so the animation
402
+ * serializer (`serialize-animate.ts`) can render the SAME node tree after it
403
+ * injects SMIL animation children — preserving the DDR-067 single-source
404
+ * invariant for animated marks.
405
+ */
406
+ export function renderNode(node: SvgNode, dialect: Dialect, indent: string): string {
401
407
  const pad = indent;
402
408
  const attrParts: string[] = [];
403
409