@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,184 @@
1
+ // annotate-write — FigJam v3 AI write verb (`maude design annotate`).
2
+ // Drives bin/annotate.mjs as a subprocess against a temp design root and
3
+ // asserts the output through the CANONICAL parser: everything the verb writes
4
+ // must be sanitize-stable, byte-identical under parse → re-serialize, and
5
+ // readable back as a graph by read-annotations --graph.
6
+
7
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
8
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { GlobalRegistrator } from '@happy-dom/global-registrator';
12
+
13
+ import {
14
+ type ArrowStroke,
15
+ type StickyStroke,
16
+ type Stroke,
17
+ sanitizeAnnotationSvg,
18
+ strokesToSvg,
19
+ svgToStrokes,
20
+ } from '../annotations-model.ts';
21
+
22
+ const BIN = new URL('../bin/annotate.mjs', import.meta.url).pathname;
23
+ const READER = new URL('../bin/read-annotations.mjs', import.meta.url).pathname;
24
+
25
+ let root: string;
26
+
27
+ beforeAll(() => {
28
+ GlobalRegistrator.register();
29
+ root = mkdtempSync(join(tmpdir(), 'annotate-test-'));
30
+ // A design root with no config — resolveDesignRoot defaults to .design.
31
+ writeFileSync(
32
+ join(root, 'flow.json'),
33
+ JSON.stringify({
34
+ nodes: [
35
+ { id: 'a', label: 'Start' },
36
+ { id: 'b', label: 'Middle' },
37
+ { id: 'c', label: 'End', shape: 'ellipse' },
38
+ ],
39
+ edges: [
40
+ { from: 'a', to: 'b' },
41
+ { from: 'b', to: 'c', label: 'ships' },
42
+ ],
43
+ })
44
+ );
45
+ Bun.spawnSync(['mkdir', '-p', join(root, '.design')]);
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await GlobalRegistrator.unregister();
50
+ rmSync(root, { recursive: true, force: true });
51
+ });
52
+
53
+ function runAnnotate(args: string[], stdin?: string): { code: number; out: string; err: string } {
54
+ const proc = Bun.spawnSync(['bun', BIN, ...args], {
55
+ stdin: stdin ? new TextEncoder().encode(stdin) : undefined,
56
+ });
57
+ return {
58
+ code: proc.exitCode,
59
+ out: new TextDecoder().decode(proc.stdout),
60
+ err: new TextDecoder().decode(proc.stderr),
61
+ };
62
+ }
63
+
64
+ function readSvg(): string {
65
+ return readFileSync(join(root, '.design', 'ui-flow.annotations.svg'), 'utf8');
66
+ }
67
+
68
+ describe('annotate --flow', () => {
69
+ test('writes an auto-laid-out diagram of bound connectors', () => {
70
+ const res = runAnnotate(['ui/Flow.tsx', '--flow', join(root, 'flow.json'), '--root', root]);
71
+ expect(res.code).toBe(0);
72
+ const result = JSON.parse(res.out) as { ok: boolean; refs: Record<string, string> };
73
+ expect(result.ok).toBe(true);
74
+ expect(Object.keys(result.refs)).toEqual(['@a', '@b', '@c']);
75
+
76
+ const svg = readSvg();
77
+ // Canonical-parser compatible, sanitize-stable, byte-identical round-trip.
78
+ expect(sanitizeAnnotationSvg(svg)).toBe(svg);
79
+ const strokes = svgToStrokes(svg);
80
+ expect(strokesToSvg(strokes)).toBe(svg);
81
+ // 3 shapes + 3 labels (2 anchored + 1 ellipse-anchored) + 2 arrows + 1 edge label.
82
+ const arrows = strokes.filter((s): s is ArrowStroke => s.tool === 'arrow');
83
+ expect(arrows).toHaveLength(2);
84
+ for (const a of arrows) {
85
+ expect(a.startBind?.hostId).toBeDefined();
86
+ expect(a.endBind?.hostId).toBeDefined();
87
+ expect(a.author).toBe('ai');
88
+ }
89
+ // Layered layout: 'b' sits one column right of 'a'.
90
+ const aShape = strokes.find((s) => s.id === result.refs['@a']) as Stroke & { x: number };
91
+ const bShape = strokes.find((s) => s.id === result.refs['@b']) as Stroke & { x: number };
92
+ expect(bShape.x).toBeGreaterThan(aShape.x);
93
+ });
94
+
95
+ test('read-annotations --graph reads the diagram back as the same graph', () => {
96
+ const proc = Bun.spawnSync(['node', READER, 'ui/Flow.tsx', '--root', root, '--graph']);
97
+ const parsed = JSON.parse(new TextDecoder().decode(proc.stdout)) as {
98
+ graph: { nodes: Array<{ label: string | null }>; edges: unknown[] };
99
+ };
100
+ expect(parsed.graph.edges).toHaveLength(2);
101
+ expect(parsed.graph.nodes.map((n) => n.label).sort()).toEqual(['End', 'Middle', 'Start']);
102
+ });
103
+ });
104
+
105
+ describe('annotate --ops', () => {
106
+ test('create sticky + group + connect-to-existing + delete, via stdin', () => {
107
+ // Connect to a node minted by the previous --flow run.
108
+ const flowSvg = readSvg();
109
+ const existingId = (svgToStrokes(flowSvg).find((s) => s.tool === 'rect') as Stroke).id;
110
+ const ops = {
111
+ ops: [
112
+ { op: 'create', type: 'sticky', ref: '@n1', text: 'pain point', x: 100, y: 600 },
113
+ { op: 'create', type: 'sticky', ref: '@n2', text: 'idea', x: 340, y: 600 },
114
+ { op: 'group', ids: ['@n1', '@n2'] },
115
+ { op: 'connect', from: '@n1', to: existingId },
116
+ ],
117
+ };
118
+ const res = runAnnotate(['ui/Flow.tsx', '--root', root], JSON.stringify(ops));
119
+ expect(res.code).toBe(0);
120
+ const svg = readSvg();
121
+ expect(sanitizeAnnotationSvg(svg)).toBe(svg);
122
+ const strokes = svgToStrokes(svg);
123
+ expect(strokesToSvg(svg && strokes ? strokes : [])).toBe(svg);
124
+ const stickies = strokes.filter((s): s is StickyStroke => s.tool === 'sticky');
125
+ expect(stickies).toHaveLength(2);
126
+ expect(stickies[0]?.groupIds).toEqual(stickies[1]?.groupIds);
127
+ expect(stickies[0]?.groupIds?.length).toBe(1);
128
+ const bound = strokes.filter(
129
+ (s): s is ArrowStroke => s.tool === 'arrow' && s.endBind?.hostId === existingId
130
+ );
131
+ expect(bound).toHaveLength(1);
132
+
133
+ // Delete one sticky — the element disappears and the file stays canonical.
134
+ const del = runAnnotate(
135
+ ['ui/Flow.tsx', '--root', root],
136
+ JSON.stringify({ ops: [{ op: 'delete', id: stickies[1]?.id }] })
137
+ );
138
+ expect(del.code).toBe(0);
139
+ const after = svgToStrokes(readSvg());
140
+ expect(after.filter((s) => s.tool === 'sticky')).toHaveLength(1);
141
+ expect(strokesToSvg(after)).toBe(readSvg());
142
+ });
143
+
144
+ test('unknown connect target fails loud, writes nothing', () => {
145
+ const before = readSvg();
146
+ const res = runAnnotate(
147
+ ['ui/Flow.tsx', '--root', root],
148
+ JSON.stringify({ ops: [{ op: 'connect', from: '@ghost', to: 'nope' }] })
149
+ );
150
+ expect(res.code).toBe(2);
151
+ expect(readSvg()).toBe(before);
152
+ });
153
+
154
+ test('--dry-run prints the merged SVG without writing', () => {
155
+ const before = readSvg();
156
+ const res = runAnnotate(
157
+ ['ui/Flow.tsx', '--root', root, '--dry-run'],
158
+ JSON.stringify({ ops: [{ op: 'create', type: 'text', text: 'ghost', x: 0, y: 0 }] })
159
+ );
160
+ expect(res.code).toBe(0);
161
+ expect(res.out).toContain('ghost');
162
+ expect(readSvg()).toBe(before);
163
+ });
164
+
165
+ test('non-loopback _server.json.url is refused — writes to file, never PUTs off-box (F2)', () => {
166
+ // A poisoned/foreign _server.json points the egress at an external host.
167
+ // The verb must NOT PUT there (SSRF/exfil); it falls back to the file write.
168
+ const serverJson = join(root, '.design', '_server.json');
169
+ writeFileSync(serverJson, JSON.stringify({ url: 'http://attacker.example.com:4399' }));
170
+ try {
171
+ const res = runAnnotate(
172
+ ['ui/F2.tsx', '--root', root],
173
+ JSON.stringify({ ops: [{ op: 'create', type: 'text', text: 'secret', x: 0, y: 0 }] })
174
+ );
175
+ expect(res.code).toBe(0);
176
+ // via:"file" proves the off-box PUT was skipped (a successful PUT → "server").
177
+ expect(JSON.parse(res.out).via).toBe('file');
178
+ const svg = readFileSync(join(root, '.design', 'ui-f2.annotations.svg'), 'utf8');
179
+ expect(svg).toContain('secret');
180
+ } finally {
181
+ rmSync(serverJson, { force: true });
182
+ }
183
+ });
184
+ });
@@ -0,0 +1,88 @@
1
+ // annotations-align — FigJam v3 align + distribute (pure bbox math, DOM-free).
2
+
3
+ import { describe, expect, test } from 'bun:test';
4
+
5
+ import { alignStrokes, distributeStrokes } from '../annotations-align.ts';
6
+ import type { RectStroke, Stroke } from '../annotations-model.ts';
7
+
8
+ function rect(
9
+ id: string,
10
+ x: number,
11
+ y: number,
12
+ w = 50,
13
+ h = 50,
14
+ extra: Partial<RectStroke> = {}
15
+ ): RectStroke {
16
+ return { id, tool: 'rect', color: '#1f1f1f', width: 3, x, y, w, h, ...extra };
17
+ }
18
+
19
+ const byId = (strokes: readonly Stroke[], id: string): RectStroke =>
20
+ strokes.find((s) => s.id === id) as RectStroke;
21
+
22
+ describe('alignStrokes', () => {
23
+ test('align left snaps every unit to the selection bbox left edge', () => {
24
+ const strokes = [rect('a', 0, 0), rect('b', 100, 80), rect('c', 40, 200)];
25
+ const out = alignStrokes(strokes, ['a', 'b', 'c'], 'left');
26
+ expect(byId(out, 'a').x).toBe(0);
27
+ expect(byId(out, 'b').x).toBe(0);
28
+ expect(byId(out, 'c').x).toBe(0);
29
+ // y untouched by a horizontal align
30
+ expect(byId(out, 'b').y).toBe(80);
31
+ });
32
+
33
+ test('align h-center / right / bottom', () => {
34
+ const strokes = [rect('a', 0, 0, 100, 100), rect('b', 200, 50, 20, 20)];
35
+ const centered = alignStrokes(strokes, ['a', 'b'], 'h-center');
36
+ // Selection spans x:0..220 → center 110; b (w20) → x=100.
37
+ expect(byId(centered, 'b').x).toBe(100);
38
+ const right = alignStrokes(strokes, ['a', 'b'], 'right');
39
+ expect(byId(right, 'a').x).toBe(120);
40
+ const bottom = alignStrokes(strokes, ['a', 'b'], 'bottom');
41
+ expect(byId(bottom, 'b').y).toBe(80);
42
+ });
43
+
44
+ test('a group aligns as ONE unit — members keep their relative offsets', () => {
45
+ const strokes = [
46
+ rect('g1', 100, 0, 50, 50, { groupIds: ['g'] }),
47
+ rect('g2', 160, 30, 50, 50, { groupIds: ['g'] }),
48
+ rect('solo', 0, 200),
49
+ ];
50
+ const out = alignStrokes(strokes, ['g1', 'solo'], 'left');
51
+ // Group bbox spans x:100..210; aligning left moves the group by -100.
52
+ expect(byId(out, 'g1').x).toBe(0);
53
+ expect(byId(out, 'g2').x).toBe(60);
54
+ expect(byId(out, 'solo').x).toBe(0);
55
+ });
56
+
57
+ test('fewer than two units → referential no-op', () => {
58
+ const strokes = [
59
+ rect('a', 0, 0),
60
+ rect('b', 10, 10, 50, 50, { groupIds: ['g'] }),
61
+ rect('c', 70, 10, 50, 50, { groupIds: ['g'] }),
62
+ ];
63
+ expect(alignStrokes(strokes, ['b'], 'left')).toBe(strokes);
64
+ });
65
+ });
66
+
67
+ describe('distributeStrokes', () => {
68
+ test('equalizes gaps, pinning the first and last unit', () => {
69
+ // a: 0..50, b: 60..110, c: 200..250 → span 250, total 150, gap = 50.
70
+ const strokes = [rect('a', 0, 0), rect('b', 60, 0), rect('c', 200, 0)];
71
+ const out = distributeStrokes(strokes, ['a', 'b', 'c'], 'h');
72
+ expect(byId(out, 'a').x).toBe(0);
73
+ expect(byId(out, 'b').x).toBe(100);
74
+ expect(byId(out, 'c').x).toBe(200);
75
+ });
76
+
77
+ test('vertical distribute', () => {
78
+ // a: 0..50, b: 55..105, c: 300..350 → span 350, total 150, gap = 100.
79
+ const strokes = [rect('a', 0, 0), rect('b', 0, 55), rect('c', 0, 300)];
80
+ const out = distributeStrokes(strokes, ['a', 'b', 'c'], 'v');
81
+ expect(byId(out, 'b').y).toBe(150);
82
+ });
83
+
84
+ test('fewer than three units → referential no-op', () => {
85
+ const strokes = [rect('a', 0, 0), rect('b', 100, 0)];
86
+ expect(distributeStrokes(strokes, ['a', 'b'], 'h')).toBe(strokes);
87
+ });
88
+ });
@@ -0,0 +1,124 @@
1
+ // annotations-bindings — FigJam v3 connector binding (pure, DOM-free).
2
+ // Semantics: Excalidraw proximity bind + FigJam side/center magnets; bound
3
+ // endpoints derive from the host; deleting the host unbinds + freezes.
4
+
5
+ import { describe, expect, test } from 'bun:test';
6
+
7
+ import {
8
+ anchorPoint,
9
+ BIND_THRESHOLD_PX,
10
+ bindCandidate,
11
+ facingAnchor,
12
+ isBindable,
13
+ recomputeBoundArrows,
14
+ } from '../annotations-bindings.ts';
15
+ import type { ArrowStroke, RectStroke, Stroke, TextStroke } from '../annotations-model.ts';
16
+
17
+ function rect(id: string, x = 0, y = 0, w = 100, h = 80): RectStroke {
18
+ return { id, tool: 'rect', color: '#1f1f1f', width: 3, x, y, w, h };
19
+ }
20
+ function arrow(id: string, extra: Partial<ArrowStroke> = {}): ArrowStroke {
21
+ return { id, tool: 'arrow', color: '#1f1f1f', width: 3, x1: 0, y1: 0, x2: 50, y2: 0, ...extra };
22
+ }
23
+
24
+ describe('bindCandidate', () => {
25
+ test('binds within the threshold and snaps to the nearest magnet', () => {
26
+ const strokes: Stroke[] = [rect('host', 100, 100, 100, 100)];
27
+ // Just right of the host's right edge, vertically centered → (1, 0.5).
28
+ const bind = bindCandidate(205, 150, strokes, BIND_THRESHOLD_PX);
29
+ expect(bind).toEqual({ hostId: 'host', nx: 1, ny: 0.5 });
30
+ // Top-left corner region → (0, 0).
31
+ expect(bindCandidate(98, 102, strokes, BIND_THRESHOLD_PX)).toEqual({
32
+ hostId: 'host',
33
+ nx: 0,
34
+ ny: 0,
35
+ });
36
+ });
37
+
38
+ test('misses outside the threshold; topmost host wins; exclusions respected', () => {
39
+ const below = rect('below', 0, 0, 100, 100);
40
+ const above = rect('above', 50, 50, 100, 100);
41
+ const strokes: Stroke[] = [below, above];
42
+ expect(bindCandidate(500, 500, strokes, BIND_THRESHOLD_PX)).toBeNull();
43
+ expect(bindCandidate(75, 75, strokes, BIND_THRESHOLD_PX)?.hostId).toBe('above');
44
+ expect(bindCandidate(75, 75, strokes, BIND_THRESHOLD_PX, new Set(['above']))?.hostId).toBe(
45
+ 'below'
46
+ );
47
+ });
48
+
49
+ test('only bindable tools host (no text/pen/arrow/link)', () => {
50
+ const label: TextStroke = {
51
+ id: 't',
52
+ tool: 'text',
53
+ color: '#1a1a1a',
54
+ fontSize: 14,
55
+ text: 'x',
56
+ x: 0,
57
+ y: 0,
58
+ };
59
+ expect(isBindable(label)).toBe(false);
60
+ expect(bindCandidate(0, 0, [label], BIND_THRESHOLD_PX)).toBeNull();
61
+ expect(isBindable(rect('r'))).toBe(true);
62
+ });
63
+ });
64
+
65
+ describe('anchorPoint / recomputeBoundArrows', () => {
66
+ test('bound endpoints follow the host on move (translate → recompute)', () => {
67
+ const host = rect('host', 0, 0, 100, 100);
68
+ const ar = arrow('ar', {
69
+ x1: 100,
70
+ y1: 50,
71
+ x2: 300,
72
+ y2: 50,
73
+ startBind: { hostId: 'host', nx: 1, ny: 0.5 },
74
+ });
75
+ const moved: Stroke[] = [{ ...host, x: 50, y: 20 }, ar];
76
+ const out = recomputeBoundArrows(moved);
77
+ const a = out.find((s) => s.id === 'ar') as ArrowStroke;
78
+ expect([a.x1, a.y1]).toEqual([150, 70]);
79
+ expect([a.x2, a.y2]).toEqual([300, 50]); // free end untouched
80
+ });
81
+
82
+ test('anchor stays proportional through a host resize', () => {
83
+ const host = rect('host', 0, 0, 200, 100);
84
+ expect(anchorPoint(host, 0.5, 1)).toEqual([100, 100]);
85
+ expect(anchorPoint({ ...host, w: 400 }, 0.5, 1)).toEqual([200, 100]);
86
+ });
87
+
88
+ test('missing host → bind stripped, endpoint frozen (arrow survives)', () => {
89
+ const ar = arrow('ar', {
90
+ x1: 100,
91
+ y1: 50,
92
+ x2: 300,
93
+ y2: 50,
94
+ startBind: { hostId: 'gone', nx: 1, ny: 0.5 },
95
+ });
96
+ const out = recomputeBoundArrows([ar]);
97
+ const a = out.find((s) => s.id === 'ar') as ArrowStroke;
98
+ expect(a.startBind).toBeUndefined();
99
+ expect([a.x1, a.y1]).toEqual([100, 50]);
100
+ });
101
+
102
+ test('referential no-op when every endpoint already sits at its anchor', () => {
103
+ const host = rect('host', 0, 0, 100, 100);
104
+ const ar = arrow('ar', {
105
+ x1: 100,
106
+ y1: 50,
107
+ x2: 300,
108
+ y2: 50,
109
+ startBind: { hostId: 'host', nx: 1, ny: 0.5 },
110
+ });
111
+ const strokes: Stroke[] = [host, ar];
112
+ expect(recomputeBoundArrows(strokes)).toBe(strokes);
113
+ });
114
+ });
115
+
116
+ describe('facingAnchor (AI annotate / quick-create side pick)', () => {
117
+ test('picks the side magnet facing the target', () => {
118
+ const host = rect('h', 0, 0, 100, 100);
119
+ expect(facingAnchor(host, 500, 50)).toEqual({ hostId: 'h', nx: 1, ny: 0.5 });
120
+ expect(facingAnchor(host, -400, 50)).toEqual({ hostId: 'h', nx: 0, ny: 0.5 });
121
+ expect(facingAnchor(host, 50, 900)).toEqual({ hostId: 'h', nx: 0.5, ny: 1 });
122
+ expect(facingAnchor(host, 50, -900)).toEqual({ hostId: 'h', nx: 0.5, ny: 0 });
123
+ });
124
+ });
@@ -0,0 +1,231 @@
1
+ // annotations-groups — FigJam v3 grouping / duplicate / z-order (pure, DOM-free).
2
+ // Group model: flat `groupIds` tag-array, deepest→shallowest, outermost = last.
3
+
4
+ import { describe, expect, test } from 'bun:test';
5
+
6
+ import {
7
+ duplicateStrokes,
8
+ expandIdsToGroups,
9
+ groupStrokes,
10
+ normalizeGroups,
11
+ outermostGroupOf,
12
+ reorderStrokes,
13
+ ungroupStrokes,
14
+ } from '../annotations-groups.ts';
15
+ import type {
16
+ ArrowStroke,
17
+ PenStroke,
18
+ RectStroke,
19
+ Stroke,
20
+ TextStroke,
21
+ } from '../annotations-model.ts';
22
+
23
+ function rect(id: string, x = 0, extra: Partial<RectStroke> = {}): RectStroke {
24
+ return { id, tool: 'rect', color: '#1f1f1f', width: 3, x, y: 0, w: 100, h: 80, ...extra };
25
+ }
26
+ function pen(id: string, extra: Partial<PenStroke> = {}): PenStroke {
27
+ return {
28
+ id,
29
+ tool: 'pen',
30
+ color: '#e5484d',
31
+ width: 3,
32
+ points: [
33
+ [0, 0],
34
+ [10, 10],
35
+ ],
36
+ ...extra,
37
+ };
38
+ }
39
+ function arrow(id: string, extra: Partial<ArrowStroke> = {}): ArrowStroke {
40
+ return { id, tool: 'arrow', color: '#1f1f1f', width: 3, x1: 0, y1: 0, x2: 50, y2: 0, ...extra };
41
+ }
42
+
43
+ describe('outermostGroupOf / expandIdsToGroups', () => {
44
+ test('outermost group is the LAST groupIds element (deepest→shallowest)', () => {
45
+ expect(outermostGroupOf(rect('a', 0, { groupIds: ['inner', 'outer'] }))).toBe('outer');
46
+ expect(outermostGroupOf(rect('a'))).toBeNull();
47
+ });
48
+
49
+ test('expanding one member id selects the whole outermost group, in z order', () => {
50
+ const strokes: Stroke[] = [
51
+ rect('a', 0, { groupIds: ['g1'] }),
52
+ pen('loose'),
53
+ rect('b', 200, { groupIds: ['g1'] }),
54
+ ];
55
+ expect(expandIdsToGroups(['b'], strokes)).toEqual(['a', 'b']);
56
+ });
57
+
58
+ test('ungrouped ids pass through; mixed selection keeps both', () => {
59
+ const strokes: Stroke[] = [
60
+ rect('a', 0, { groupIds: ['g1'] }),
61
+ pen('loose'),
62
+ rect('b', 200, { groupIds: ['g1'] }),
63
+ ];
64
+ expect(expandIdsToGroups(['loose', 'a'], strokes)).toEqual(['a', 'loose', 'b']);
65
+ });
66
+ });
67
+
68
+ describe('groupStrokes', () => {
69
+ test('groups ≥2 strokes under a fresh outermost id, contiguous at the topmost member', () => {
70
+ const strokes: Stroke[] = [rect('a'), pen('mid'), rect('b', 200)];
71
+ const res = groupStrokes(strokes, ['a', 'b'], 'gNew');
72
+ expect(res).not.toBeNull();
73
+ if (!res) return;
74
+ // Block lands where the topmost member ('b') sat — after 'mid'.
75
+ expect(res.strokes.map((s) => s.id)).toEqual(['mid', 'a', 'b']);
76
+ expect(
77
+ res.strokes.filter((s) => s.id !== 'mid').every((s) => outermostGroupOf(s) === 'gNew')
78
+ ).toBe(true);
79
+ expect(res.memberIds).toEqual(['a', 'b']);
80
+ });
81
+
82
+ test('grouping a group with a loose stroke NESTS (old id stays deeper)', () => {
83
+ const strokes: Stroke[] = [
84
+ rect('a', 0, { groupIds: ['g1'] }),
85
+ rect('b', 100, { groupIds: ['g1'] }),
86
+ pen('c'),
87
+ ];
88
+ const res = groupStrokes(strokes, ['c', 'a'], 'g2');
89
+ expect(res).not.toBeNull();
90
+ if (!res) return;
91
+ const a = res.strokes.find((s) => s.id === 'a');
92
+ expect(a?.groupIds).toEqual(['g1', 'g2']);
93
+ const c = res.strokes.find((s) => s.id === 'c');
94
+ expect(c?.groupIds).toEqual(['g2']);
95
+ });
96
+
97
+ test('fewer than two resolved members → null', () => {
98
+ expect(groupStrokes([rect('a')], ['a'])).toBeNull();
99
+ expect(groupStrokes([rect('a')], ['a', 'ghost'])).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe('ungroupStrokes / normalizeGroups', () => {
104
+ test('ungroup strips ONE level (the outermost) and drops the key when empty', () => {
105
+ const strokes: Stroke[] = [
106
+ rect('a', 0, { groupIds: ['g1', 'g2'] }),
107
+ rect('b', 100, { groupIds: ['g1', 'g2'] }),
108
+ ];
109
+ const out = ungroupStrokes(strokes, ['a']);
110
+ expect(out.map((s) => s.groupIds)).toEqual([['g1'], ['g1']]);
111
+ const out2 = ungroupStrokes(out, ['b']);
112
+ for (const s of out2) expect('groupIds' in s).toBe(false);
113
+ });
114
+
115
+ test('normalizeGroups dissolves singleton groups after a delete', () => {
116
+ const strokes: Stroke[] = [rect('a', 0, { groupIds: ['g1'] }), pen('loose')];
117
+ const out = normalizeGroups(strokes);
118
+ expect('groupIds' in (out[0] as Stroke)).toBe(false);
119
+ // Referential no-op when nothing changes.
120
+ const stable: Stroke[] = [rect('x', 0, { groupIds: ['g'] }), rect('y', 1, { groupIds: ['g'] })];
121
+ expect(normalizeGroups(stable)).toBe(stable);
122
+ });
123
+ });
124
+
125
+ describe('duplicateStrokes', () => {
126
+ test('clones with fresh ids, remapped group ids, offset applied', () => {
127
+ let n = 0;
128
+ const makeId = () => `dup${++n}`;
129
+ const strokes: Stroke[] = [
130
+ rect('a', 0, { groupIds: ['g1'] }),
131
+ rect('b', 100, { groupIds: ['g1'] }),
132
+ ];
133
+ const res = duplicateStrokes(strokes, ['a'], 16, 16, makeId, () => 'gDup');
134
+ expect(res.strokes).toHaveLength(4);
135
+ expect(res.newIds).toEqual(['dup1', 'dup2']);
136
+ const dup1 = res.strokes.find((s) => s.id === 'dup1') as RectStroke;
137
+ expect(dup1.x).toBe(16);
138
+ expect(dup1.groupIds).toEqual(['gDup']);
139
+ // Originals untouched.
140
+ expect((res.strokes.find((s) => s.id === 'a') as RectStroke).x).toBe(0);
141
+ });
142
+
143
+ test('anchored text travels with its duplicated host and is NOT selectable', () => {
144
+ const host = rect('host');
145
+ const label: TextStroke = {
146
+ id: 'label',
147
+ tool: 'text',
148
+ color: '#1a1a1a',
149
+ fontSize: 14,
150
+ text: 'hi',
151
+ anchorId: 'host',
152
+ };
153
+ let n = 0;
154
+ const res = duplicateStrokes([host, label], ['host'], 10, 0, () => `d${++n}`);
155
+ expect(res.strokes).toHaveLength(4);
156
+ const dupLabel = res.strokes.find((s) => s.tool === 'text' && s.id !== 'label') as TextStroke;
157
+ expect(dupLabel.anchorId).toBe('d1'); // remapped to the cloned host
158
+ expect(res.newIds).toEqual(['d1']); // anchored-text clone excluded
159
+ });
160
+
161
+ test('arrow binds remap to co-duplicated hosts; binds to outside hosts are kept', () => {
162
+ const a = rect('a');
163
+ const b = rect('b', 300);
164
+ const ar = arrow('ar', {
165
+ startBind: { hostId: 'a', nx: 1, ny: 0.5 },
166
+ endBind: { hostId: 'b', nx: 0, ny: 0.5 },
167
+ });
168
+ let n = 0;
169
+ const res = duplicateStrokes([a, b, ar], ['a', 'ar'], 0, 0, () => `d${++n}`);
170
+ const dupArrow = res.strokes.find((s) => s.tool === 'arrow' && s.id !== 'ar') as ArrowStroke;
171
+ expect(dupArrow.startBind?.hostId).toBe('d1'); // co-duplicated → remapped
172
+ expect(dupArrow.endBind?.hostId).toBe('b'); // outside host → kept
173
+ });
174
+ });
175
+
176
+ describe('reorderStrokes (z-order, group-contiguous units)', () => {
177
+ const base = (): Stroke[] => [
178
+ rect('a'),
179
+ rect('g1a', 100, { groupIds: ['g1'] }),
180
+ rect('g1b', 200, { groupIds: ['g1'] }),
181
+ pen('top'),
182
+ ];
183
+
184
+ test('bring to front / send to back move the unit as one block', () => {
185
+ expect(reorderStrokes(base(), ['a'], 'front').map((s) => s.id)).toEqual([
186
+ 'g1a',
187
+ 'g1b',
188
+ 'top',
189
+ 'a',
190
+ ]);
191
+ expect(reorderStrokes(base(), ['top'], 'back').map((s) => s.id)).toEqual([
192
+ 'top',
193
+ 'a',
194
+ 'g1a',
195
+ 'g1b',
196
+ ]);
197
+ });
198
+
199
+ test('forward/backward hop over a WHOLE neighbouring group', () => {
200
+ expect(reorderStrokes(base(), ['a'], 'forward').map((s) => s.id)).toEqual([
201
+ 'g1a',
202
+ 'g1b',
203
+ 'a',
204
+ 'top',
205
+ ]);
206
+ expect(reorderStrokes(base(), ['top'], 'backward').map((s) => s.id)).toEqual([
207
+ 'a',
208
+ 'top',
209
+ 'g1a',
210
+ 'g1b',
211
+ ]);
212
+ });
213
+
214
+ test('selecting one group member moves the whole group', () => {
215
+ expect(reorderStrokes(base(), ['g1b'], 'front').map((s) => s.id)).toEqual([
216
+ 'a',
217
+ 'top',
218
+ 'g1a',
219
+ 'g1b',
220
+ ]);
221
+ });
222
+
223
+ test('already at the boundary is a no-op', () => {
224
+ expect(reorderStrokes(base(), ['top'], 'forward').map((s) => s.id)).toEqual([
225
+ 'a',
226
+ 'g1a',
227
+ 'g1b',
228
+ 'top',
229
+ ]);
230
+ });
231
+ });