@1agh/maude 0.28.1 → 0.30.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 (372) hide show
  1. package/README.md +3 -3
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +4537 -0
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
  11. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  12. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  13. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
  15. package/apps/studio/bin/annotate.mjs +732 -0
  16. package/apps/studio/bin/annotate.sh +17 -0
  17. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  18. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  19. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  20. package/apps/studio/bin/read-annotations.mjs +573 -0
  21. package/apps/studio/bin/read-annotations.sh +11 -0
  22. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  24. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  25. package/apps/studio/bin/to-lottie-verify.html +68 -0
  26. package/apps/studio/bin/to-lottie.sh +128 -0
  27. package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
  28. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
  29. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  30. package/apps/studio/canvas-create.ts +104 -0
  31. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
  32. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
  33. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
  34. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  35. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  36. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  37. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
  38. package/apps/studio/client/app.jsx +6742 -0
  39. package/apps/studio/client/canvas-url.js +81 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  41. package/apps/studio/client/index.html +16 -0
  42. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  43. package/apps/studio/client/styles/3-shell-maude.css +1161 -0
  44. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  45. package/apps/studio/client/styles/4-components-maude.css +74 -0
  46. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
  47. package/apps/studio/client/styles/5-maude-overrides.css +137 -0
  48. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  49. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  50. package/apps/studio/client/tour/overlay.jsx +279 -0
  51. package/apps/studio/client/tour/usage-tour.js +48 -0
  52. package/apps/studio/client/whats-new-seen.js +52 -0
  53. package/apps/studio/client/whats-new.jsx +250 -0
  54. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
  55. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  56. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
  57. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
  58. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
  59. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
  60. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
  61. package/apps/studio/dist/client.bundle.js +25545 -0
  62. package/apps/studio/dist/comment-mount.js +2046 -0
  63. package/apps/studio/dist/styles.css +8018 -0
  64. package/apps/studio/dom-selection.ts +342 -0
  65. package/apps/studio/draw/animate.ts +333 -0
  66. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  67. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  68. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  69. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  70. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  71. package/apps/studio/draw/morph.ts +204 -0
  72. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  73. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  74. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  75. package/apps/studio/draw/serialize-animate.ts +324 -0
  76. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  77. package/apps/studio/draw/test/animate.test.ts +172 -0
  78. package/apps/studio/draw/test/morph.test.ts +107 -0
  79. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  80. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  82. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
  83. package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
  84. package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
  85. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
  86. package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
  87. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  88. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
  89. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  90. package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
  91. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
  92. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
  93. package/apps/studio/sync/cold-start.ts +158 -0
  94. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
  95. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
  96. package/apps/studio/sync/journal.ts +190 -0
  97. package/apps/studio/sync/migrate-seed.ts +321 -0
  98. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
  99. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
  100. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
  101. package/apps/studio/test/annotate-write.test.ts +184 -0
  102. package/apps/studio/test/annotations-align.test.ts +88 -0
  103. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  104. package/apps/studio/test/annotations-groups.test.ts +231 -0
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  107. package/apps/studio/test/annotations-snap.test.ts +79 -0
  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/apps/studio/test/canvas-create-api.test.ts +361 -0
  111. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  112. package/apps/studio/test/canvas-edit.test.ts +319 -0
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  114. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
  116. package/apps/studio/test/canvas-url.test.ts +99 -0
  117. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
  118. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
  119. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
  120. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  121. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  122. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  123. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  124. package/apps/studio/test/motion-sample.test.ts +35 -0
  125. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  126. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  127. package/apps/studio/test/read-annotations.test.ts +409 -0
  128. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  130. package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
  131. package/apps/studio/test/sync-agent.test.ts +575 -0
  132. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
  133. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  134. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
  135. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
  136. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
  137. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  138. package/apps/studio/test/sync-journal.test.ts +176 -0
  139. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
  140. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
  141. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  142. package/apps/studio/test/use-annotation-resize.test.ts +402 -0
  143. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
  144. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  145. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
  146. package/apps/studio/test/whats-new.test.ts +101 -0
  147. package/apps/studio/text-imports.d.ts +9 -0
  148. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
  149. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
  150. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  151. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
  152. package/apps/studio/use-annotation-resize.tsx +912 -0
  153. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
  154. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  156. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
  157. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  158. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
  159. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  161. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
  162. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  163. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
  164. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  165. package/apps/studio/whats-new.json +264 -0
  166. package/apps/studio/whats-new.schema.json +86 -0
  167. package/apps/studio/whats-new.ts +87 -0
  168. package/cli/bin/maude.mjs +10 -0
  169. package/cli/commands/design-link.test.mjs +84 -0
  170. package/cli/commands/design.mjs +16 -7
  171. package/cli/commands/design.test.mjs +40 -0
  172. package/cli/commands/help.mjs +8 -2
  173. package/cli/commands/hub.mjs +3 -3
  174. package/cli/commands/hub.test.mjs +1 -1
  175. package/cli/commands/scenario-report.mjs +2 -9
  176. package/cli/lib/design-link.mjs +51 -1
  177. package/cli/lib/flow-design-integration.test.mjs +1 -1
  178. package/cli/lib/gitignore-block.mjs +1 -0
  179. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  180. package/cli/lib/preflight.mjs +1 -1
  181. package/package.json +15 -15
  182. package/plugins/design/templates/_shell.html +28 -4
  183. package/plugins/design/templates/brief-board.tsx.template +61 -0
  184. package/plugins/design/templates/canvas.tsx.template +1 -1
  185. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  186. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  187. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  188. package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
  189. package/plugins/design/dev-server/client/app.jsx +0 -2985
  190. package/plugins/design/dev-server/client/index.html +0 -15
  191. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  192. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  193. package/plugins/design/dev-server/dist/styles.css +0 -2710
  194. package/plugins/design/dev-server/dom-selection.ts +0 -156
  195. package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
  196. package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
  197. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
  198. package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
  199. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
  200. package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
  201. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  340. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  341. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  342. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  343. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  344. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  345. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  346. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  347. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  348. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  349. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  350. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  351. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  352. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  353. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  354. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  355. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  356. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  357. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  358. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  359. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  362. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  363. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  364. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  365. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  366. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  367. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  368. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  369. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  370. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  371. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  372. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,319 @@
1
+ // canvas-edit.ts — Phase 3.6 Task 5. AST-aware single-attribute edits on TSX
2
+ // canvases. The same component+jsxIndex bookkeeping as canvas-pipeline.ts
3
+ // computes the target ID; tests round-trip an edit through transpileCanvasSource
4
+ // to confirm the IDs line up.
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import {
9
+ applyEdit,
10
+ applyRemove,
11
+ applyTextEdit,
12
+ CanvasEditError,
13
+ editAttribute,
14
+ } from '../canvas-edit.ts';
15
+ import { transpileCanvasSource } from '../canvas-pipeline.ts';
16
+
17
+ const CANVAS = '/abs/Canvas.tsx';
18
+
19
+ function idsOf(source: string): Record<string, string> {
20
+ // Run the pipeline to learn what data-cd-id each JSX element gets, returning
21
+ // a map keyed by the element-type name so tests can ask "what id is the
22
+ // <span>?" without hardcoding the hash.
23
+ const { withIds } = transpileCanvasSource(CANVAS, source);
24
+ const out: Record<string, string> = {};
25
+ for (const m of withIds.matchAll(/<(\w+)([^>]*?)data-cd-id="([0-9a-f]{8})"/g)) {
26
+ if (!out[m[1] as string]) out[m[1] as string] = m[3] as string;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ describe('canvas-edit / applyEdit', () => {
32
+ test('swaps an existing className literal value', () => {
33
+ const src = `function Demo() { return <button className="btn">OK</button>; }`;
34
+ const ids = idsOf(src);
35
+ const id = ids.button as string;
36
+ const out = applyEdit(CANVAS, src, id, 'className', 'btn btn--ghost');
37
+ expect(out.source).toBe(
38
+ `function Demo() { return <button className="btn btn--ghost">OK</button>; }`
39
+ );
40
+ // "btn" (3 chars) → "btn btn--ghost" (14 chars): +11 bytes inside the
41
+ // literal; quotes unchanged.
42
+ expect(out.delta).toBe(11);
43
+ });
44
+
45
+ test('inserts className when missing', () => {
46
+ const src = 'function Demo() { return <div>x</div>; }';
47
+ const ids = idsOf(src);
48
+ const id = ids.div as string;
49
+ const out = applyEdit(CANVAS, src, id, 'className', 'card');
50
+ expect(out.source).toBe(`function Demo() { return <div className="card">x</div>; }`);
51
+ });
52
+
53
+ test('rewrites a plain attribute (aria-label)', () => {
54
+ const src = `function Demo() { return <button aria-label="old">x</button>; }`;
55
+ const ids = idsOf(src);
56
+ const id = ids.button as string;
57
+ const out = applyEdit(CANVAS, src, id, 'aria-label', 'new');
58
+ expect(out.source).toBe(`function Demo() { return <button aria-label="new">x</button>; }`);
59
+ });
60
+
61
+ test('JSX-attribute-escapes a " in a replaced literal value (no JSON \\" corruption)', () => {
62
+ // Regression: the replace branch used to emit JSON.stringify(value) → `\"`,
63
+ // which is invalid JS-escaping inside a JSX attribute and corrupts the source.
64
+ // It must land as the HTML entity &quot; instead. See DDR-105.
65
+ const src = `function Demo() { return <button aria-label="old">x</button>; }`;
66
+ const ids = idsOf(src);
67
+ const id = ids.button as string;
68
+ const out = applyEdit(CANVAS, src, id, 'aria-label', 'say "hi" <b>');
69
+ expect(out.source).toBe(
70
+ `function Demo() { return <button aria-label="say &quot;hi&quot; &lt;b&gt;">x</button>; }`
71
+ );
72
+ // Never the JS-escaped form that broke things.
73
+ expect(out.source).not.toContain('\\"');
74
+ });
75
+
76
+ test('JSX-attribute-escapes a " in a newly inserted attribute value', () => {
77
+ const src = 'function Demo() { return <div>x</div>; }';
78
+ const ids = idsOf(src);
79
+ const id = ids.div as string;
80
+ const out = applyEdit(CANVAS, src, id, 'title', 'a "quoted" title');
81
+ expect(out.source).toBe(
82
+ `function Demo() { return <div title="a &quot;quoted&quot; title">x</div>; }`
83
+ );
84
+ });
85
+
86
+ test('replaces an expression-container attr value with a JSX-escaped literal', () => {
87
+ // <Tag name={expr} /> → <Tag name="value" />, entity-escaped (not JSON-escaped).
88
+ const src = `function Demo() { return <button aria-label={lbl}>x</button>; }`;
89
+ const ids = idsOf(src);
90
+ const id = ids.button as string;
91
+ const out = applyEdit(CANVAS, src, id, 'aria-label', 'with "quote"');
92
+ expect(out.source).toBe(
93
+ `function Demo() { return <button aria-label="with &quot;quote&quot;">x</button>; }`
94
+ );
95
+ });
96
+
97
+ test('swaps a single style.<prop> value inside style={{...}}', () => {
98
+ const src = 'function Demo() { return <div style={{ padding: 8, margin: 4 }}>x</div>; }';
99
+ const ids = idsOf(src);
100
+ const id = ids.div as string;
101
+ const out = applyEdit(CANVAS, src, id, 'style.padding', '14');
102
+ expect(out.source).toBe(
103
+ 'function Demo() { return <div style={{ padding: 14, margin: 4 }}>x</div>; }'
104
+ );
105
+ });
106
+
107
+ test('inserts a missing style key into existing style={{...}}', () => {
108
+ const src = 'function Demo() { return <div style={{ padding: 8 }}>x</div>; }';
109
+ const ids = idsOf(src);
110
+ const id = ids.div as string;
111
+ const out = applyEdit(CANVAS, src, id, 'style.color', '"#facc15"');
112
+ // The exact spacing of the appended key is implementation-defined; assert
113
+ // the substring lands without trampling the prior key.
114
+ expect(out.source).toContain('padding: 8');
115
+ expect(out.source).toContain('color: "#facc15"');
116
+ });
117
+
118
+ test('inserts a brand-new style={{}} prop when missing', () => {
119
+ const src = 'function Demo() { return <div>x</div>; }';
120
+ const ids = idsOf(src);
121
+ const id = ids.div as string;
122
+ const out = applyEdit(CANVAS, src, id, 'style.padding', '14');
123
+ expect(out.source).toContain('<div style={{ padding: 14 }}>x</div>');
124
+ });
125
+
126
+ test('throws CanvasEditError when id is not found', () => {
127
+ const src = 'function Demo() { return <div>x</div>; }';
128
+ expect(() => applyEdit(CANVAS, src, 'deadbeef', 'className', 'x')).toThrow(CanvasEditError);
129
+ });
130
+
131
+ test('refuses to edit data-cd-id (owned by pipeline)', () => {
132
+ const src = 'function Demo() { return <div>x</div>; }';
133
+ const ids = idsOf(src);
134
+ const id = ids.div as string;
135
+ expect(() => applyEdit(CANVAS, src, id, 'data-cd-id', 'beefcafe')).toThrow(CanvasEditError);
136
+ });
137
+
138
+ test('throws on malformed source', () => {
139
+ const src = 'function Demo() { return <not closing';
140
+ expect(() => applyEdit(CANVAS, src, 'aaaaaaaa', 'className', 'x')).toThrow();
141
+ });
142
+
143
+ test('targets the right element when multiple share a type', () => {
144
+ const src =
145
+ 'function Demo() { return (<section>' +
146
+ `<button className="a">A</button>` +
147
+ `<button className="b">B</button>` +
148
+ '</section>); }';
149
+ // The pipeline visits in pre-order; expose all <button> ids by walking the
150
+ // withIds output and capturing each match.
151
+ const { withIds } = transpileCanvasSource(CANVAS, src);
152
+ const matches = [
153
+ ...withIds.matchAll(/<button[^>]*data-cd-id="([0-9a-f]{8})"[^>]*className="([ab])"/g),
154
+ ];
155
+ expect(matches.length).toBe(2);
156
+ const idA = matches.find((m) => m[2] === 'a')?.[1] as string;
157
+ expect(idA).toBeDefined();
158
+ const out = applyEdit(CANVAS, src, idA, 'className', 'A-edited');
159
+ expect(out.source).toContain('className="A-edited"');
160
+ expect(out.source).toContain('className="b"');
161
+ });
162
+ });
163
+
164
+ describe('canvas-edit / applyRemove (reset)', () => {
165
+ test('removes one style key, leaving the siblings + object intact', () => {
166
+ const src = 'function Demo() { return <div style={{ padding: 8, margin: 4 }}>x</div>; }';
167
+ const ids = idsOf(src);
168
+ const out = applyRemove(CANVAS, src, ids.div as string, 'style.margin');
169
+ expect(out.source).toBe('function Demo() { return <div style={{ padding: 8 }}>x</div>; }');
170
+ });
171
+
172
+ test('removes the leading style key (consumes the trailing comma)', () => {
173
+ const src = 'function Demo() { return <div style={{ padding: 8, margin: 4 }}>x</div>; }';
174
+ const ids = idsOf(src);
175
+ const out = applyRemove(CANVAS, src, ids.div as string, 'style.padding');
176
+ expect(out.source).toBe('function Demo() { return <div style={{ margin: 4 }}>x</div>; }');
177
+ });
178
+
179
+ test('removing the only style key drops the whole style attribute', () => {
180
+ const src = 'function Demo() { return <div style={{ padding: 8 }}>x</div>; }';
181
+ const ids = idsOf(src);
182
+ const out = applyRemove(CANVAS, src, ids.div as string, 'style.padding');
183
+ expect(out.source).toBe('function Demo() { return <div>x</div>; }');
184
+ });
185
+
186
+ test('accepts a kebab style prop name (border-radius → borderRadius)', () => {
187
+ const src = 'function Demo() { return <div style={{ borderRadius: 8, padding: 4 }}>x</div>; }';
188
+ const ids = idsOf(src);
189
+ const out = applyRemove(CANVAS, src, ids.div as string, 'style.border-radius');
190
+ expect(out.source).toBe('function Demo() { return <div style={{ padding: 4 }}>x</div>; }');
191
+ });
192
+
193
+ test('removes a plain attribute (custom data-*)', () => {
194
+ const src = 'function Demo() { return <button data-state="active">x</button>; }';
195
+ const ids = idsOf(src);
196
+ const out = applyRemove(CANVAS, src, ids.button as string, 'data-state');
197
+ expect(out.source).toBe('function Demo() { return <button>x</button>; }');
198
+ });
199
+
200
+ test('is a no-op (delta 0) when the key / attribute is absent', () => {
201
+ const src = 'function Demo() { return <div style={{ padding: 8 }}>x</div>; }';
202
+ const ids = idsOf(src);
203
+ const a = applyRemove(CANVAS, src, ids.div as string, 'style.margin');
204
+ expect(a.source).toBe(src);
205
+ expect(a.delta).toBe(0);
206
+ const b = applyRemove(CANVAS, src, ids.div as string, 'data-nope');
207
+ expect(b.source).toBe(src);
208
+ expect(b.delta).toBe(0);
209
+ });
210
+
211
+ test('refuses to remove data-cd-id (pipeline-owned)', () => {
212
+ const src = 'function Demo() { return <div>x</div>; }';
213
+ const ids = idsOf(src);
214
+ expect(() => applyRemove(CANVAS, src, ids.div as string, 'data-cd-id')).toThrow(
215
+ CanvasEditError
216
+ );
217
+ });
218
+ });
219
+
220
+ describe('canvas-edit / editAttribute (fs)', () => {
221
+ test('writes the edit atomically + is a no-op when source unchanged', async () => {
222
+ const tmp = `/tmp/canvas-edit-${Math.random().toString(36).slice(2)}.tsx`;
223
+ const src = `function Demo() { return <div className="x">y</div>; }`;
224
+ await Bun.write(tmp, src);
225
+ const ids = idsOf(src);
226
+ const id = ids.div as string;
227
+
228
+ const r1 = await editAttribute(tmp, id, 'className', 'longer-value');
229
+ expect(r1.delta).toBeGreaterThan(0);
230
+ const after = await Bun.file(tmp).text();
231
+ expect(after).toContain('className="longer-value"');
232
+
233
+ // Second pass with the same value — no-op (the source already has it).
234
+ const r2 = await editAttribute(tmp, id, 'className', 'longer-value');
235
+ expect(r2.delta).toBe(0);
236
+ });
237
+ });
238
+
239
+ // Phase 12 (DDR-103) — inline text-content edit. Leaf-text only; JSX-escaped.
240
+ describe('canvas-edit / applyTextEdit', () => {
241
+ test('overwrites a leaf text node', () => {
242
+ const src = `function Demo() { return <button>Save</button>; }`;
243
+ const id = idsOf(src).button as string;
244
+ const out = applyTextEdit(CANVAS, src, id, 'Uložit');
245
+ expect(out.source).toBe(`function Demo() { return <button>Uložit</button>; }`);
246
+ });
247
+
248
+ test('preserves surrounding whitespace / indentation', () => {
249
+ const src = [
250
+ 'function Demo() {',
251
+ ' return (',
252
+ ' <button>',
253
+ ' Save',
254
+ ' </button>',
255
+ ' );',
256
+ '}',
257
+ ].join('\n');
258
+ const id = idsOf(src).button as string;
259
+ const out = applyTextEdit(CANVAS, src, id, 'Go');
260
+ expect(out.source).toContain(' <button>\n Go\n </button>');
261
+ });
262
+
263
+ test('JSX-escapes <, >, {, }, & so text can never become markup or an expression', () => {
264
+ const src = `function Demo() { return <code>x</code>; }`;
265
+ const id = idsOf(src).code as string;
266
+ const out = applyTextEdit(CANVAS, src, id, 'a < b && c > {d}');
267
+ expect(out.source).toContain('a &lt; b &amp;&amp; c &gt; &#123;d&#125;');
268
+ expect(out.source).not.toContain('{d}');
269
+ });
270
+
271
+ test('refuses an element with mixed (element) children', () => {
272
+ const src = `function Demo() { return <div>hi <b>there</b></div>; }`;
273
+ const id = idsOf(src).div as string;
274
+ expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
275
+ });
276
+
277
+ test('refuses an element whose only child is a JSX expression', () => {
278
+ const src = 'function Demo() { const t = "x"; return <h1>{t}</h1>; }';
279
+ const id = idsOf(src).h1 as string;
280
+ expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
281
+ });
282
+
283
+ test('refuses a self-closing / empty element (no text to edit)', () => {
284
+ const src = `function Demo() { return <div className="x" />; }`;
285
+ const id = idsOf(src).div as string;
286
+ expect(() => applyTextEdit(CANVAS, src, id, 'x')).toThrow(CanvasEditError);
287
+ });
288
+
289
+ test('throws on a missing id', () => {
290
+ const src = `function Demo() { return <button>Save</button>; }`;
291
+ expect(() => applyTextEdit(CANVAS, src, 'deadbeef', 'x')).toThrow(CanvasEditError);
292
+ });
293
+ });
294
+
295
+ // Phase 12 (DDR-103) — the `style.<prop>` write path the CSS knobs ride
296
+ // (api.editCss maps property → camelCase + always quotes the value).
297
+ describe('canvas-edit / applyEdit style.<prop> (CSS-knob path)', () => {
298
+ test('inserts an inline style object when none exists', () => {
299
+ const src = `function Demo() { return <div>x</div>; }`;
300
+ const id = idsOf(src).div as string;
301
+ const out = applyEdit(CANVAS, src, id, 'style.borderRadius', '"8px"');
302
+ expect(out.source).toContain('style={{ borderRadius: "8px" }}');
303
+ });
304
+
305
+ test('overwrites an existing style key, leaving siblings intact', () => {
306
+ const src = `function Demo() { return <div style={{ padding: 4, color: "red" }}>x</div>; }`;
307
+ const id = idsOf(src).div as string;
308
+ const out = applyEdit(CANVAS, src, id, 'style.color', '"blue"');
309
+ expect(out.source).toContain('color: "blue"');
310
+ expect(out.source).toContain('padding: 4');
311
+ });
312
+
313
+ test('accepts a token-valued (var(--…)) string for on-system edits', () => {
314
+ const src = `function Demo() { return <div>x</div>; }`;
315
+ const id = idsOf(src).div as string;
316
+ const out = applyEdit(CANVAS, src, id, 'style.padding', '"var(--space-3)"');
317
+ expect(out.source).toContain('style={{ padding: "var(--space-3)" }}');
318
+ });
319
+ });
@@ -28,7 +28,7 @@ afterAll(async () => {
28
28
  describe('canvas-lib-resolver / canvasLibPath', () => {
29
29
  test('returns the dev-server-internal canvas-lib.tsx path', () => {
30
30
  const p = canvasLibPath();
31
- expect(p.endsWith('plugins/design/dev-server/canvas-lib.tsx')).toBe(true);
31
+ expect(p.endsWith('apps/studio/canvas-lib.tsx')).toBe(true);
32
32
  expect(existsSync(p)).toBe(true);
33
33
  });
34
34
 
@@ -0,0 +1,158 @@
1
+ // use-canvas-media-drop — Phase 23 pure helpers (no DOM).
2
+ //
3
+ // Covers the drop/paste dispatch matrix + the URL helpers. The http(s) scheme
4
+ // gate (isHttpUrl) is security-relevant — a link chip must never carry a
5
+ // javascript:/data: URL into window.open.
6
+
7
+ import { describe, expect, test } from 'bun:test';
8
+
9
+ import {
10
+ anchorTextFromHtml,
11
+ classifyMediaPayload,
12
+ firstHttpUrl,
13
+ isHttpUrl,
14
+ linkDomain,
15
+ type MediaPayload,
16
+ normalizeUrl,
17
+ prettifyUrl,
18
+ } from '../use-canvas-media-drop.tsx';
19
+
20
+ const empty: MediaPayload = { files: [], uriList: '', html: '', plain: '' };
21
+ const fakeFile = (type: string): File => ({ type, name: 'x', size: 1 }) as unknown as File;
22
+
23
+ describe('use-canvas-media-drop / isHttpUrl (scheme gate)', () => {
24
+ test('accepts http + https only', () => {
25
+ expect(isHttpUrl('https://example.com')).toBe(true);
26
+ expect(isHttpUrl('http://example.com/a/b?c=1')).toBe(true);
27
+ });
28
+ test('rejects javascript: / data: / file: / mailto: / garbage', () => {
29
+ expect(isHttpUrl('javascript:alert(1)')).toBe(false);
30
+ expect(isHttpUrl('data:text/html,<script>')).toBe(false);
31
+ expect(isHttpUrl('file:///etc/passwd')).toBe(false);
32
+ expect(isHttpUrl('mailto:a@b.com')).toBe(false);
33
+ expect(isHttpUrl('not a url')).toBe(false);
34
+ expect(isHttpUrl('')).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('use-canvas-media-drop / normalizeUrl (no scheme required)', () => {
39
+ test('keeps an explicit http(s) URL', () => {
40
+ expect(normalizeUrl('https://example.com/x')).toBe('https://example.com/x');
41
+ expect(normalizeUrl('http://example.com')).toBe('http://example.com');
42
+ });
43
+ test('prepends https:// to a bare domain', () => {
44
+ expect(normalizeUrl('example.com')).toBe('https://example.com');
45
+ expect(normalizeUrl('www.example.com/blog/post?x=1')).toBe(
46
+ 'https://www.example.com/blog/post?x=1'
47
+ );
48
+ expect(normalizeUrl('sub.example.io:8080/path')).toBe('https://sub.example.io:8080/path');
49
+ });
50
+ test('rejects dangerous schemes even though it is lenient about http(s)', () => {
51
+ expect(normalizeUrl('javascript:alert(1)')).toBeNull();
52
+ expect(normalizeUrl('data:text/html,x')).toBeNull();
53
+ expect(normalizeUrl('file:///etc/passwd')).toBeNull();
54
+ expect(normalizeUrl('mailto:a@b.com')).toBeNull();
55
+ });
56
+ test('does NOT mistake a bare decimal / non-domain for a URL', () => {
57
+ expect(normalizeUrl('3.5')).toBeNull();
58
+ expect(normalizeUrl('v1.2')).toBeNull();
59
+ expect(normalizeUrl('hello world')).toBeNull();
60
+ expect(normalizeUrl('')).toBeNull();
61
+ });
62
+ });
63
+
64
+ describe('use-canvas-media-drop / firstHttpUrl', () => {
65
+ test('a bare-domain line becomes an https URL (no scheme needed to paste)', () => {
66
+ expect(firstHttpUrl('example.com')).toBe('https://example.com');
67
+ });
68
+
69
+ test('skips uri-list comment lines and returns the first real URL', () => {
70
+ expect(firstHttpUrl('# a comment\nhttps://example.com/p\nhttps://other.com')).toBe(
71
+ 'https://example.com/p'
72
+ );
73
+ });
74
+ test('finds an inline URL in a prose paste', () => {
75
+ expect(firstHttpUrl('see https://example.com/post for details')).toBe(
76
+ 'https://example.com/post'
77
+ );
78
+ });
79
+ test('returns null for non-URL text', () => {
80
+ expect(firstHttpUrl('just some words')).toBeNull();
81
+ expect(firstHttpUrl('javascript:alert(1)')).toBeNull();
82
+ });
83
+ });
84
+
85
+ describe('use-canvas-media-drop / linkDomain + prettifyUrl', () => {
86
+ test('linkDomain strips www.', () => {
87
+ expect(linkDomain('https://www.example.com/x')).toBe('example.com');
88
+ expect(linkDomain('https://sub.example.io')).toBe('sub.example.io');
89
+ });
90
+ test('prettifyUrl yields host + path (no protocol, no trailing slash)', () => {
91
+ expect(prettifyUrl('https://www.example.com/blog/post/')).toBe('example.com/blog/post');
92
+ expect(prettifyUrl('https://example.com/')).toBe('example.com');
93
+ });
94
+ });
95
+
96
+ describe('use-canvas-media-drop / anchorTextFromHtml', () => {
97
+ test('extracts the anchor text from a text/html payload', () => {
98
+ expect(anchorTextFromHtml('<a href="https://x.io">Cool Post</a>')).toBe('Cool Post');
99
+ });
100
+ test('strips inner markup and collapses whitespace', () => {
101
+ expect(anchorTextFromHtml('<a href="#"><b>Bold</b> title</a>')).toBe('Bold title');
102
+ });
103
+ test('returns null when there is no anchor', () => {
104
+ expect(anchorTextFromHtml('<div>no link</div>')).toBeNull();
105
+ expect(anchorTextFromHtml('')).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe('use-canvas-media-drop / classifyMediaPayload (dispatch matrix)', () => {
110
+ test('an image file wins (even when a uri-list is also present)', () => {
111
+ const intent = classifyMediaPayload({
112
+ ...empty,
113
+ files: [fakeFile('image/png')],
114
+ uriList: 'https://example.com',
115
+ });
116
+ expect(intent?.kind).toBe('image');
117
+ });
118
+
119
+ test('a non-image file is ignored', () => {
120
+ expect(classifyMediaPayload({ ...empty, files: [fakeFile('application/pdf')] })).toBeNull();
121
+ });
122
+
123
+ test('a URL drop becomes a link with the anchor text as the title', () => {
124
+ const intent = classifyMediaPayload({
125
+ ...empty,
126
+ uriList: 'https://example.com/post',
127
+ html: '<a href="https://example.com/post">Read this</a>',
128
+ });
129
+ expect(intent).toEqual({ kind: 'link', url: 'https://example.com/post', title: 'Read this' });
130
+ });
131
+
132
+ test('a URL drop with no anchor text falls back to the prettified URL', () => {
133
+ const intent = classifyMediaPayload({ ...empty, uriList: 'https://example.com/post' });
134
+ expect(intent).toEqual({
135
+ kind: 'link',
136
+ url: 'https://example.com/post',
137
+ title: 'example.com/post',
138
+ });
139
+ });
140
+
141
+ test('a bare-domain paste (no scheme) becomes a link normalized to https', () => {
142
+ const intent = classifyMediaPayload({ ...empty, plain: 'example.com/post' });
143
+ expect(intent).toEqual({
144
+ kind: 'link',
145
+ url: 'https://example.com/post',
146
+ title: 'example.com/post',
147
+ });
148
+ });
149
+
150
+ test('a plain-text (non-media) paste classifies as null — caller leaves it alone', () => {
151
+ expect(classifyMediaPayload({ ...empty, plain: 'hello world' })).toBeNull();
152
+ });
153
+
154
+ test('a javascript: URL is never classified as a link', () => {
155
+ expect(classifyMediaPayload({ ...empty, plain: 'javascript:alert(1)' })).toBeNull();
156
+ expect(classifyMediaPayload({ ...empty, uriList: 'javascript:alert(1)' })).toBeNull();
157
+ });
158
+ });
@@ -51,7 +51,24 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
51
51
  expect(await code('/.design/%2e%2e%2fsecret-outside.css')).toBe(403);
52
52
 
53
53
  // (2) Privileged / out-of-scope routes stay 403 on the canvas origin.
54
- for (const p of ['/_config', '/_sync-status', '/_api/export', '/package.json']) {
54
+ // /_api/canvas (Phase 22 file-write) is deliberately NOT in the canvas
55
+ // origin's route allowlist — an untrusted canvas iframe must never create
56
+ // arbitrary .tsx files. It is reachable ONLY from the main origin.
57
+ // Phase 12 (DDR-103) — the in-canvas direct-edit write routes are
58
+ // MAIN-ORIGIN ONLY: absent from CANVAS_SAFE_API + startCanvasServer's
59
+ // route map, so the untrusted canvas iframe origin must never reach a
60
+ // source-write endpoint. A GET here 403s at the gate (not 405 from the
61
+ // handler), proving the route is unreachable on this origin.
62
+ for (const p of [
63
+ '/_config',
64
+ '/_sync-status',
65
+ '/_api/export',
66
+ '/_api/canvas',
67
+ '/_api/edit-css',
68
+ '/_api/edit-text',
69
+ '/_api/edit-attr',
70
+ '/package.json',
71
+ ]) {
55
72
  expect(await code(p)).toBe(403);
56
73
  }
57
74
 
@@ -60,6 +77,13 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
60
77
  expect(await code('/_canvas-shell.html')).toBe(200);
61
78
  // A real .tsx canvas under designRoot transpiles + serves (200).
62
79
  expect(await code('/.design/ui/Gate.tsx')).toBe(200);
80
+ // Phase 23 — the capped image-upload route IS reachable from the canvas
81
+ // origin (it must be — drag-drop/paste/picker run inside the iframe). A GET
82
+ // returns 405 (method-not-allowed) which proves the route is REACHED via
83
+ // the canvas server's explicit `routes` allowlist — NOT 403 (gate-blocked)
84
+ // or 404 (fell through to file-serve). Guards the dual-allowlist invariant:
85
+ // CANVAS_SAFE_API + the startCanvasServer `routes` map must stay in sync.
86
+ expect(await code('/_api/asset')).toBe(405);
63
87
 
64
88
  // (4) The shell carries the hardened CSP (A6).
65
89
  const shell = await fetch(`${canvas}/_canvas-shell.html`);
@@ -0,0 +1,99 @@
1
+ // DDR-093 — canvasUrl() token-resolution. Regression guard for the
2
+ // "non-default-DS UI canvas renders unstyled" bug: every `ui/*.tsx` authored
3
+ // under a non-default design system used to load designSystems[0]'s tokens
4
+ // (scoped to a different `.<rootClass>` subtree) so every `var(--*)` went
5
+ // undefined and the canvas rendered white.
6
+
7
+ import { describe, expect, test } from 'bun:test';
8
+
9
+ import { canvasUrl, urlOf } from '../client/canvas-url.js';
10
+
11
+ // Mirrors this repo's real multi-DS config: `project` is the default (ds0),
12
+ // `maude` is the non-default one that the bug broke.
13
+ const cfg = {
14
+ designRel: '.design',
15
+ designSystems: [
16
+ { name: 'project', path: 'system/project', tokensCssRel: 'system/project/colors_and_type.css' },
17
+ { name: 'maude', path: 'system/maude', tokensCssRel: 'system/maude/colors_and_type.css' },
18
+ ],
19
+ };
20
+
21
+ function paramsOf(url: string): URLSearchParams {
22
+ const qs = url.split('?')[1] ?? '';
23
+ return new URLSearchParams(qs);
24
+ }
25
+
26
+ describe('canvasUrl — per-canvas design-system token resolution', () => {
27
+ test('explicit opts.ds resolves a non-default DS canvas to its OWN tokens', () => {
28
+ const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg, { ds: 'maude' }));
29
+ expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
30
+ expect(p.get('components')).toBe('system/maude/preview/_components.css');
31
+ });
32
+
33
+ test('RCA interface form — relative path + opts.ds', () => {
34
+ const p = paramsOf(canvasUrl('ui/Studio.tsx', cfg, { ds: 'maude' }));
35
+ expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
36
+ expect(p.get('components')).toBe('system/maude/preview/_components.css');
37
+ });
38
+
39
+ test('opts.ds = the default DS resolves to ds0 tokens', () => {
40
+ const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg, { ds: 'project' }));
41
+ expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
42
+ expect(p.get('components')).toBe('system/project/preview/_components.css');
43
+ });
44
+
45
+ test('cfg.canvasDesignSystems map drives resolution when opts.ds is absent', () => {
46
+ const withMap = { ...cfg, canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' } };
47
+ const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', withMap));
48
+ expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
49
+ expect(p.get('components')).toBe('system/maude/preview/_components.css');
50
+ });
51
+
52
+ test('opts.ds wins over the map', () => {
53
+ const withMap = { ...cfg, canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' } };
54
+ const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', withMap, { ds: 'project' }));
55
+ expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
56
+ });
57
+
58
+ test('unknown / no DS falls back to ds0 (single-DS + legacy behavior intact)', () => {
59
+ const p = paramsOf(canvasUrl('.design/ui/Plain.tsx', cfg, {}));
60
+ expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
61
+ expect(p.get('components')).toBe('system/project/preview/_components.css');
62
+ });
63
+
64
+ test('an unknown DS name (not in designSystems) falls back to ds0, never undefined', () => {
65
+ const p = paramsOf(canvasUrl('.design/ui/Ghost.tsx', cfg, { ds: 'does-not-exist' }));
66
+ expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
67
+ });
68
+
69
+ test('specimen paths stay path-resolved and are unaffected by the map', () => {
70
+ const withWrongMap = {
71
+ ...cfg,
72
+ // A bogus map entry must NOT override the specimen branch.
73
+ canvasDesignSystems: { '.design/system/maude/preview/Buttons.tsx': 'project' },
74
+ };
75
+ const p = paramsOf(canvasUrl('.design/system/maude/preview/Buttons.tsx', withWrongMap));
76
+ expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
77
+ expect(p.get('layout')).toBe('system/maude/preview/_layout.css');
78
+ expect(p.get('components')).toBe('system/maude/preview/_components.css');
79
+ });
80
+
81
+ test('the bug it guards against: ds0 is the WRONG DS for a maude canvas', () => {
82
+ // Documents the failure mode — with neither opts.ds nor a map entry the
83
+ // resolver returns designSystems[0] (project), which is exactly what broke a
84
+ // maude canvas before DDR-093. The fix is that the map now supplies 'maude'.
85
+ const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg));
86
+ expect(p.get('tokens')).toBe('system/project/colors_and_type.css'); // ds0 — wrong for maude
87
+ const fixed = paramsOf(
88
+ canvasUrl('.design/ui/Studio.tsx', {
89
+ ...cfg,
90
+ canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' },
91
+ })
92
+ );
93
+ expect(fixed.get('tokens')).toBe('system/maude/colors_and_type.css'); // fixed
94
+ });
95
+
96
+ test('non-tsx paths bypass the shell and return a direct URL', () => {
97
+ expect(canvasUrl('.design/ui/legacy.html', cfg)).toBe(urlOf('.design/ui/legacy.html'));
98
+ });
99
+ });
@@ -4,8 +4,6 @@
4
4
 
5
5
  import { describe, expect, test } from 'bun:test';
6
6
 
7
- import * as Y from 'yjs';
8
-
9
7
  import { Y_TYPES } from '../collab/persistence.ts';
10
8
  import { createRegistry } from '../collab/registry.ts';
11
9
  import type { RoomCallbacks } from '../collab/room.ts';
@@ -1,17 +1,12 @@
1
1
  // Unit: Room behavior — two in-memory peers see each other, debounced flush
2
2
  // fires, flush is idempotent, room teardown is clean.
3
3
 
4
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
4
+ import { describe, expect, test } from 'bun:test';
5
5
 
6
6
  import { Awareness } from 'y-protocols/awareness';
7
7
  import * as Y from 'yjs';
8
8
 
9
- import {
10
- encodeAwarenessFrame,
11
- encodeHandshake,
12
- encodeSyncUpdate,
13
- handleMessage,
14
- } from '../collab/protocol.ts';
9
+ import { encodeAwarenessFrame, handleMessage } from '../collab/protocol.ts';
15
10
  import { createRoom, type RoomCallbacks, type RoomConn } from '../collab/room.ts';
16
11
 
17
12
  function makeConn(id: string): RoomConn & { recv: Uint8Array[] } {