@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
package/README.md CHANGED
@@ -106,7 +106,7 @@ Plugin slash-commands reach all executable logic through the on-PATH `maude` bin
106
106
  Two clean paths, no middle ground ([DDR-047](.ai/decisions/DDR-047-collab-scope-cut-no-lan-mode-hub-admin-ui.md)):
107
107
 
108
108
  - **v1.0 — git handoff OR loopback multi-tab.** Push / pull is the cross-machine story. On a single machine, two browser tabs (or two Claude Code instances editing the same repo) sync cursors, comments, and annotations live over loopback WebSocket. The dev server refuses any non-loopback `host` header on the collab WS endpoint.
109
- - **v1.1 — deploy a hub** (Phase 9, in-flight). Cross-machine live collab needs a hub binary you deploy yourself. No tunnel mode; no shared cloud.
109
+ - **v1.1 — deploy a hub** (Phase 9, in-flight). Cross-machine live collab needs a hub binary you deploy yourself. No tunnel mode; no shared cloud. **Boot order can't eat your work** ([DDR-102](.ai/decisions/DDR-102-cold-start-divergence-resolution.md)): a per-machine journal tells clean catch-ups apart from genuine divergence; diverged canvases snapshot **both** versions to `_history/` before the newer one wins, so the loser is one `/design:rollback` away — and `maude design status` reports per-canvas sync state honestly (synced / pending / auth-rejected).
110
110
 
111
111
  ## Security
112
112
 
@@ -129,8 +129,8 @@ The repo is a **pnpm workspace monorepo** with one published npm package (`@1agh
129
129
  | --------- | ------- |
130
130
  | `.` (root) | The single npm publisher — CLI, dev-server entry, plugin templates that ship to npm. |
131
131
  | `site/` | Docs site — Fumadocs + Next.js, deployed to Vercel ([DDR-005](.ai/decisions/DDR-005-docs-site-stack-and-hosting.md)). |
132
- | `plugins/design/dev-server/` | Zero-dep Node dev server + browser client. Bundled output (`dist/`) is the only thing in the npm tarball. |
133
- | `plugins/design/hub/` | Reserved for the v1.1 federated hub (Phase 9). |
132
+ | `apps/studio/` | Zero-dep Node dev server + browser client. Bundled output (`dist/`) is the only thing in the npm tarball. |
133
+ | `apps/hub/` | Reserved for the v1.1 federated hub (Phase 9). |
134
134
 
135
135
  Common scripts at the root:
136
136
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file ai-banner.tsx — yellow "Claude is editing this canvas" banner
3
- * @scope plugins/design/dev-server/ai-banner.tsx
3
+ * @scope apps/studio/ai-banner.tsx
4
4
  * @purpose Mounted by canvas-shell. Subscribes to the inspector WS
5
5
  * `ai-activity` event for the canvas's own file path; shows a
6
6
  * non-modal yellow banner while AI work is in progress and clears
@@ -183,7 +183,6 @@ export function AiBanner(): JSX.Element | null {
183
183
 
184
184
  if (!entry) return null;
185
185
  return (
186
- // biome-ignore lint/a11y/useSemanticElements: <output> is a form-result element; role="status" on a div is the canonical live-region banner pattern (APG).
187
186
  <div className="dc-ai-banner" role="status" aria-live="polite">
188
187
  <span className="dc-ai-banner__dot" aria-hidden="true" />
189
188
  <span>{entry.author} is editing this canvas — your changes may conflict</span>
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @file annotations-align.ts — FigJam v3 align + distribute
3
+ * @scope apps/studio/annotations-align.ts
4
+ * @purpose Pure bbox math (no React, no DOM) for the multi-select Align
5
+ * cluster. Units follow FigJam: a whole group aligns as ONE box
6
+ * (its members translate together), an ungrouped stroke is its own
7
+ * unit. Align needs ≥ 2 units, distribute ≥ 3 (first/last pinned,
8
+ * gaps equalized — Figma "distribute spacing" semantics).
9
+ */
10
+
11
+ import { expandIdsToGroups, outermostGroupOf } from './annotations-groups.ts';
12
+ import { type AnchorHost, type Stroke, strokeBBox, translateOne } from './annotations-model.ts';
13
+
14
+ export type AlignEdge = 'left' | 'h-center' | 'right' | 'top' | 'v-center' | 'bottom';
15
+ export type DistributeAxis = 'h' | 'v';
16
+
17
+ interface BBox {
18
+ x: number;
19
+ y: number;
20
+ w: number;
21
+ h: number;
22
+ }
23
+
24
+ interface AlignUnit {
25
+ ids: Set<string>;
26
+ bbox: BBox;
27
+ }
28
+
29
+ function unionBox(a: BBox, b: BBox): BBox {
30
+ const x = Math.min(a.x, b.x);
31
+ const y = Math.min(a.y, b.y);
32
+ return {
33
+ x,
34
+ y,
35
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
36
+ h: Math.max(a.y + a.h, b.y + b.h) - y,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Partition the (expanded) selection into alignment units. Anchored text is
42
+ * skipped (it has no independent bbox and rides its host via `translateOne`'s
43
+ * no-op); arrows participate — bound endpoints get re-pinned by the caller's
44
+ * `recomputeBoundArrows` pass after the translate.
45
+ */
46
+ function unitsOf(strokes: readonly Stroke[], ids: readonly string[]): AlignUnit[] {
47
+ const anchors = new Map<string, AnchorHost>();
48
+ for (const s of strokes) {
49
+ if (s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon') anchors.set(s.id, s);
50
+ }
51
+ const sel = new Set(expandIdsToGroups(ids, strokes));
52
+ const byGroup = new Map<string, AlignUnit>();
53
+ const units: AlignUnit[] = [];
54
+ for (const s of strokes) {
55
+ if (!sel.has(s.id)) continue;
56
+ if (s.tool === 'text' && s.anchorId) continue; // rides its host
57
+ const bb = strokeBBox(s, anchors);
58
+ if (!bb) continue;
59
+ const g = outermostGroupOf(s);
60
+ if (g != null) {
61
+ const existing = byGroup.get(g);
62
+ if (existing) {
63
+ existing.ids.add(s.id);
64
+ existing.bbox = unionBox(existing.bbox, bb);
65
+ } else {
66
+ const unit: AlignUnit = { ids: new Set([s.id]), bbox: bb };
67
+ byGroup.set(g, unit);
68
+ units.push(unit);
69
+ }
70
+ } else {
71
+ units.push({ ids: new Set([s.id]), bbox: bb });
72
+ }
73
+ }
74
+ return units;
75
+ }
76
+
77
+ function applyDeltas(
78
+ strokes: readonly Stroke[],
79
+ deltas: Map<string, readonly [number, number]>
80
+ ): Stroke[] {
81
+ if (deltas.size === 0) return strokes as Stroke[];
82
+ return strokes.map((s) => {
83
+ const d = deltas.get(s.id);
84
+ return d ? translateOne(s, d[0], d[1]) : s;
85
+ });
86
+ }
87
+
88
+ /** Align every unit's edge/center to the selection's union bbox. */
89
+ export function alignStrokes(
90
+ strokes: readonly Stroke[],
91
+ ids: readonly string[],
92
+ edge: AlignEdge
93
+ ): Stroke[] {
94
+ const units = unitsOf(strokes, ids);
95
+ if (units.length < 2) return strokes as Stroke[];
96
+ let sel = units[0]?.bbox;
97
+ if (!sel) return strokes as Stroke[];
98
+ for (let i = 1; i < units.length; i++) {
99
+ const u = units[i];
100
+ if (u) sel = unionBox(sel, u.bbox);
101
+ }
102
+ const deltas = new Map<string, readonly [number, number]>();
103
+ for (const u of units) {
104
+ let dx = 0;
105
+ let dy = 0;
106
+ if (edge === 'left') dx = sel.x - u.bbox.x;
107
+ else if (edge === 'h-center') dx = sel.x + sel.w / 2 - (u.bbox.x + u.bbox.w / 2);
108
+ else if (edge === 'right') dx = sel.x + sel.w - (u.bbox.x + u.bbox.w);
109
+ else if (edge === 'top') dy = sel.y - u.bbox.y;
110
+ else if (edge === 'v-center') dy = sel.y + sel.h / 2 - (u.bbox.y + u.bbox.h / 2);
111
+ else dy = sel.y + sel.h - (u.bbox.y + u.bbox.h);
112
+ if (dx !== 0 || dy !== 0) {
113
+ for (const id of u.ids) deltas.set(id, [dx, dy] as const);
114
+ }
115
+ }
116
+ return applyDeltas(strokes, deltas);
117
+ }
118
+
119
+ /**
120
+ * Equalize the gaps between units along one axis. First and last unit (by
121
+ * leading edge) stay pinned; everything between shifts so every gap is equal.
122
+ */
123
+ export function distributeStrokes(
124
+ strokes: readonly Stroke[],
125
+ ids: readonly string[],
126
+ axis: DistributeAxis
127
+ ): Stroke[] {
128
+ const units = unitsOf(strokes, ids);
129
+ if (units.length < 3) return strokes as Stroke[];
130
+ const lead = (u: AlignUnit): number => (axis === 'h' ? u.bbox.x : u.bbox.y);
131
+ const size = (u: AlignUnit): number => (axis === 'h' ? u.bbox.w : u.bbox.h);
132
+ const sorted = [...units].sort((a, b) => lead(a) - lead(b));
133
+ const first = sorted[0];
134
+ const last = sorted[sorted.length - 1];
135
+ if (!first || !last) return strokes as Stroke[];
136
+ const span = lead(last) + size(last) - lead(first);
137
+ const total = sorted.reduce((m, u) => m + size(u), 0);
138
+ const gap = (span - total) / (sorted.length - 1);
139
+ const deltas = new Map<string, readonly [number, number]>();
140
+ let cursor = lead(first);
141
+ for (const u of sorted) {
142
+ const d = cursor - lead(u);
143
+ if (d !== 0) {
144
+ for (const id of u.ids) deltas.set(id, axis === 'h' ? ([d, 0] as const) : ([0, d] as const));
145
+ }
146
+ cursor += size(u) + gap;
147
+ }
148
+ return applyDeltas(strokes, deltas);
149
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @file annotations-bindings.ts — FigJam v3 connector binding
3
+ * @scope apps/studio/annotations-bindings.ts
4
+ * @purpose Pure helpers (no React, no DOM) for arrow ↔ shape magnetic
5
+ * binding. Semantics follow Excalidraw's binding system + FigJam's
6
+ * connector magnets: bind on proximity (~15 px at 100 % zoom,
7
+ * callers scale by 1/zoom), anchors snap to the {0, 0.5, 1} ×
8
+ * {0, 0.5, 1} magnet grid over the host bbox, bound endpoints are
9
+ * DERIVED from the host on every mutation (`recomputeBoundArrows`),
10
+ * and deleting a host strips the bind but keeps the arrow with the
11
+ * endpoint frozen in place (Excalidraw `fixBindingsAfterDeletion`).
12
+ */
13
+
14
+ import {
15
+ type ArrowBind,
16
+ type ArrowStroke,
17
+ rotatePoint,
18
+ type Stroke,
19
+ strokeBBox,
20
+ strokeRotation,
21
+ } from './annotations-model.ts';
22
+
23
+ /** Hosts an arrow endpoint can attach to (shapes + cards, FigJam set). */
24
+ export function isBindable(s: Stroke): boolean {
25
+ return (
26
+ s.tool === 'rect' ||
27
+ s.tool === 'ellipse' ||
28
+ s.tool === 'polygon' ||
29
+ s.tool === 'sticky' ||
30
+ s.tool === 'image'
31
+ );
32
+ }
33
+
34
+ /** Bind proximity threshold in world px at zoom 1 (Excalidraw's 15 px). */
35
+ export const BIND_THRESHOLD_PX = 15;
36
+
37
+ const MAGNETS = [0, 0.5, 1] as const;
38
+
39
+ function snapMagnet(v: number): number {
40
+ let best: number = MAGNETS[0];
41
+ let bestDist = Number.POSITIVE_INFINITY;
42
+ for (const m of MAGNETS) {
43
+ const d = Math.abs(v - m);
44
+ if (d < bestDist) {
45
+ bestDist = d;
46
+ best = m;
47
+ }
48
+ }
49
+ return best;
50
+ }
51
+
52
+ function clamp01(v: number): number {
53
+ return Math.max(0, Math.min(1, v));
54
+ }
55
+
56
+ /**
57
+ * Topmost bindable host within `threshold` of the world point, with the anchor
58
+ * snapped to the nearest side/center magnet. `excludeIds` keeps an arrow from
59
+ * binding to a host that is itself part of the current gesture.
60
+ */
61
+ export function bindCandidate(
62
+ wx: number,
63
+ wy: number,
64
+ strokes: readonly Stroke[],
65
+ threshold: number,
66
+ excludeIds?: ReadonlySet<string>
67
+ ): ArrowBind | null {
68
+ for (let i = strokes.length - 1; i >= 0; i--) {
69
+ const s = strokes[i];
70
+ if (!s || !isBindable(s) || excludeIds?.has(s.id)) continue;
71
+ const bb = strokeBBox(s);
72
+ if (!bb || bb.w <= 0 || bb.h <= 0) continue;
73
+ if (
74
+ wx < bb.x - threshold ||
75
+ wx > bb.x + bb.w + threshold ||
76
+ wy < bb.y - threshold ||
77
+ wy > bb.y + bb.h + threshold
78
+ ) {
79
+ continue;
80
+ }
81
+ return {
82
+ hostId: s.id,
83
+ nx: snapMagnet(clamp01((wx - bb.x) / bb.w)),
84
+ ny: snapMagnet(clamp01((wy - bb.y) / bb.h)),
85
+ };
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /** World position of a normalized anchor over the host's bbox. A rotated host
91
+ * carries its magnets with it (the anchor rotates around the bbox center). */
92
+ export function anchorPoint(host: Stroke, nx: number, ny: number): [number, number] | null {
93
+ const bb = strokeBBox(host);
94
+ if (!bb) return null;
95
+ const px = bb.x + nx * bb.w;
96
+ const py = bb.y + ny * bb.h;
97
+ const rot = strokeRotation(host);
98
+ if (rot === 0) return [px, py];
99
+ return rotatePoint(px, py, bb.x + bb.w / 2, bb.y + bb.h / 2, rot);
100
+ }
101
+
102
+ /**
103
+ * Re-derive every bound arrow endpoint from its host. Idempotent — an endpoint
104
+ * already at its anchor is left referentially untouched, and the ORIGINAL
105
+ * array is returned when nothing changed (so the drag-commit no-op checks keep
106
+ * working). A bind whose host no longer exists (or stopped being bindable) is
107
+ * stripped with the endpoint frozen in place — the arrow survives unbound.
108
+ *
109
+ * FigJam v3 auto-routing: an AUTO (non-pinned) bind re-picks the side magnet
110
+ * FACING the arrow's other end on every recompute, so a connector keeps a
111
+ * sensible direction as the shapes move around each other. A `pinned` bind
112
+ * (explicit user re-anchor) keeps its magnet.
113
+ */
114
+ export function recomputeBoundArrows(strokes: readonly Stroke[]): Stroke[] {
115
+ const byId = new Map<string, Stroke>();
116
+ for (const s of strokes) byId.set(s.id, s);
117
+ let mutated = false;
118
+ const out = strokes.map((s) => {
119
+ if (s.tool !== 'arrow' || (!s.startBind && !s.endBind)) return s;
120
+ let next: ArrowStroke = s;
121
+ // Opposite-end reference points for the facing computation. When both
122
+ // ends are bound, face the OTHER HOST's center (stable under endpoint
123
+ // churn); a free end is its literal coordinate.
124
+ const startHost = s.startBind ? byId.get(s.startBind.hostId) : undefined;
125
+ const endHost = s.endBind ? byId.get(s.endBind.hostId) : undefined;
126
+ const centerOf = (host: Stroke | undefined): [number, number] | null => {
127
+ if (!host) return null;
128
+ const bb = strokeBBox(host);
129
+ return bb ? [bb.x + bb.w / 2, bb.y + bb.h / 2] : null;
130
+ };
131
+ const startRef: [number, number] = centerOf(endHost) ?? [s.x2, s.y2];
132
+ const endRef: [number, number] = centerOf(startHost) ?? [s.x1, s.y1];
133
+ const applyEnd = (
134
+ bind: ArrowBind | undefined,
135
+ end: 'start' | 'end',
136
+ ref: [number, number]
137
+ ): void => {
138
+ if (!bind) return;
139
+ const host = byId.get(bind.hostId);
140
+ if (!host || !isBindable(host)) {
141
+ const copy: ArrowStroke = { ...next };
142
+ delete copy[end === 'start' ? 'startBind' : 'endBind'];
143
+ next = copy;
144
+ mutated = true;
145
+ return;
146
+ }
147
+ let effective = bind;
148
+ if (!bind.pinned) {
149
+ const facing = facingAnchor(host, ref[0], ref[1]);
150
+ if (facing && (facing.nx !== bind.nx || facing.ny !== bind.ny)) {
151
+ effective = { ...bind, nx: facing.nx, ny: facing.ny };
152
+ next =
153
+ end === 'start' ? { ...next, startBind: effective } : { ...next, endBind: effective };
154
+ mutated = true;
155
+ }
156
+ }
157
+ const pt = anchorPoint(host, effective.nx, effective.ny);
158
+ if (!pt) return;
159
+ if (end === 'start') {
160
+ if (next.x1 !== pt[0] || next.y1 !== pt[1]) {
161
+ next = { ...next, x1: pt[0], y1: pt[1] };
162
+ mutated = true;
163
+ }
164
+ } else if (next.x2 !== pt[0] || next.y2 !== pt[1]) {
165
+ next = { ...next, x2: pt[0], y2: pt[1] };
166
+ mutated = true;
167
+ }
168
+ };
169
+ applyEnd(s.startBind, 'start', startRef);
170
+ applyEnd(next.endBind, 'end', endRef);
171
+ return next;
172
+ });
173
+ return mutated ? out : (strokes as Stroke[]);
174
+ }
175
+
176
+ /**
177
+ * Side-magnet anchor on `host` facing the world point `(tx, ty)` — used by the
178
+ * AI annotate verb + quick-create to pick a sensible attachment side without a
179
+ * pointer position: the dominant axis of the direction vector decides the
180
+ * side, the cross axis centers.
181
+ */
182
+ export function facingAnchor(host: Stroke, tx: number, ty: number): ArrowBind | null {
183
+ const bb = strokeBBox(host);
184
+ if (!bb || bb.w <= 0 || bb.h <= 0) return null;
185
+ const cx = bb.x + bb.w / 2;
186
+ const cy = bb.y + bb.h / 2;
187
+ const dx = tx - cx;
188
+ const dy = ty - cy;
189
+ // Normalize by the half-extents so a wide-but-short card still picks the
190
+ // side the target visually faces.
191
+ const rx = Math.abs(dx) / Math.max(1, bb.w / 2);
192
+ const ry = Math.abs(dy) / Math.max(1, bb.h / 2);
193
+ if (rx >= ry) {
194
+ return { hostId: host.id, nx: dx >= 0 ? 1 : 0, ny: 0.5 };
195
+ }
196
+ return { hostId: host.id, nx: 0.5, ny: dy >= 0 ? 1 : 0 };
197
+ }