@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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file annotations-context-toolbar.tsx — Phase 5.1 per-selection toolbar
3
- * @scope plugins/design/dev-server/annotations-context-toolbar.tsx
3
+ * @scope apps/studio/annotations-context-toolbar.tsx
4
4
  * @purpose FigJam-style floating chrome anchored above the currently
5
5
  * selected annotation strokes. Reads `useAnnotationSelection` +
6
6
  * `useStrokesStore`; mutations dispatch through the store so the
@@ -21,14 +21,18 @@
21
21
  */
22
22
 
23
23
  import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
24
-
24
+ import type { AlignEdge } from './annotations-align.ts';
25
25
  import {
26
26
  type ArrowHead,
27
27
  type ArrowLineType,
28
+ type ConvertibleShapeKind,
29
+ convertShapeKind,
28
30
  FILL_PALETTE,
31
+ type ListType,
29
32
  STICKY_PALETTE,
30
33
  STROKE_PALETTE,
31
34
  type Stroke,
35
+ shapeKindOf,
32
36
  type TextAlign,
33
37
  useStrokesStore,
34
38
  } from './annotations-layer.tsx';
@@ -44,19 +48,33 @@ import {
44
48
  IconArrowNone,
45
49
  IconBold,
46
50
  IconChevronDown,
47
- IconCornerPill,
48
- IconCornerSoft,
49
- IconCornerSquare,
50
51
  IconDash,
52
+ IconDistributeH,
53
+ IconDistributeV,
54
+ IconGroup,
55
+ IconItalic,
51
56
  IconLineCurved,
52
57
  IconLineElbow,
53
58
  IconLineStraight,
54
59
  IconLineThick,
55
60
  IconLineThin,
61
+ IconLink,
62
+ IconListBullet,
63
+ IconListOrdered,
64
+ IconObjAlignBottom,
65
+ IconObjAlignHCenter,
66
+ IconObjAlignLeft,
67
+ IconObjAlignRight,
68
+ IconObjAlignTop,
69
+ IconObjAlignVCenter,
56
70
  IconStrike,
57
71
  IconTrash,
72
+ IconUnderline,
73
+ IconUngroup,
74
+ SHAPE_KIND_ICONS,
58
75
  } from './canvas-icons.tsx';
59
76
  import { useAnnotationSelectionOptional } from './use-annotation-selection.tsx';
77
+ import { isHttpUrl, linkDomain } from './use-canvas-media-drop.tsx';
60
78
 
61
79
  // Phase 24 — arrowhead + line-type option metadata (icon + label per value),
62
80
  // driving the per-end head dropdowns + the line-type dropdown.
@@ -97,6 +115,28 @@ const ALIGN_OPTIONS: ReadonlyArray<{ value: TextAlign; label: string }> = [
97
115
  { value: 'center', label: 'Align center' },
98
116
  { value: 'right', label: 'Align right' },
99
117
  ];
118
+ // FigJam v3 — object align + distribute actions for the multi-select cluster.
119
+ type AlignAction = AlignEdge | 'dist-h' | 'dist-v';
120
+ const ALIGN_ACTION_OPTIONS: ReadonlyArray<{ value: AlignAction; label: string }> = [
121
+ { value: 'left', label: 'Align left' },
122
+ { value: 'h-center', label: 'Align horizontal centers' },
123
+ { value: 'right', label: 'Align right' },
124
+ { value: 'top', label: 'Align top' },
125
+ { value: 'v-center', label: 'Align vertical centers' },
126
+ { value: 'bottom', label: 'Align bottom' },
127
+ { value: 'dist-h', label: 'Distribute horizontal spacing' },
128
+ { value: 'dist-v', label: 'Distribute vertical spacing' },
129
+ ];
130
+ const ALIGN_ACTION_ICON: Record<string, (p: { size?: number }) => ReactNode> = {
131
+ left: IconObjAlignLeft,
132
+ 'h-center': IconObjAlignHCenter,
133
+ right: IconObjAlignRight,
134
+ top: IconObjAlignTop,
135
+ 'v-center': IconObjAlignVCenter,
136
+ bottom: IconObjAlignBottom,
137
+ 'dist-h': IconDistributeH,
138
+ 'dist-v': IconDistributeV,
139
+ };
100
140
  const FONT_SIZE_PRESETS: ReadonlyArray<{ px: number; label: string }> = [
101
141
  { px: 12, label: 'Small' },
102
142
  { px: 16, label: 'Medium' },
@@ -253,6 +293,28 @@ const TOOLBAR_CSS = `
253
293
  background: color-mix(in oklab, #ff5a4d 26%, transparent);
254
294
  color: #ffffff;
255
295
  }
296
+ /* Phase 23 — media (image alt / link url) inline text field inside the dark
297
+ property bar. */
298
+ .dc-annot-ctx-field {
299
+ display: inline-flex;
300
+ align-items: center;
301
+ gap: 6px;
302
+ padding: 0 2px;
303
+ }
304
+ .dc-annot-ctx-field label { color: rgba(255,255,255,0.6); font-size: 11px; }
305
+ .dc-annot-ctx-input {
306
+ width: 220px;
307
+ max-width: 44vw;
308
+ padding: 4px 7px;
309
+ border: 1px solid rgba(255,255,255,0.18);
310
+ border-radius: 6px;
311
+ background: rgba(255,255,255,0.06);
312
+ color: #fff;
313
+ font: inherit;
314
+ font-size: 12px;
315
+ }
316
+ .dc-annot-ctx-input::placeholder { color: rgba(255,255,255,0.4); }
317
+ .dc-annot-ctx-input:focus-visible { outline: 2px solid #ffffff; outline-offset: -1px; }
256
318
  /* Phase 24 — icon dropdown (arrowhead / line-type / text-align). The wrapper
257
319
  anchors the menu directly above the trigger. */
258
320
  .dc-annot-ctx-dd { position: relative; display: inline-flex; }
@@ -371,13 +433,43 @@ function unionRect(rects: DOMRect[]): { x: number; y: number; w: number; h: numb
371
433
 
372
434
  type SwatchMode = 'stroke' | 'fill';
373
435
 
374
- export function AnnotationContextToolbar() {
436
+ export function AnnotationContextToolbar({
437
+ editingId = null,
438
+ }: {
439
+ /**
440
+ * FigJam v3 — while an inline text editor is open, the toolbar flips into
441
+ * TEXT mode: size / B / I / S / U / align controls that drive the EDITOR
442
+ * through `maude:editor-format` events (mutating the stroke mid-edit would
443
+ * re-render the contentEditable and clobber typed text). The editor echoes
444
+ * its live state back via `maude:editor-format-state`.
445
+ */
446
+ editingId?: string | null;
447
+ } = {}) {
375
448
  ensureToolbarStyles();
376
449
  const annotSel = useAnnotationSelectionOptional();
377
450
  const store = useStrokesStore();
378
451
  const ref = useRef<HTMLDivElement | null>(null);
379
452
  const rafRef = useRef<number | null>(null);
380
453
  const [, force] = useState({});
454
+ // Edit-mode mirror of the editor's live formatting state.
455
+ const [editorFmt, setEditorFmt] = useState<{
456
+ bold?: boolean;
457
+ italic?: boolean;
458
+ underline?: boolean;
459
+ strike?: boolean;
460
+ fontSize?: number;
461
+ align?: string;
462
+ }>({});
463
+ useEffect(() => {
464
+ if (!editingId || typeof document === 'undefined') return;
465
+ const onState = (e: Event) => {
466
+ setEditorFmt((e as CustomEvent<Record<string, unknown>>).detail ?? {});
467
+ };
468
+ document.addEventListener('maude:editor-format-state', onState);
469
+ // Ask the already-mounted editor for its current state (mount-order race).
470
+ document.dispatchEvent(new CustomEvent('maude:editor-format-request'));
471
+ return () => document.removeEventListener('maude:editor-format-state', onState);
472
+ }, [editingId]);
381
473
  // T30 / G_S1 — collapsed Stroke|Fill toggle state. Defaults to 'stroke';
382
474
  // auto-reverts to 'stroke' whenever caps.fill is false so the toggle
383
475
  // can't get stranded on a hidden mode after the selection changes type.
@@ -402,47 +494,66 @@ export function AnnotationContextToolbar() {
402
494
  fill: false,
403
495
  thickness: false,
404
496
  fontSize: false,
405
- cornerRadius: false,
406
497
  arrowDir: false,
407
498
  dash: false,
499
+ shapeKind: false,
408
500
  };
409
501
  }
410
- const allFillable = selectedStrokes.every((s) => s.tool === 'rect' || s.tool === 'ellipse');
502
+ // Wave H — polygons (diamond / triangle) are full shapes: fill + stroke
503
+ // weight, identical toolbar to rect/ellipse. The same "every selected
504
+ // stroke is a closed shape" test gates fill AND the shape-kind switcher.
505
+ const allClosedShapes = selectedStrokes.every(
506
+ (s) => s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
507
+ );
411
508
  // T20 — rect + ellipse now carry stroke weight too.
412
509
  const allThickness = selectedStrokes.every(
413
- (s) => s.tool === 'pen' || s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse'
510
+ (s) =>
511
+ s.tool === 'pen' ||
512
+ s.tool === 'arrow' ||
513
+ s.tool === 'rect' ||
514
+ s.tool === 'ellipse' ||
515
+ s.tool === 'polygon'
414
516
  );
415
- // Phase 21 — fontSize applies to text + sticky; cornerRadius to rect +
416
- // sticky; arrow direction + dash to arrows.
517
+ // Phase 21 — fontSize applies to text + sticky; arrow direction + dash to
518
+ // arrows. (The Phase-21 rect corner-radius cluster was RETIRED in Wave H —
519
+ // the shape-kind switcher's square/rounded covers it and showing both read
520
+ // as "shapes twice". Pill rects keep rendering; `cornerRadius` stays in
521
+ // the model + serialization, there's just no dedicated toolbar control.)
417
522
  const fontSizeApplicable = selectedStrokes.some(
418
523
  (s) => s.tool === 'text' || s.tool === 'sticky'
419
524
  );
420
- // Phase 24 — the corner-radius control is rect-only now. Stickies have a
421
- // fixed soft radius (no switch); the sticky-color swatch row is handled
422
- // separately below.
423
- const allRect = selectedStrokes.every((s) => s.tool === 'rect');
424
525
  const allArrow = selectedStrokes.every((s) => s.tool === 'arrow');
526
+ // Dash now applies to arrows AND every outlined shape (rect / ellipse /
527
+ // polygon) — item 7. Heads + line-type (arrowDir) stay arrow-only.
528
+ const allDashable = selectedStrokes.every(
529
+ (s) => s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
530
+ );
425
531
  return {
426
532
  color: true,
427
- fill: allFillable,
533
+ fill: allClosedShapes,
428
534
  thickness: allThickness,
429
535
  fontSize: fontSizeApplicable,
430
- cornerRadius: allRect,
431
536
  arrowDir: allArrow,
432
- dash: allArrow,
537
+ dash: allDashable,
538
+ // Wave H — the shape-kind switcher (square/rounded/circle/diamond/
539
+ // triangle conversion) shows when EVERY selected stroke is a closed
540
+ // shape. Conversions keep ids, so anchored text + binds follow.
541
+ shapeKind: allClosedShapes,
433
542
  };
434
543
  }, [selectedStrokes]);
435
544
 
436
545
  // Position tracker — uses rAF to follow pan/zoom while the toolbar is up.
546
+ // Edit mode anchors over the EDITED stroke (the selection may be elsewhere).
437
547
  useEffect(() => {
438
- if (selectedStrokes.length === 0) return;
548
+ if (selectedStrokes.length === 0 && !editingId) return;
549
+ const targetIds = editingId ? [editingId] : selectedStrokes.map((s) => s.id);
439
550
  const tick = () => {
440
551
  rafRef.current = null;
441
552
  const el = ref.current;
442
553
  if (!el) return;
443
554
  const rects: DOMRect[] = [];
444
- for (const s of selectedStrokes) {
445
- const node = document.querySelector(`[data-id="${cssEscape(s.id)}"]`);
555
+ for (const id of targetIds) {
556
+ const node = document.querySelector(`[data-id="${cssEscape(id)}"]`);
446
557
  if (node) rects.push(node.getBoundingClientRect());
447
558
  }
448
559
  const u = unionRect(rects);
@@ -455,7 +566,10 @@ export function AnnotationContextToolbar() {
455
566
  // Lay out off-screen first so the size is real, then position.
456
567
  const tbW = el.offsetWidth || 280;
457
568
  const tbH = el.offsetHeight || 36;
458
- const margin = 8;
569
+ // Wave G — clear the selection chrome that floats around the stroke
570
+ // (halo pad + connection dots at ~16 px + the corner rotate zones)
571
+ // instead of sitting glued to the bbox and covering the top dot.
572
+ const margin = 28;
459
573
  let top = u.y - tbH - margin;
460
574
  if (top < 8) top = u.y + u.h + margin;
461
575
  let left = u.x + (u.w - tbW) / 2;
@@ -470,7 +584,7 @@ export function AnnotationContextToolbar() {
470
584
  return () => {
471
585
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
472
586
  };
473
- }, [selectedStrokes]);
587
+ }, [selectedStrokes, editingId]);
474
588
 
475
589
  // Force a re-render when the strokes themselves mutate (the position rAF
476
590
  // already follows the bbox; this ensures the active-state of the swatches
@@ -480,134 +594,168 @@ export function AnnotationContextToolbar() {
480
594
  force({});
481
595
  }, [selectedStrokes]);
482
596
 
597
+ // FigJam v3 — every bulk style mutation routes through `applyToStrokes` so
598
+ // an N-stroke selection edit is ONE undo record (the pre-v3 per-stroke
599
+ // `updateStroke` loop pushed N records — ⌘Z had to be pressed N times).
600
+ const selectedIds = useMemo(() => selectedStrokes.map((s) => s.id), [selectedStrokes]);
483
601
  const setColor = useCallback(
484
602
  (c: string) => {
485
- if (!store) return;
486
- for (const s of selectedStrokes) store.updateStroke(s.id, { color: c });
603
+ store?.applyToStrokes(selectedIds, () => ({ color: c }), 'set color');
487
604
  },
488
- [store, selectedStrokes]
605
+ [store, selectedIds]
489
606
  );
490
607
  const setFill = useCallback(
491
608
  (f: string | null) => {
492
- if (!store) return;
493
- for (const s of selectedStrokes) {
494
- if (s.tool === 'rect' || s.tool === 'ellipse') {
495
- store.updateStroke(s.id, { fill: f } as Partial<Stroke>);
496
- }
497
- }
609
+ store?.applyToStrokes(
610
+ selectedIds,
611
+ (s) =>
612
+ s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
613
+ ? ({ fill: f } as Partial<Stroke>)
614
+ : null,
615
+ 'set fill'
616
+ );
498
617
  },
499
- [store, selectedStrokes]
618
+ [store, selectedIds]
500
619
  );
501
620
  const setThickness = useCallback(
502
621
  (w: number) => {
503
- if (!store) return;
504
- for (const s of selectedStrokes) {
505
- // T20 — rect + ellipse now carry stroke weight.
506
- if (s.tool === 'pen' || s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse') {
507
- store.updateStroke(s.id, { width: w } as Partial<Stroke>);
508
- }
509
- }
622
+ // T20 — rect + ellipse carry stroke weight too; Wave H adds polygon.
623
+ store?.applyToStrokes(
624
+ selectedIds,
625
+ (s) =>
626
+ s.tool === 'pen' ||
627
+ s.tool === 'arrow' ||
628
+ s.tool === 'rect' ||
629
+ s.tool === 'ellipse' ||
630
+ s.tool === 'polygon'
631
+ ? ({ width: w } as Partial<Stroke>)
632
+ : null,
633
+ 'set thickness'
634
+ );
510
635
  },
511
- [store, selectedStrokes]
636
+ [store, selectedIds]
512
637
  );
513
638
  const setFontSize = useCallback(
514
639
  (sz: number) => {
515
- if (!store) return;
516
- for (const s of selectedStrokes) {
517
- // Phase 21 — sticky carries fontSize too.
518
- if (s.tool === 'text' || s.tool === 'sticky') {
519
- store.updateStroke(s.id, { fontSize: sz } as Partial<Stroke>);
520
- }
521
- }
640
+ // Phase 21 — sticky carries fontSize too.
641
+ store?.applyToStrokes(
642
+ selectedIds,
643
+ (s) =>
644
+ s.tool === 'text' || s.tool === 'sticky' ? ({ fontSize: sz } as Partial<Stroke>) : null,
645
+ 'set font size'
646
+ );
522
647
  },
523
- [store, selectedStrokes]
648
+ [store, selectedIds]
524
649
  );
525
- // Phase 24 — bold / strike / align on text + sticky bodies.
526
- const setBold = useCallback(
527
- (bold: boolean) => {
528
- if (!store) return;
529
- for (const s of selectedStrokes) {
530
- if (s.tool === 'text' || s.tool === 'sticky') {
531
- store.updateStroke(s.id, { bold } as Partial<Stroke>);
532
- }
533
- }
650
+ // Phase 24 — bold / strike / italic / underline / list / align on text +
651
+ // sticky bodies, all single-record.
652
+ const applyTextPatch = useCallback(
653
+ (patch: Partial<Stroke>, label: string) => {
654
+ store?.applyToStrokes(
655
+ selectedIds,
656
+ (s) => (s.tool === 'text' || s.tool === 'sticky' ? patch : null),
657
+ label
658
+ );
534
659
  },
535
- [store, selectedStrokes]
660
+ [store, selectedIds]
661
+ );
662
+ const setBold = useCallback(
663
+ (bold: boolean) => applyTextPatch({ bold } as Partial<Stroke>, 'set bold'),
664
+ [applyTextPatch]
536
665
  );
537
666
  const setStrike = useCallback(
538
- (strike: boolean) => {
539
- if (!store) return;
540
- for (const s of selectedStrokes) {
541
- if (s.tool === 'text' || s.tool === 'sticky') {
542
- store.updateStroke(s.id, { strike } as Partial<Stroke>);
543
- }
544
- }
545
- },
546
- [store, selectedStrokes]
667
+ (strike: boolean) => applyTextPatch({ strike } as Partial<Stroke>, 'set strikethrough'),
668
+ [applyTextPatch]
669
+ );
670
+ const setItalic = useCallback(
671
+ (italic: boolean) => applyTextPatch({ italic } as Partial<Stroke>, 'set italic'),
672
+ [applyTextPatch]
673
+ );
674
+ const setUnderline = useCallback(
675
+ (underline: boolean) => applyTextPatch({ underline } as Partial<Stroke>, 'set underline'),
676
+ [applyTextPatch]
677
+ );
678
+ const setListType = useCallback(
679
+ (listType: ListType | undefined) =>
680
+ applyTextPatch({ listType } as Partial<Stroke>, 'set list style'),
681
+ [applyTextPatch]
547
682
  );
548
683
  const setAlign = useCallback(
549
- (align: TextAlign) => {
550
- if (!store) return;
551
- for (const s of selectedStrokes) {
552
- if (s.tool === 'text' || s.tool === 'sticky') {
553
- store.updateStroke(s.id, { align } as Partial<Stroke>);
554
- }
555
- }
556
- },
557
- [store, selectedStrokes]
684
+ (align: TextAlign) => applyTextPatch({ align } as Partial<Stroke>, 'set alignment'),
685
+ [applyTextPatch]
558
686
  );
559
- // Phase 21corner radius (rect + sticky).
560
- const setCornerRadius = useCallback(
561
- (r: number) => {
562
- if (!store) return;
563
- for (const s of selectedStrokes) {
564
- if (s.tool === 'rect' || s.tool === 'sticky') {
565
- store.updateStroke(s.id, { cornerRadius: r } as Partial<Stroke>);
566
- }
567
- }
687
+ // Wave Hconvert the selection to another shape kind (one undo record).
688
+ const setSelectionShapeKind = useCallback(
689
+ (kind: ConvertibleShapeKind) => {
690
+ store?.applyToStrokes(selectedIds, (s) => convertShapeKind(s, kind), 'change shape');
568
691
  },
569
- [store, selectedStrokes]
692
+ [store, selectedIds]
570
693
  );
571
- // Phase 24 — per-end arrowhead + line-type (replaces the 4 fixed direction
572
- // presets with two head dropdowns + a routing dropdown).
694
+ // Phase 24 — per-end arrowhead + line-type.
573
695
  const setStartHead = useCallback(
574
696
  (startHead: ArrowHead) => {
575
- if (!store) return;
576
- for (const s of selectedStrokes) {
577
- if (s.tool === 'arrow') store.updateStroke(s.id, { startHead } as Partial<Stroke>);
578
- }
697
+ store?.applyToStrokes(
698
+ selectedIds,
699
+ (s) => (s.tool === 'arrow' ? ({ startHead } as Partial<Stroke>) : null),
700
+ 'set arrowhead'
701
+ );
579
702
  },
580
- [store, selectedStrokes]
703
+ [store, selectedIds]
581
704
  );
582
705
  const setEndHead = useCallback(
583
706
  (endHead: ArrowHead) => {
584
- if (!store) return;
585
- for (const s of selectedStrokes) {
586
- if (s.tool === 'arrow') store.updateStroke(s.id, { endHead } as Partial<Stroke>);
587
- }
707
+ store?.applyToStrokes(
708
+ selectedIds,
709
+ (s) => (s.tool === 'arrow' ? ({ endHead } as Partial<Stroke>) : null),
710
+ 'set arrowhead'
711
+ );
588
712
  },
589
- [store, selectedStrokes]
713
+ [store, selectedIds]
590
714
  );
591
715
  const setLineType = useCallback(
592
716
  (lineType: ArrowLineType) => {
593
- if (!store) return;
594
- for (const s of selectedStrokes) {
595
- if (s.tool === 'arrow') store.updateStroke(s.id, { lineType } as Partial<Stroke>);
596
- }
717
+ store?.applyToStrokes(
718
+ selectedIds,
719
+ (s) => (s.tool === 'arrow' ? ({ lineType } as Partial<Stroke>) : null),
720
+ 'set line type'
721
+ );
597
722
  },
598
- [store, selectedStrokes]
723
+ [store, selectedIds]
599
724
  );
600
- // Phase 21 — arrow dash toggle.
725
+ // Dash toggle — arrow + rect / ellipse / polygon (item 7).
601
726
  const setDashed = useCallback(
602
727
  (dashed: boolean) => {
603
- if (!store) return;
604
- for (const s of selectedStrokes) {
605
- if (s.tool === 'arrow') {
606
- store.updateStroke(s.id, { dashed } as Partial<Stroke>);
607
- }
728
+ store?.applyToStrokes(
729
+ selectedIds,
730
+ (s) =>
731
+ s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
732
+ ? ({ dashed } as Partial<Stroke>)
733
+ : null,
734
+ 'set dash'
735
+ );
736
+ },
737
+ [store, selectedIds]
738
+ );
739
+ // FigJam v3 — group / ungroup / align / distribute on the selection.
740
+ const groupSel = useCallback(() => {
741
+ if (!annotSel || !store) return;
742
+ const members = store.groupSelection(annotSel.selectedIds);
743
+ if (members) annotSel.replace(members);
744
+ }, [annotSel, store]);
745
+ const ungroupSel = useCallback(() => {
746
+ if (!annotSel || !store) return;
747
+ store.ungroupSelection(annotSel.selectedIds);
748
+ }, [annotSel, store]);
749
+ const alignSel = useCallback(
750
+ (op: AlignAction) => {
751
+ if (!annotSel || !store) return;
752
+ if (op === 'dist-h' || op === 'dist-v') {
753
+ store.distributeSelection(annotSel.selectedIds, op === 'dist-h' ? 'h' : 'v');
754
+ } else {
755
+ store.alignSelection(annotSel.selectedIds, op);
608
756
  }
609
757
  },
610
- [store, selectedStrokes]
758
+ [annotSel, store]
611
759
  );
612
760
  const remove = useCallback(() => {
613
761
  if (!annotSel || !store) return;
@@ -615,21 +763,226 @@ export function AnnotationContextToolbar() {
615
763
  annotSel.clear();
616
764
  }, [annotSel, store]);
617
765
 
766
+ // FigJam v3 — TEXT mode while an inline editor is open: a focused strip of
767
+ // text controls dispatching to the editor (Image-6 FigJam parity).
768
+ if (editingId) {
769
+ const dispatchFmt = (key: string, value?: unknown) => {
770
+ document.dispatchEvent(new CustomEvent('maude:editor-format', { detail: { key, value } }));
771
+ };
772
+ const fmtBtn = (
773
+ key: 'bold' | 'italic' | 'strike' | 'underline',
774
+ label: string,
775
+ icon: ReactNode
776
+ ) => (
777
+ <button
778
+ type="button"
779
+ className="dc-annot-ctx-ibtn"
780
+ aria-label={label}
781
+ aria-pressed={!!editorFmt[key]}
782
+ title={label}
783
+ // The editor commits on blur/outside-pointerdown — suppress the
784
+ // mousedown so clicking the toolbar doesn't end the edit session.
785
+ onMouseDown={(e) => e.preventDefault()}
786
+ onPointerDown={(e) => e.stopPropagation()}
787
+ onClick={() => dispatchFmt(key)}
788
+ >
789
+ {icon}
790
+ </button>
791
+ );
792
+ return (
793
+ <div
794
+ ref={ref}
795
+ className="dc-annot-ctx"
796
+ role="toolbar"
797
+ aria-label="Text formatting"
798
+ style={{ display: 'flex', top: -9999, left: -9999 }}
799
+ onPointerDown={(e) => e.stopPropagation()}
800
+ >
801
+ <FontSizeDropdown value={editorFmt.fontSize} onPick={(n) => dispatchFmt('fontSize', n)} />
802
+ {fmtBtn('bold', 'Bold', <IconBold />)}
803
+ {fmtBtn('italic', 'Italic', <IconItalic />)}
804
+ {fmtBtn('strike', 'Strikethrough', <IconStrike />)}
805
+ {fmtBtn('underline', 'Underline', <IconUnderline />)}
806
+ <IconDropdown
807
+ ariaLabel="Text alignment"
808
+ value={editorFmt.align ?? 'left'}
809
+ options={ALIGN_OPTIONS}
810
+ renderIcon={(v) => {
811
+ const I = ALIGN_ICON[v as TextAlign];
812
+ return I ? <I size={16} /> : null;
813
+ }}
814
+ onPick={(v) => dispatchFmt('align', v)}
815
+ />
816
+ </div>
817
+ );
818
+ }
819
+
618
820
  if (!annotSel || !store || selectedStrokes.length === 0) return null;
619
821
 
822
+ // ── Phase 23 — dedicated panel for a single image / link selection. ─────────
823
+ // Image / link carry no color / fill / thickness, so the generic swatch bar is
824
+ // meaningless for them; render a focused panel instead (image alt for a11y;
825
+ // link url/title + Open). Uncontrolled fields keyed by id commit ONCE on
826
+ // blur/Enter (no per-keystroke undo spam). Multi-select / mixed selections
827
+ // fall through to the generic bar below.
828
+ const soleMedia =
829
+ selectedStrokes.length === 1 &&
830
+ (selectedStrokes[0]?.tool === 'image' || selectedStrokes[0]?.tool === 'link')
831
+ ? selectedStrokes[0]
832
+ : null;
833
+ if (soleMedia?.tool === 'image') {
834
+ const img = soleMedia;
835
+ const commitAlt = (value: string) => {
836
+ const next = value.trim();
837
+ if ((img.alt ?? '') !== next) {
838
+ store.updateStroke(img.id, { alt: next || undefined } as Partial<Stroke>);
839
+ }
840
+ };
841
+ return (
842
+ <div
843
+ ref={ref}
844
+ className="dc-annot-ctx"
845
+ role="toolbar"
846
+ aria-label="Image properties"
847
+ style={{ display: 'flex', top: -9999, left: -9999 }}
848
+ >
849
+ <div className="dc-annot-ctx-field">
850
+ <label htmlFor="dc-img-alt">Alt</label>
851
+ <input
852
+ id="dc-img-alt"
853
+ key={img.id}
854
+ className="dc-annot-ctx-input"
855
+ type="text"
856
+ defaultValue={img.alt ?? ''}
857
+ placeholder="Describe this image (alt text)"
858
+ aria-label="Image alt text"
859
+ onBlur={(e) => commitAlt(e.currentTarget.value)}
860
+ onKeyDown={(e) => {
861
+ if (e.key === 'Enter') {
862
+ e.preventDefault();
863
+ e.currentTarget.blur();
864
+ }
865
+ }}
866
+ />
867
+ </div>
868
+ <div className="dc-annot-ctx-sep" />
869
+ <button
870
+ type="button"
871
+ className="dc-annot-ctx-ibtn dc-annot-ctx-ibtn--danger"
872
+ aria-label="Delete image"
873
+ title="Delete"
874
+ onClick={remove}
875
+ >
876
+ <IconTrash />
877
+ </button>
878
+ </div>
879
+ );
880
+ }
881
+ if (soleMedia?.tool === 'link') {
882
+ const link = soleMedia;
883
+ const commitUrl = (value: string) => {
884
+ const next = value.trim();
885
+ if (isHttpUrl(next) && next !== link.url) {
886
+ store.updateStroke(link.id, { url: next, domain: linkDomain(next) } as Partial<Stroke>);
887
+ }
888
+ };
889
+ const commitTitle = (value: string) => {
890
+ const next = value.trim();
891
+ if (next !== link.title) store.updateStroke(link.id, { title: next } as Partial<Stroke>);
892
+ };
893
+ const openLink = () => {
894
+ if (isHttpUrl(link.url)) window.open(link.url, '_blank', 'noopener,noreferrer');
895
+ };
896
+ return (
897
+ <div
898
+ ref={ref}
899
+ className="dc-annot-ctx"
900
+ role="toolbar"
901
+ aria-label="Link properties"
902
+ style={{ display: 'flex', top: -9999, left: -9999 }}
903
+ >
904
+ <button
905
+ type="button"
906
+ className="dc-annot-ctx-ibtn"
907
+ aria-label={`Open ${link.domain || 'link'} in a new tab`}
908
+ title="Open link"
909
+ onClick={openLink}
910
+ >
911
+ <IconLink />
912
+ </button>
913
+ <div className="dc-annot-ctx-sep" />
914
+ <div className="dc-annot-ctx-field">
915
+ <input
916
+ key={`${link.id}-url`}
917
+ className="dc-annot-ctx-input"
918
+ type="url"
919
+ inputMode="url"
920
+ defaultValue={link.url}
921
+ placeholder="https://…"
922
+ aria-label="Link URL"
923
+ onBlur={(e) => commitUrl(e.currentTarget.value)}
924
+ onKeyDown={(e) => {
925
+ if (e.key === 'Enter') {
926
+ e.preventDefault();
927
+ e.currentTarget.blur();
928
+ }
929
+ }}
930
+ />
931
+ <input
932
+ key={`${link.id}-title`}
933
+ className="dc-annot-ctx-input"
934
+ type="text"
935
+ defaultValue={link.title}
936
+ placeholder="Title"
937
+ aria-label="Link title"
938
+ onBlur={(e) => commitTitle(e.currentTarget.value)}
939
+ onKeyDown={(e) => {
940
+ if (e.key === 'Enter') {
941
+ e.preventDefault();
942
+ e.currentTarget.blur();
943
+ }
944
+ }}
945
+ />
946
+ </div>
947
+ <div className="dc-annot-ctx-sep" />
948
+ <button
949
+ type="button"
950
+ className="dc-annot-ctx-ibtn dc-annot-ctx-ibtn--danger"
951
+ aria-label="Delete link"
952
+ title="Delete"
953
+ onClick={remove}
954
+ >
955
+ <IconTrash />
956
+ </button>
957
+ </div>
958
+ );
959
+ }
960
+
961
+ // FigJam v3 — group / align availability. Selection expansion guarantees a
962
+ // group is always selected whole, so "any member grouped" ⇒ ungroupable.
963
+ const canGroup = selectedStrokes.length >= 2;
964
+ const canUngroup = selectedStrokes.some((s) => (s.groupIds?.length ?? 0) > 0);
965
+ const canAlign = selectedStrokes.length >= 2;
966
+
620
967
  // Active values for swatch highlighting — when uniform across selection.
621
968
  const uniqColor = uniformValue(selectedStrokes.map((s) => s.color));
622
969
  const uniqFill = caps.fill
623
970
  ? uniformValue(
624
971
  selectedStrokes.map((s) =>
625
- s.tool === 'rect' || s.tool === 'ellipse' ? (s.fill ?? null) : undefined
972
+ s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
973
+ ? (s.fill ?? null)
974
+ : undefined
626
975
  )
627
976
  )
628
977
  : undefined;
629
978
  const uniqThickness = caps.thickness
630
979
  ? uniformValue(
631
980
  selectedStrokes.map((s) =>
632
- s.tool === 'pen' || s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse'
981
+ s.tool === 'pen' ||
982
+ s.tool === 'arrow' ||
983
+ s.tool === 'rect' ||
984
+ s.tool === 'ellipse' ||
985
+ s.tool === 'polygon'
633
986
  ? s.width
634
987
  : undefined
635
988
  )
@@ -659,6 +1012,29 @@ export function AnnotationContextToolbar() {
659
1012
  )
660
1013
  )
661
1014
  : undefined;
1015
+ const uniqItalic = caps.fontSize
1016
+ ? uniformValue(
1017
+ selectedStrokes.map((s) =>
1018
+ s.tool === 'text' || s.tool === 'sticky' ? !!s.italic : undefined
1019
+ )
1020
+ )
1021
+ : undefined;
1022
+ const uniqUnderline = caps.fontSize
1023
+ ? uniformValue(
1024
+ selectedStrokes.map((s) =>
1025
+ s.tool === 'text' || s.tool === 'sticky' ? !!s.underline : undefined
1026
+ )
1027
+ )
1028
+ : undefined;
1029
+ // List type — undefined when no list (the active-state for the two list
1030
+ // toggles reads off this; both off ⇒ no list).
1031
+ const uniqListType = caps.fontSize
1032
+ ? uniformValue(
1033
+ selectedStrokes.map((s) =>
1034
+ s.tool === 'text' || s.tool === 'sticky' ? (s.listType ?? 'none') : undefined
1035
+ )
1036
+ )
1037
+ : undefined;
662
1038
  const uniqAlign = caps.fontSize
663
1039
  ? uniformValue(
664
1040
  selectedStrokes.map((s) => {
@@ -668,18 +1044,9 @@ export function AnnotationContextToolbar() {
668
1044
  })
669
1045
  )
670
1046
  : undefined;
671
- // Phase 21 — uniform corner radius across rect/sticky (default per type: rect
672
- // sharp = 0, sticky soft = 8).
673
- const uniqRadius = caps.cornerRadius
674
- ? uniformValue(
675
- selectedStrokes.map((s) =>
676
- s.tool === 'rect'
677
- ? (s.cornerRadius ?? 0)
678
- : s.tool === 'sticky'
679
- ? (s.cornerRadius ?? 8)
680
- : undefined
681
- )
682
- )
1047
+ // Wave H — uniform shape kind across the selection (undefined = mixed).
1048
+ const uniqShapeKind = caps.shapeKind
1049
+ ? uniformValue(selectedStrokes.map((s) => shapeKindOf(s) ?? undefined))
683
1050
  : undefined;
684
1051
  // Phase 21 — uniform arrow head pair (default start none / end triangle).
685
1052
  const uniqStartHead = caps.arrowDir
@@ -699,7 +1066,11 @@ export function AnnotationContextToolbar() {
699
1066
  : undefined;
700
1067
  const uniqDashed = caps.dash
701
1068
  ? uniformValue(
702
- selectedStrokes.map((s) => (s.tool === 'arrow' ? (s.dashed ?? false) : undefined))
1069
+ selectedStrokes.map((s) =>
1070
+ s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse' || s.tool === 'polygon'
1071
+ ? (s.dashed ?? false)
1072
+ : undefined
1073
+ )
703
1074
  )
704
1075
  : undefined;
705
1076
 
@@ -817,6 +1188,16 @@ export function AnnotationContextToolbar() {
817
1188
  >
818
1189
  <IconBold />
819
1190
  </button>
1191
+ <button
1192
+ type="button"
1193
+ className="dc-annot-ctx-ibtn"
1194
+ aria-label="Italic"
1195
+ aria-pressed={uniqItalic === true}
1196
+ title="Italic"
1197
+ onClick={() => setItalic(!(uniqItalic === true))}
1198
+ >
1199
+ <IconItalic />
1200
+ </button>
820
1201
  <button
821
1202
  type="button"
822
1203
  className="dc-annot-ctx-ibtn"
@@ -827,6 +1208,16 @@ export function AnnotationContextToolbar() {
827
1208
  >
828
1209
  <IconStrike />
829
1210
  </button>
1211
+ <button
1212
+ type="button"
1213
+ className="dc-annot-ctx-ibtn"
1214
+ aria-label="Underline"
1215
+ aria-pressed={uniqUnderline === true}
1216
+ title="Underline"
1217
+ onClick={() => setUnderline(!(uniqUnderline === true))}
1218
+ >
1219
+ <IconUnderline />
1220
+ </button>
830
1221
  <IconDropdown
831
1222
  ariaLabel="Text alignment"
832
1223
  value={uniqAlign ?? 'left'}
@@ -837,43 +1228,59 @@ export function AnnotationContextToolbar() {
837
1228
  }}
838
1229
  onPick={(v) => setAlign(v as TextAlign)}
839
1230
  />
840
- </>
841
- ) : null}
842
- {caps.cornerRadius ? (
843
- <>
844
- <div className="dc-annot-ctx-sep" />
1231
+ {/* List style — two mutually-exclusive toggles; click-again clears. */}
845
1232
  <button
846
1233
  type="button"
847
1234
  className="dc-annot-ctx-ibtn"
848
- aria-label="Square corners"
849
- aria-pressed={uniqRadius === 0}
850
- title="Square corners"
851
- onClick={() => setCornerRadius(0)}
1235
+ aria-label="Bulleted list"
1236
+ aria-pressed={uniqListType === 'bullet'}
1237
+ title="Bulleted list"
1238
+ onClick={() => setListType(uniqListType === 'bullet' ? undefined : 'bullet')}
852
1239
  >
853
- <IconCornerSquare />
1240
+ <IconListBullet />
854
1241
  </button>
855
1242
  <button
856
1243
  type="button"
857
1244
  className="dc-annot-ctx-ibtn"
858
- aria-label="Soft corners"
859
- aria-pressed={uniqRadius === 8}
860
- title="Soft corners"
861
- onClick={() => setCornerRadius(8)}
1245
+ aria-label="Numbered list"
1246
+ aria-pressed={uniqListType === 'number'}
1247
+ title="Numbered list"
1248
+ onClick={() => setListType(uniqListType === 'number' ? undefined : 'number')}
862
1249
  >
863
- <IconCornerSoft />
864
- </button>
865
- <button
866
- type="button"
867
- className="dc-annot-ctx-ibtn"
868
- aria-label="Pill corners"
869
- aria-pressed={uniqRadius === 999}
870
- title="Pill corners"
871
- onClick={() => setCornerRadius(999)}
872
- >
873
- <IconCornerPill />
1250
+ <IconListOrdered />
874
1251
  </button>
875
1252
  </>
876
1253
  ) : null}
1254
+ {caps.shapeKind ? (
1255
+ <>
1256
+ <div className="dc-annot-ctx-sep" />
1257
+ {(
1258
+ [
1259
+ ['square', 'Square'],
1260
+ ['rounded', 'Rounded square'],
1261
+ ['circle', 'Circle'],
1262
+ ['diamond', 'Diamond'],
1263
+ ['triangle', 'Triangle'],
1264
+ ['triangle-down', 'Triangle down'],
1265
+ ] as Array<[ConvertibleShapeKind, string]>
1266
+ ).map(([kind, label]) => {
1267
+ const KindIcon = SHAPE_KIND_ICONS[kind];
1268
+ return (
1269
+ <button
1270
+ key={kind}
1271
+ type="button"
1272
+ className="dc-annot-ctx-ibtn"
1273
+ aria-label={label}
1274
+ aria-pressed={uniqShapeKind === kind}
1275
+ title={label}
1276
+ onClick={() => setSelectionShapeKind(kind)}
1277
+ >
1278
+ {KindIcon ? <KindIcon size={16} /> : null}
1279
+ </button>
1280
+ );
1281
+ })}
1282
+ </>
1283
+ ) : null}
877
1284
  {caps.arrowDir ? (
878
1285
  <>
879
1286
  <div className="dc-annot-ctx-sep" />
@@ -924,6 +1331,46 @@ export function AnnotationContextToolbar() {
924
1331
  </button>
925
1332
  </>
926
1333
  ) : null}
1334
+ {canGroup || canUngroup || canAlign ? (
1335
+ <>
1336
+ <div className="dc-annot-ctx-sep" />
1337
+ {canGroup ? (
1338
+ <button
1339
+ type="button"
1340
+ className="dc-annot-ctx-ibtn"
1341
+ aria-label="Group selection"
1342
+ title="Group (⌘G)"
1343
+ onClick={groupSel}
1344
+ >
1345
+ <IconGroup />
1346
+ </button>
1347
+ ) : null}
1348
+ {canUngroup ? (
1349
+ <button
1350
+ type="button"
1351
+ className="dc-annot-ctx-ibtn"
1352
+ aria-label="Ungroup selection"
1353
+ title="Ungroup (⌘⇧G)"
1354
+ onClick={ungroupSel}
1355
+ >
1356
+ <IconUngroup />
1357
+ </button>
1358
+ ) : null}
1359
+ {canAlign ? (
1360
+ <ActionDropdown
1361
+ ariaLabel="Align and distribute"
1362
+ triggerIcon={<IconObjAlignLeft size={16} />}
1363
+ options={ALIGN_ACTION_OPTIONS}
1364
+ renderIcon={(v) => {
1365
+ const I = ALIGN_ACTION_ICON[v];
1366
+ return I ? <I size={16} /> : null;
1367
+ }}
1368
+ isDisabled={(v) => (v === 'dist-h' || v === 'dist-v') && selectedStrokes.length < 3}
1369
+ onPick={(v) => alignSel(v as AlignAction)}
1370
+ />
1371
+ ) : null}
1372
+ </>
1373
+ ) : null}
927
1374
  <div className="dc-annot-ctx-sep" />
928
1375
  <button
929
1376
  type="button"
@@ -1017,6 +1464,89 @@ function IconDropdown({
1017
1464
  );
1018
1465
  }
1019
1466
 
1467
+ /**
1468
+ * FigJam v3 — an ACTION dropdown (vs IconDropdown's radio semantics): each
1469
+ * item fires an operation instead of reflecting a current value. Used by the
1470
+ * align/distribute cluster; distribute items disable below three strokes.
1471
+ */
1472
+ function ActionDropdown({
1473
+ ariaLabel,
1474
+ triggerIcon,
1475
+ options,
1476
+ renderIcon,
1477
+ isDisabled,
1478
+ onPick,
1479
+ }: {
1480
+ ariaLabel: string;
1481
+ triggerIcon: ReactNode;
1482
+ options: ReadonlyArray<{ value: string; label: string }>;
1483
+ renderIcon: (v: string) => ReactNode;
1484
+ isDisabled?: (v: string) => boolean;
1485
+ onPick: (v: string) => void;
1486
+ }) {
1487
+ const [open, setOpen] = useState(false);
1488
+ const ref = useRef<HTMLDivElement | null>(null);
1489
+ useEffect(() => {
1490
+ if (!open) return;
1491
+ const onDown = (e: PointerEvent) => {
1492
+ if (!ref.current?.contains(e.target as Node)) setOpen(false);
1493
+ };
1494
+ const onKey = (e: KeyboardEvent) => {
1495
+ if (e.key === 'Escape') setOpen(false);
1496
+ };
1497
+ document.addEventListener('pointerdown', onDown, true);
1498
+ document.addEventListener('keydown', onKey, true);
1499
+ return () => {
1500
+ document.removeEventListener('pointerdown', onDown, true);
1501
+ document.removeEventListener('keydown', onKey, true);
1502
+ };
1503
+ }, [open]);
1504
+ return (
1505
+ <div ref={ref} className="dc-annot-ctx-dd">
1506
+ <button
1507
+ type="button"
1508
+ className="dc-annot-ctx-ibtn dc-annot-ctx-dd-trigger"
1509
+ aria-haspopup="menu"
1510
+ aria-expanded={open}
1511
+ aria-label={ariaLabel}
1512
+ title={ariaLabel}
1513
+ onClick={() => setOpen((o) => !o)}
1514
+ >
1515
+ {triggerIcon}
1516
+ <span className="dc-annot-ctx-dd-caret" aria-hidden="true">
1517
+ <IconChevronDown size={8} />
1518
+ </span>
1519
+ </button>
1520
+ {open ? (
1521
+ <div className="dc-annot-ctx-menu" role="menu" aria-label={ariaLabel}>
1522
+ {options.map((o) => {
1523
+ const disabled = isDisabled?.(o.value) ?? false;
1524
+ return (
1525
+ <button
1526
+ key={o.value}
1527
+ type="button"
1528
+ role="menuitem"
1529
+ aria-label={o.label}
1530
+ title={o.label}
1531
+ className="dc-annot-ctx-ibtn"
1532
+ disabled={disabled}
1533
+ style={disabled ? { opacity: 0.35, cursor: 'default' } : undefined}
1534
+ onClick={() => {
1535
+ if (disabled) return;
1536
+ onPick(o.value);
1537
+ setOpen(false);
1538
+ }}
1539
+ >
1540
+ {renderIcon(o.value)}
1541
+ </button>
1542
+ );
1543
+ })}
1544
+ </div>
1545
+ ) : null}
1546
+ </div>
1547
+ );
1548
+ }
1549
+
1020
1550
  /**
1021
1551
  * Phase 24 — font-size control: named presets (Small → Huge) + a numeric input
1022
1552
  * for an arbitrary px value (clamped 8–200, applied on Enter / blur). The