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