@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.
- package/README.md +3 -3
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +4537 -0
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
- package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
- package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
- package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
- package/apps/studio/bin/read-annotations.mjs +573 -0
- package/apps/studio/bin/read-annotations.sh +11 -0
- package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
- package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
- package/apps/studio/bin/to-lottie-verify.html +68 -0
- package/apps/studio/bin/to-lottie.sh +128 -0
- package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
- package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
- package/apps/studio/canvas-create.ts +104 -0
- package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
- package/apps/studio/client/app.jsx +6742 -0
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/apps/studio/client/index.html +16 -0
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +1161 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
- package/apps/studio/client/styles/4-components-maude.css +74 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
- package/apps/studio/client/styles/5-maude-overrides.css +137 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
- package/apps/studio/client/tour/overlay.jsx +279 -0
- package/apps/studio/client/tour/usage-tour.js +48 -0
- package/apps/studio/client/whats-new-seen.js +52 -0
- package/apps/studio/client/whats-new.jsx +250 -0
- package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
- package/apps/studio/dist/client.bundle.js +25545 -0
- package/apps/studio/dist/comment-mount.js +2046 -0
- package/apps/studio/dist/styles.css +8018 -0
- package/apps/studio/dom-selection.ts +342 -0
- package/apps/studio/draw/animate.ts +333 -0
- package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
- package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
- package/apps/studio/draw/morph.ts +204 -0
- package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
- package/apps/studio/draw/serialize-animate.ts +324 -0
- package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
- package/apps/studio/draw/test/animate.test.ts +172 -0
- package/apps/studio/draw/test/morph.test.ts +107 -0
- package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
- package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
- package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
- package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
- package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +321 -0
- package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
- package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
- package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/annotations-text-format.test.ts +396 -0
- package/apps/studio/test/asset-api.test.ts +149 -0
- package/apps/studio/test/canvas-create-api.test.ts +361 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
- package/apps/studio/test/canvas-edit.test.ts +319 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
- package/apps/studio/test/canvas-media-drop.test.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
- package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
- package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
- package/apps/studio/test/motion-sample.test.ts +35 -0
- package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
- package/apps/studio/test/read-annotations.test.ts +409 -0
- package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
- package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
- package/apps/studio/test/sync-agent.test.ts +575 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
- package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
- package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/apps/studio/test/use-annotation-resize.test.ts +402 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
- package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
- package/apps/studio/test/whats-new.test.ts +101 -0
- package/apps/studio/text-imports.d.ts +9 -0
- package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
- package/apps/studio/use-annotation-resize.tsx +912 -0
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
- package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
- package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
- package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +264 -0
- package/apps/studio/whats-new.schema.json +86 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +16 -7
- package/cli/commands/design.test.mjs +40 -0
- package/cli/commands/help.mjs +8 -2
- package/cli/commands/hub.mjs +3 -3
- package/cli/commands/hub.test.mjs +1 -1
- package/cli/commands/scenario-report.mjs +2 -9
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
- package/cli/lib/preflight.mjs +1 -1
- package/package.json +15 -15
- package/plugins/design/templates/_shell.html +28 -4
- package/plugins/design/templates/brief-board.tsx.template +61 -0
- package/plugins/design/templates/canvas.tsx.template +1 -1
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/plugins/flow/.claude-plugin/config.schema.json +16 -0
- package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
- package/plugins/design/dev-server/client/app.jsx +0 -2985
- package/plugins/design/dev-server/client/index.html +0 -15
- package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
- package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
- package/plugins/design/dev-server/dist/styles.css +0 -2710
- package/plugins/design/dev-server/dom-selection.ts +0 -156
- package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
- package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
- package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
- package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
- /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
- /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
|
|
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
|
-
|
|
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) =>
|
|
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;
|
|
416
|
-
//
|
|
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:
|
|
533
|
+
fill: allClosedShapes,
|
|
428
534
|
thickness: allThickness,
|
|
429
535
|
fontSize: fontSizeApplicable,
|
|
430
|
-
cornerRadius: allRect,
|
|
431
536
|
arrowDir: allArrow,
|
|
432
|
-
dash:
|
|
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
|
|
445
|
-
const node = document.querySelector(`[data-id="${cssEscape(
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
for (const s of selectedStrokes) store.updateStroke(s.id, { color: c });
|
|
603
|
+
store?.applyToStrokes(selectedIds, () => ({ color: c }), 'set color');
|
|
487
604
|
},
|
|
488
|
-
[store,
|
|
605
|
+
[store, selectedIds]
|
|
489
606
|
);
|
|
490
607
|
const setFill = useCallback(
|
|
491
608
|
(f: string | null) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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,
|
|
618
|
+
[store, selectedIds]
|
|
500
619
|
);
|
|
501
620
|
const setThickness = useCallback(
|
|
502
621
|
(w: number) => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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,
|
|
636
|
+
[store, selectedIds]
|
|
512
637
|
);
|
|
513
638
|
const setFontSize = useCallback(
|
|
514
639
|
(sz: number) => {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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,
|
|
648
|
+
[store, selectedIds]
|
|
524
649
|
);
|
|
525
|
-
// Phase 24 — bold / strike / align on text +
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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,
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
560
|
-
const
|
|
561
|
-
(
|
|
562
|
-
|
|
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 H — convert 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,
|
|
692
|
+
[store, selectedIds]
|
|
570
693
|
);
|
|
571
|
-
// Phase 24 — per-end arrowhead + line-type
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
697
|
+
store?.applyToStrokes(
|
|
698
|
+
selectedIds,
|
|
699
|
+
(s) => (s.tool === 'arrow' ? ({ startHead } as Partial<Stroke>) : null),
|
|
700
|
+
'set arrowhead'
|
|
701
|
+
);
|
|
579
702
|
},
|
|
580
|
-
[store,
|
|
703
|
+
[store, selectedIds]
|
|
581
704
|
);
|
|
582
705
|
const setEndHead = useCallback(
|
|
583
706
|
(endHead: ArrowHead) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
707
|
+
store?.applyToStrokes(
|
|
708
|
+
selectedIds,
|
|
709
|
+
(s) => (s.tool === 'arrow' ? ({ endHead } as Partial<Stroke>) : null),
|
|
710
|
+
'set arrowhead'
|
|
711
|
+
);
|
|
588
712
|
},
|
|
589
|
-
[store,
|
|
713
|
+
[store, selectedIds]
|
|
590
714
|
);
|
|
591
715
|
const setLineType = useCallback(
|
|
592
716
|
(lineType: ArrowLineType) => {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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,
|
|
723
|
+
[store, selectedIds]
|
|
599
724
|
);
|
|
600
|
-
//
|
|
725
|
+
// Dash toggle — arrow + rect / ellipse / polygon (item 7).
|
|
601
726
|
const setDashed = useCallback(
|
|
602
727
|
(dashed: boolean) => {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
[
|
|
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'
|
|
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' ||
|
|
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
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
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) =>
|
|
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="
|
|
849
|
-
aria-pressed={
|
|
850
|
-
title="
|
|
851
|
-
onClick={() =>
|
|
1235
|
+
aria-label="Bulleted list"
|
|
1236
|
+
aria-pressed={uniqListType === 'bullet'}
|
|
1237
|
+
title="Bulleted list"
|
|
1238
|
+
onClick={() => setListType(uniqListType === 'bullet' ? undefined : 'bullet')}
|
|
852
1239
|
>
|
|
853
|
-
<
|
|
1240
|
+
<IconListBullet />
|
|
854
1241
|
</button>
|
|
855
1242
|
<button
|
|
856
1243
|
type="button"
|
|
857
1244
|
className="dc-annot-ctx-ibtn"
|
|
858
|
-
aria-label="
|
|
859
|
-
aria-pressed={
|
|
860
|
-
title="
|
|
861
|
-
onClick={() =>
|
|
1245
|
+
aria-label="Numbered list"
|
|
1246
|
+
aria-pressed={uniqListType === 'number'}
|
|
1247
|
+
title="Numbered list"
|
|
1248
|
+
onClick={() => setListType(uniqListType === 'number' ? undefined : 'number')}
|
|
862
1249
|
>
|
|
863
|
-
<
|
|
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
|