@1agh/maude 0.28.1 → 0.29.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 +2 -2
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +285 -5
- package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1206 -119
- package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
- 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/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 +438 -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 +5 -3
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
- 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 +46 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -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 +56 -41
- package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +459 -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 +357 -34
- package/apps/studio/client/styles/5-maude-overrides.css +123 -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 +191 -0
- package/apps/studio/client/tour/usage-tour.js +35 -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 +1 -1
- 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 +1 -1
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
- package/apps/studio/dist/client.bundle.js +22825 -0
- package/apps/studio/dist/comment-mount.js +1960 -0
- package/apps/studio/dist/styles.css +5695 -0
- package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
- 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 +13 -1
- package/{plugins/design/dev-server → apps/studio}/http.ts +102 -1
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +18 -0
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +1 -1
- 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-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/{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 +17 -1
- package/apps/studio/test/canvas-url.test.ts +99 -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/tour-overlay.test.tsx +56 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
- 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 +4 -69
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +40 -7
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +1 -1
- 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 +1 -1
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +1 -1
- 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 +9 -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 +3 -1
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +139 -0
- package/apps/studio/whats-new.schema.json +70 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design.mjs +15 -7
- package/cli/commands/design.test.mjs +36 -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/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/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/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 → 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/_png-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-edit.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}/history.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/agent.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/connection-state.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/migrate-seed.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/status.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/activity.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-edit.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-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-room.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/csp-canvas-shell.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-migrate.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-agent.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-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.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-hardening.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.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-runtime.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-status.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-artboard-drag.test.ts +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-collab.test.ts +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,172 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
type CubicBezierHandles,
|
|
4
|
+
easeBezier,
|
|
5
|
+
LINEAR,
|
|
6
|
+
lerpValue,
|
|
7
|
+
lerpVertices,
|
|
8
|
+
parallel,
|
|
9
|
+
resolveStagger,
|
|
10
|
+
sampleTrack,
|
|
11
|
+
sequence,
|
|
12
|
+
shiftTrack,
|
|
13
|
+
stagger,
|
|
14
|
+
type Track,
|
|
15
|
+
timeline,
|
|
16
|
+
track,
|
|
17
|
+
trackSpan,
|
|
18
|
+
} from '../animate.ts';
|
|
19
|
+
|
|
20
|
+
describe('easeBezier', () => {
|
|
21
|
+
test('linear is the identity', () => {
|
|
22
|
+
for (const x of [0, 0.25, 0.5, 0.75, 1]) {
|
|
23
|
+
expect(easeBezier(LINEAR, x)).toBeCloseTo(x, 6);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
test('clamps the endpoints exactly', () => {
|
|
27
|
+
const e: CubicBezierHandles = [0.42, 0, 0.58, 1];
|
|
28
|
+
expect(easeBezier(e, 0)).toBe(0);
|
|
29
|
+
expect(easeBezier(e, 1)).toBe(1);
|
|
30
|
+
expect(easeBezier(e, -0.5)).toBe(0);
|
|
31
|
+
expect(easeBezier(e, 1.5)).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
test('ease-in-out is symmetric about the midpoint and monotone', () => {
|
|
34
|
+
const e: CubicBezierHandles = [0.42, 0, 0.58, 1];
|
|
35
|
+
expect(easeBezier(e, 0.5)).toBeCloseTo(0.5, 4);
|
|
36
|
+
let prev = -1;
|
|
37
|
+
for (let i = 0; i <= 20; i++) {
|
|
38
|
+
const y = easeBezier(e, i / 20);
|
|
39
|
+
expect(y).toBeGreaterThanOrEqual(prev - 1e-9);
|
|
40
|
+
prev = y;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
test('preserves overshoot (y > 1) for a snap easing', () => {
|
|
44
|
+
const snap: CubicBezierHandles = [0.34, 1.42, 0.64, 1];
|
|
45
|
+
let maxY = 0;
|
|
46
|
+
for (let i = 0; i <= 100; i++) maxY = Math.max(maxY, easeBezier(snap, i / 100));
|
|
47
|
+
expect(maxY).toBeGreaterThan(1); // the overshoot the snap depends on
|
|
48
|
+
});
|
|
49
|
+
test('round-trips a known x→t→y solve (ease-out)', () => {
|
|
50
|
+
// cubic-bezier(0,0,0.58,1): solving x then evaluating y must land in (x,1).
|
|
51
|
+
const e: CubicBezierHandles = [0, 0, 0.58, 1];
|
|
52
|
+
const y = easeBezier(e, 0.5);
|
|
53
|
+
expect(y).toBeGreaterThan(0.5); // ease-out is ahead of linear at the midpoint
|
|
54
|
+
expect(y).toBeLessThan(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('lerpValue / lerpVertices', () => {
|
|
59
|
+
test('numbers lerp', () => {
|
|
60
|
+
expect(lerpValue(0, 10, 0.3)).toBeCloseTo(3, 6);
|
|
61
|
+
});
|
|
62
|
+
test('number arrays lerp element-wise', () => {
|
|
63
|
+
expect(lerpValue([0, 10], [10, 0], 0.5)).toEqual([5, 5]);
|
|
64
|
+
});
|
|
65
|
+
test('vertex arrays lerp by component', () => {
|
|
66
|
+
const out = lerpVertices([{ x: 0, y: 0 }], [{ x: 4, y: 8 }], 0.25);
|
|
67
|
+
expect(out).toEqual([{ x: 1, y: 2 }]);
|
|
68
|
+
});
|
|
69
|
+
test('strings are a discrete hold (no interpolation)', () => {
|
|
70
|
+
expect(lerpValue('rotate(0)', 'rotate(90)', 0.5)).toBe('rotate(0)');
|
|
71
|
+
expect(lerpValue('rotate(0)', 'rotate(90)', 1)).toBe('rotate(90)');
|
|
72
|
+
});
|
|
73
|
+
test('vertex count mismatch throws (interpolability constraint)', () => {
|
|
74
|
+
expect(() => lerpVertices([{ x: 0, y: 0 }], [], 0.5)).toThrow(/vertex count mismatch/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('sampleTrack', () => {
|
|
79
|
+
const tr = track('opacity', [
|
|
80
|
+
{ t: 0, value: 0, ease: LINEAR },
|
|
81
|
+
{ t: 1, value: 1 },
|
|
82
|
+
]);
|
|
83
|
+
test('holds before the first and after the last keyframe', () => {
|
|
84
|
+
expect(sampleTrack(tr, -1)).toBe(0);
|
|
85
|
+
expect(sampleTrack(tr, 5)).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
test('linear interpolation mid-segment', () => {
|
|
88
|
+
expect(sampleTrack(tr, 0.5)).toBeCloseTo(0.5, 6);
|
|
89
|
+
});
|
|
90
|
+
test('applies the leading keyframe easing', () => {
|
|
91
|
+
const eased = track('opacity', [
|
|
92
|
+
{ t: 0, value: 0, ease: [0, 0, 0.58, 1] }, // ease-out
|
|
93
|
+
{ t: 1, value: 1 },
|
|
94
|
+
]);
|
|
95
|
+
expect(sampleTrack(eased, 0.5) as number).toBeGreaterThan(0.5);
|
|
96
|
+
});
|
|
97
|
+
test('empty track samples undefined', () => {
|
|
98
|
+
expect(sampleTrack(track('opacity', []), 0)).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('combinators', () => {
|
|
103
|
+
const a = (): Track =>
|
|
104
|
+
track(
|
|
105
|
+
'opacity',
|
|
106
|
+
[
|
|
107
|
+
{ t: 0, value: 0 },
|
|
108
|
+
{ t: 1, value: 1 },
|
|
109
|
+
],
|
|
110
|
+
'a'
|
|
111
|
+
);
|
|
112
|
+
const b = (): Track =>
|
|
113
|
+
track(
|
|
114
|
+
'opacity',
|
|
115
|
+
[
|
|
116
|
+
{ t: 0, value: 1 },
|
|
117
|
+
{ t: 2, value: 0 },
|
|
118
|
+
],
|
|
119
|
+
'b'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
test('trackSpan is the latest keyframe time', () => {
|
|
123
|
+
expect(trackSpan([a(), b()])).toBe(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('shiftTrack offsets every keyframe', () => {
|
|
127
|
+
const s = shiftTrack(a(), 3);
|
|
128
|
+
expect(s.keyframes.map((k) => k.t)).toEqual([3, 4]);
|
|
129
|
+
expect(a().keyframes[0].t).toBe(0); // immutable
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('stagger offsets track i by i·delay', () => {
|
|
133
|
+
const out = stagger([a(), b(), a()], 0.5);
|
|
134
|
+
expect(out[0].keyframes[0].t).toBe(0);
|
|
135
|
+
expect(out[1].keyframes[0].t).toBe(0.5);
|
|
136
|
+
expect(out[2].keyframes[0].t).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('resolveStagger bakes the declarative field and grows dur', () => {
|
|
140
|
+
const tl = timeline({ tracks: [a(), b()], dur: 2, stagger: 0.5 });
|
|
141
|
+
const r = resolveStagger(tl);
|
|
142
|
+
expect(r.stagger).toBeUndefined();
|
|
143
|
+
expect(r.tracks[1].keyframes[0].t).toBe(0.5);
|
|
144
|
+
expect(r.dur).toBe(2.5); // b ended at 2 → shifted to 2.5
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('parallel unions tracks and takes the max duration', () => {
|
|
148
|
+
const tl = parallel(
|
|
149
|
+
timeline({ tracks: [a()], dur: 1 }),
|
|
150
|
+
timeline({ tracks: [b()], dur: 2, begin: 0.5 })
|
|
151
|
+
);
|
|
152
|
+
expect(tl.tracks.length).toBe(2);
|
|
153
|
+
expect(tl.dur).toBe(2.5); // 0.5 begin + 2 dur
|
|
154
|
+
expect(tl.tracks[1].keyframes[0].t).toBe(0.5); // b shifted by its begin
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('sequence chains timelines end-to-start and sums durations', () => {
|
|
158
|
+
const tl = sequence(timeline({ tracks: [a()], dur: 1 }), timeline({ tracks: [b()], dur: 2 }));
|
|
159
|
+
expect(tl.dur).toBe(3);
|
|
160
|
+
expect(tl.tracks[0].keyframes.map((k) => k.t)).toEqual([0, 1]);
|
|
161
|
+
expect(tl.tracks[1].keyframes.map((k) => k.t)).toEqual([1, 3]); // shifted by first dur
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('sequence honors a per-timeline begin gap', () => {
|
|
165
|
+
const tl = sequence(
|
|
166
|
+
timeline({ tracks: [a()], dur: 1 }),
|
|
167
|
+
timeline({ tracks: [a()], dur: 1, begin: 0.5 })
|
|
168
|
+
);
|
|
169
|
+
expect(tl.tracks[1].keyframes[0].t).toBe(1.5); // 1 (cursor) + 0.5 (gap)
|
|
170
|
+
expect(tl.dur).toBe(2.5);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { sampleTrack } from '../animate.ts';
|
|
3
|
+
import { blobPath } from '../geometry.ts';
|
|
4
|
+
import { morphVariants, parseMorphPath, templateToPath } from '../morph.ts';
|
|
5
|
+
import type { Point } from '../primitives.ts';
|
|
6
|
+
|
|
7
|
+
describe('parseMorphPath', () => {
|
|
8
|
+
test('extracts vertices + a command template (M…C…Z)', () => {
|
|
9
|
+
const { template, vertices } = parseMorphPath('M0 0C1 1 2 2 3 3Z');
|
|
10
|
+
expect(template.commands.map((c) => c.cmd)).toEqual(['M', 'C', 'Z']);
|
|
11
|
+
expect(template.vertexCount).toBe(4); // M=1 + C=3
|
|
12
|
+
expect(vertices).toEqual([
|
|
13
|
+
{ x: 0, y: 0 },
|
|
14
|
+
{ x: 1, y: 1 },
|
|
15
|
+
{ x: 2, y: 2 },
|
|
16
|
+
{ x: 3, y: 3 },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
test('round-trips through templateToPath', () => {
|
|
20
|
+
const d = 'M0 0L10 0L10 10L0 10Z';
|
|
21
|
+
const { template, vertices } = parseMorphPath(d);
|
|
22
|
+
expect(templateToPath(template, vertices)).toBe(d);
|
|
23
|
+
});
|
|
24
|
+
test('parses negative + decimal + exponent numbers', () => {
|
|
25
|
+
const { vertices } = parseMorphPath('M-1.5 2e1L3 -4.25');
|
|
26
|
+
expect(vertices).toEqual([
|
|
27
|
+
{ x: -1.5, y: 20 },
|
|
28
|
+
{ x: 3, y: -4.25 },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
test('rejects non-morph-safe commands (H/V/A)', () => {
|
|
32
|
+
expect(() => parseMorphPath('M0 0H10')).toThrow(/morph-safe/);
|
|
33
|
+
expect(() => parseMorphPath('M0 0A5 5 0 0 1 10 10')).toThrow(/morph-safe/);
|
|
34
|
+
});
|
|
35
|
+
test('rejects relative (lowercase) commands', () => {
|
|
36
|
+
expect(() => parseMorphPath('M0 0l10 10')).toThrow(/morph-safe/);
|
|
37
|
+
});
|
|
38
|
+
test('templateToPath enforces the vertex-count invariant', () => {
|
|
39
|
+
const { template } = parseMorphPath('M0 0L1 1');
|
|
40
|
+
expect(() => templateToPath(template, [{ x: 0, y: 0 }])).toThrow(/vertex count/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('morphVariants', () => {
|
|
45
|
+
const base = blobPath(50, 50, 30, { lobes: 7, seed: 3 });
|
|
46
|
+
|
|
47
|
+
test('every keyframe shares the fixed vertex count (interpolability)', () => {
|
|
48
|
+
const { track, template } = morphVariants(base, { n: 4, jitter: 3, seed: 7 });
|
|
49
|
+
for (const kf of track.keyframes) {
|
|
50
|
+
expect((kf.value as Point[]).length).toBe(template.vertexCount);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('is deterministic for a given seed', () => {
|
|
55
|
+
const a = morphVariants(base, { n: 3, jitter: 4, seed: 9 });
|
|
56
|
+
const b = morphVariants(base, { n: 3, jitter: 4, seed: 9 });
|
|
57
|
+
expect(a.track.keyframes).toEqual(b.track.keyframes);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('different seeds produce different jitter', () => {
|
|
61
|
+
const a = morphVariants(base, { n: 3, jitter: 4, seed: 1 });
|
|
62
|
+
const b = morphVariants(base, { n: 3, jitter: 4, seed: 2 });
|
|
63
|
+
expect(a.track.keyframes[1].value).not.toEqual(b.track.keyframes[1].value);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('jitter 0 keeps every keyframe equal to the base shape', () => {
|
|
67
|
+
const { track } = morphVariants(base, { n: 3, jitter: 0, seed: 5 });
|
|
68
|
+
const first = track.keyframes[0].value;
|
|
69
|
+
for (const kf of track.keyframes) expect(kf.value).toEqual(first);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('loop (default) returns to the base on the final keyframe', () => {
|
|
73
|
+
const { track } = morphVariants(base, { n: 3, jitter: 5, seed: 5 });
|
|
74
|
+
const kfs = track.keyframes;
|
|
75
|
+
expect(kfs[kfs.length - 1].value).toEqual(kfs[0].value); // seamless repeat
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('loop:false drops the closing base keyframe', () => {
|
|
79
|
+
const loop = morphVariants(base, { n: 3, jitter: 5, loop: true });
|
|
80
|
+
const open = morphVariants(base, { n: 3, jitter: 5, loop: false });
|
|
81
|
+
expect(open.track.keyframes.length).toBe(loop.track.keyframes.length - 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('keyframes span [0, dur] evenly', () => {
|
|
85
|
+
const { track } = morphVariants(base, { n: 2, dur: 2 });
|
|
86
|
+
const ts = track.keyframes.map((k) => k.t);
|
|
87
|
+
expect(ts[0]).toBe(0);
|
|
88
|
+
expect(ts[ts.length - 1]).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('sampling + toPath yields a valid morph-safe d at mid-timeline', () => {
|
|
92
|
+
const { track, toPath } = morphVariants(base, { n: 3, jitter: 4, dur: 1, seed: 2 });
|
|
93
|
+
const mid = sampleTrack(track, 0.5) as Point[];
|
|
94
|
+
const d = toPath(mid);
|
|
95
|
+
expect(d.startsWith('M')).toBe(true);
|
|
96
|
+
// Re-parsing the rendered frame must keep the same vertex count.
|
|
97
|
+
expect(() => parseMorphPath(d)).not.toThrow();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('ease is applied to all but the last keyframe', () => {
|
|
101
|
+
const ease = [0.34, 1.42, 0.64, 1] as const;
|
|
102
|
+
const { track } = morphVariants(base, { n: 2, ease });
|
|
103
|
+
const kfs = track.keyframes;
|
|
104
|
+
for (let i = 0; i < kfs.length - 1; i++) expect(kfs[i].ease).toEqual(ease);
|
|
105
|
+
expect(kfs[kfs.length - 1].ease).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { parallel, type Timeline, timeline, track } from '../animate.ts';
|
|
3
|
+
import { morphVariants } from '../morph.ts';
|
|
4
|
+
import { circle, type DrawPrimitive, path } from '../primitives.ts';
|
|
5
|
+
import { buildAnimPlan, toAnimatedJsx, toAnimatedSvg } from '../serialize-animate.ts';
|
|
6
|
+
|
|
7
|
+
// An animated mark: a morphing blob + a blinking eye + a rotating blob.
|
|
8
|
+
const blob = morphVariants('M10 50L50 10L90 50L50 90Z', {
|
|
9
|
+
n: 3,
|
|
10
|
+
jitter: 5,
|
|
11
|
+
seed: 4,
|
|
12
|
+
dur: 1.2,
|
|
13
|
+
ease: [0.34, 1.42, 0.64, 1], // snap with overshoot
|
|
14
|
+
target: 'blob',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const PRIMS: DrawPrimitive[] = [
|
|
18
|
+
path({ d: 'M10 50L50 10L90 50L50 90Z', id: 'blob', fill: '#e94' }),
|
|
19
|
+
circle({ cx: 50, cy: 45, r: 6, id: 'eye', fill: '#222' }),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const TL: Timeline = parallel(
|
|
23
|
+
timeline({ tracks: [blob.track], repeat: 'indefinite' }),
|
|
24
|
+
timeline({
|
|
25
|
+
tracks: [
|
|
26
|
+
track(
|
|
27
|
+
'opacity',
|
|
28
|
+
[
|
|
29
|
+
{ t: 0, value: 1, ease: [0.42, 0, 0.58, 1] },
|
|
30
|
+
{ t: 0.5, value: 0.1 },
|
|
31
|
+
{ t: 1, value: 1 },
|
|
32
|
+
],
|
|
33
|
+
'eye'
|
|
34
|
+
),
|
|
35
|
+
track(
|
|
36
|
+
'rotate',
|
|
37
|
+
[
|
|
38
|
+
{ t: 0, value: 0 },
|
|
39
|
+
{ t: 1.2, value: 360 },
|
|
40
|
+
],
|
|
41
|
+
'blob'
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
repeat: 'indefinite',
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const OPTS = { viewBox: '0 0 100 100', pathRenderers: { blob: blob.toPath } };
|
|
49
|
+
|
|
50
|
+
describe('buildAnimPlan', () => {
|
|
51
|
+
const plan = buildAnimPlan(TL, OPTS);
|
|
52
|
+
test('one entry per targeted, ≥2-keyframe track', () => {
|
|
53
|
+
expect(plan.length).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
test('d-morph maps to <animate attributeName=d>', () => {
|
|
56
|
+
const d = plan.find((e) => e.property === 'd');
|
|
57
|
+
expect(d?.kind).toBe('animate');
|
|
58
|
+
expect(d?.attr).toBe('d');
|
|
59
|
+
expect(d?.values[0].startsWith('M')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
test('rotate maps to animateTransform', () => {
|
|
62
|
+
const r = plan.find((e) => e.property === 'rotate');
|
|
63
|
+
expect(r?.kind).toBe('animateTransform');
|
|
64
|
+
expect(r?.attr).toBe('rotate');
|
|
65
|
+
});
|
|
66
|
+
test('keyTimes are normalized 0→1', () => {
|
|
67
|
+
for (const e of plan) {
|
|
68
|
+
expect(e.keyTimes[0]).toBe(0);
|
|
69
|
+
expect(e.keyTimes[e.keyTimes.length - 1]).toBe(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
test('eased track carries n-1 keySplines', () => {
|
|
73
|
+
const op = plan.find((e) => e.property === 'opacity');
|
|
74
|
+
expect(op).toBeDefined();
|
|
75
|
+
expect(op?.keySplines?.length).toBe((op?.values.length ?? 0) - 1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('toAnimatedSvg (SMIL)', () => {
|
|
80
|
+
const svg = toAnimatedSvg(PRIMS, TL, OPTS);
|
|
81
|
+
test('injects SMIL d-morph with spline easing', () => {
|
|
82
|
+
expect(svg).toContain('<animate attributeName="d"');
|
|
83
|
+
expect(svg).toContain('calcMode="spline"');
|
|
84
|
+
expect(svg).toContain('keySplines=');
|
|
85
|
+
});
|
|
86
|
+
test('rotate is an additive animateTransform (no static-transform clobber)', () => {
|
|
87
|
+
expect(svg).toMatch(/<animateTransform[^>]*type="rotate"[^>]*additive="sum"/);
|
|
88
|
+
});
|
|
89
|
+
test('preserves the overshoot handle verbatim in keySplines', () => {
|
|
90
|
+
expect(svg).toContain('0.34 1.42 0.64 1');
|
|
91
|
+
});
|
|
92
|
+
test('emits a prefers-reduced-motion gate', () => {
|
|
93
|
+
expect(svg).toContain('prefers-reduced-motion: reduce');
|
|
94
|
+
});
|
|
95
|
+
test('is a complete SVG document', () => {
|
|
96
|
+
expect(svg).toContain('xmlns="http://www.w3.org/2000/svg"');
|
|
97
|
+
expect(svg).toContain('repeatCount="indefinite"');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('toAnimatedJsx (single-source parity)', () => {
|
|
102
|
+
const svg = toAnimatedSvg(PRIMS, TL, OPTS);
|
|
103
|
+
const jsx = toAnimatedJsx(PRIMS, TL, OPTS);
|
|
104
|
+
const count = (s: string, re: RegExp) => (s.match(re) ?? []).length;
|
|
105
|
+
|
|
106
|
+
test('same count of <animate> elements as the SVG form', () => {
|
|
107
|
+
expect(count(jsx, /<animate /g)).toBe(count(svg, /<animate /g));
|
|
108
|
+
});
|
|
109
|
+
test('same count of <animateTransform> elements as the SVG form', () => {
|
|
110
|
+
expect(count(jsx, /<animateTransform /g)).toBe(count(svg, /<animateTransform /g));
|
|
111
|
+
});
|
|
112
|
+
test('omits the xmlns document marker', () => {
|
|
113
|
+
expect(jsx).not.toContain('xmlns=');
|
|
114
|
+
});
|
|
115
|
+
test('keeps the SMIL animation attributes (host renders SMIL)', () => {
|
|
116
|
+
expect(jsx).toContain('attributeName="d"');
|
|
117
|
+
expect(jsx).toContain('keyTimes=');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('fail-loud', () => {
|
|
122
|
+
test('a track targeting a missing id throws', () => {
|
|
123
|
+
const bad = timeline({
|
|
124
|
+
tracks: [
|
|
125
|
+
track(
|
|
126
|
+
'opacity',
|
|
127
|
+
[
|
|
128
|
+
{ t: 0, value: 0 },
|
|
129
|
+
{ t: 1, value: 1 },
|
|
130
|
+
],
|
|
131
|
+
'ghost'
|
|
132
|
+
),
|
|
133
|
+
],
|
|
134
|
+
repeat: 1,
|
|
135
|
+
});
|
|
136
|
+
expect(() => toAnimatedSvg(PRIMS, bad, { viewBox: '0 0 100 100' })).toThrow(/not found/);
|
|
137
|
+
});
|
|
138
|
+
test('a d-morph track without a pathRenderer throws', () => {
|
|
139
|
+
const tl = timeline({ tracks: [blob.track], repeat: 1 });
|
|
140
|
+
expect(() => toAnimatedSvg(PRIMS, tl, { viewBox: '0 0 100 100' })).toThrow(/pathRenderer/);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file equal-spacing-detector.ts — T27 (Wave 3)
|
|
3
|
-
* @scope
|
|
3
|
+
* @scope apps/studio/equal-spacing-detector.ts
|
|
4
4
|
* @purpose Pure detector for Figma "Smart Selection" pink-dot affordance
|
|
5
5
|
* (Rasmus Andersson 2018). Given 3+ rects on a single axis,
|
|
6
6
|
* returns the equal gap + the screen-coord midpoints between
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file equal-spacing-handles.tsx — T27 (Wave 3)
|
|
3
|
-
* @scope
|
|
3
|
+
* @scope apps/studio/equal-spacing-handles.tsx
|
|
4
4
|
* @purpose Figma Smart Selection pink-dot affordance. When 3+ elements are
|
|
5
5
|
* selected AND their bounding rects are equally distributed on an
|
|
6
6
|
* axis, render small pink dots at the midpoints between adjacent
|
|
@@ -14,11 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
type EqualSpacingResult,
|
|
20
|
-
type SpacingAxis,
|
|
21
|
-
} from './equal-spacing-detector.ts';
|
|
17
|
+
import { resolveSelectionEl } from './dom-selection.ts';
|
|
18
|
+
import { detectEqualSpacing, type EqualSpacingResult } from './equal-spacing-detector.ts';
|
|
22
19
|
import { useSelectionSet } from './use-selection-set.tsx';
|
|
23
20
|
|
|
24
21
|
const HOVER_PADDING_PX = 40;
|
|
@@ -77,25 +74,6 @@ function ensureStyles(): void {
|
|
|
77
74
|
document.head.appendChild(s);
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
function cssEscape(s: string): string {
|
|
81
|
-
// CSS.escape is available everywhere we care; manual fallback handles the
|
|
82
|
-
// jsdom test path where it may be missing.
|
|
83
|
-
// biome-ignore lint/suspicious/noExplicitAny: feature detect
|
|
84
|
-
const ce = (typeof CSS !== 'undefined' ? (CSS as any).escape : null) as
|
|
85
|
-
| ((v: string) => string)
|
|
86
|
-
| null;
|
|
87
|
-
if (ce) return ce(s);
|
|
88
|
-
return s.replace(/(["\\\]])/g, '\\$1');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function safeQuery(selector: string): Element | null {
|
|
92
|
-
try {
|
|
93
|
-
return document.querySelector(selector);
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
77
|
interface ScreenRect {
|
|
100
78
|
x: number;
|
|
101
79
|
y: number;
|
|
@@ -103,12 +81,12 @@ interface ScreenRect {
|
|
|
103
81
|
h: number;
|
|
104
82
|
}
|
|
105
83
|
|
|
106
|
-
function resolveSelectionRects(
|
|
84
|
+
function resolveSelectionRects(
|
|
85
|
+
sels: Array<{ id?: string; selector: string; artboardId?: string | null }>
|
|
86
|
+
): ScreenRect[] {
|
|
107
87
|
const rects: ScreenRect[] = [];
|
|
108
88
|
for (const s of sels) {
|
|
109
|
-
const el = s
|
|
110
|
-
? document.querySelector(`[data-cd-id="${cssEscape(s.id)}"]`)
|
|
111
|
-
: safeQuery(s.selector);
|
|
89
|
+
const el = resolveSelectionEl(document, s);
|
|
112
90
|
if (!el) continue;
|
|
113
91
|
const b = (el as HTMLElement).getBoundingClientRect();
|
|
114
92
|
if (b.width === 0 && b.height === 0) continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file export-dialog.tsx — Phase 6.5 T8 export dialog.
|
|
3
|
-
* @scope
|
|
3
|
+
* @scope apps/studio/export-dialog.tsx
|
|
4
4
|
* @purpose Native `<dialog>`-based export modal. Three controls — format,
|
|
5
5
|
* scope, per-format options — plus a Recent tab populated by
|
|
6
6
|
* `/_api/export-history`. Submit fires `POST /_api/export`, the
|
|
@@ -339,6 +339,18 @@ export function ExportDialogProvider({ children }: { children: ReactNode }): Rea
|
|
|
339
339
|
useEffect(() => {
|
|
340
340
|
function onCustom(e: Event) {
|
|
341
341
|
const detail = (e as CustomEvent<{ scope?: Scope; format?: Format }>).detail ?? {};
|
|
342
|
+
// Plan C — route to the UNIFIED shell Export dialog (parent / main origin)
|
|
343
|
+
// so the in-canvas toolbar + context menu open the SAME modal as the
|
|
344
|
+
// menubar. Only fall back to this local dialog for a standalone canvas
|
|
345
|
+
// (handoff / exported embed) with no shell parent.
|
|
346
|
+
if (window.parent && window.parent !== window) {
|
|
347
|
+
try {
|
|
348
|
+
window.parent.postMessage({ dgn: 'open-export', detail }, '*');
|
|
349
|
+
return;
|
|
350
|
+
} catch {
|
|
351
|
+
/* fall through to local */
|
|
352
|
+
}
|
|
353
|
+
}
|
|
342
354
|
open(detail);
|
|
343
355
|
}
|
|
344
356
|
window.addEventListener('maude:open-export', onCustom as EventListener);
|
|
@@ -20,6 +20,7 @@ import type { Inspect } from './inspect.ts';
|
|
|
20
20
|
import { canvasSlug, writeLocator } from './locator.ts';
|
|
21
21
|
import { DEV_SERVER_ROOT } from './paths.ts';
|
|
22
22
|
import { getRuntimeBundle, packageForSlug, RUNTIME_PACKAGES, slugFor } from './runtime-bundle.ts';
|
|
23
|
+
import { loadWhatsNew } from './whats-new.ts';
|
|
23
24
|
|
|
24
25
|
// Real disk install root — never the virtual `/$bunfs/root` of compiled bins.
|
|
25
26
|
// See paths.ts for the resolution logic + Phase 19.1 / v0.18.1 rationale.
|
|
@@ -131,7 +132,10 @@ function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
|
|
|
131
132
|
// and inside the standalone binary's embedded FS in --compile mode.
|
|
132
133
|
const DIST_DIR = join(HERE, 'dist');
|
|
133
134
|
const CLIENT_DIR = join(HERE, 'client');
|
|
134
|
-
|
|
135
|
+
// Templates stayed under the design *plugin* (plugins/design/templates) when the
|
|
136
|
+
// dev-server moved to apps/studio (DDR-095) — both ship in the npm tarball, so
|
|
137
|
+
// from DEV_SERVER_ROOT (apps/studio) reach across with ../../plugins/design.
|
|
138
|
+
const TEMPLATES_DIR = join(HERE, '..', '..', 'plugins', 'design', 'templates');
|
|
135
139
|
|
|
136
140
|
// In-memory transpile cache. Key = absolute canvas path. Repeat GETs against an
|
|
137
141
|
// unchanged source skip the parse + ID-injection + Bun.Transpiler + Bun.build
|
|
@@ -424,6 +428,12 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
424
428
|
|
|
425
429
|
'/_config': () => Response.json({ ...ctx.cfg, canvasOrigin: ctx.canvasOrigin }),
|
|
426
430
|
|
|
431
|
+
// What's New feed (DDR-A) — read-only product-update list surfaced in the
|
|
432
|
+
// Maude UI chrome. Main-origin only by omission from the canvas-origin
|
|
433
|
+
// allowlist below (the untrusted canvas iframe never needs it).
|
|
434
|
+
'/_api/whats-new': () =>
|
|
435
|
+
Response.json(loadWhatsNew(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
436
|
+
|
|
427
437
|
'/_index-data': async () =>
|
|
428
438
|
Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
429
439
|
|
|
@@ -582,6 +592,91 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
582
592
|
return new Response('Method not allowed', { status: 405 });
|
|
583
593
|
},
|
|
584
594
|
|
|
595
|
+
'/_api/canvas': async (req: Request) => {
|
|
596
|
+
// Phase 22 — create + soft-delete a canvas from the browser file tree.
|
|
597
|
+
// POST body { name, kind?: "brief-board", group? } → 201 { file, rel, slug }
|
|
598
|
+
// DELETE ?file=<rel> → 200 { rel, slug, trashed[] }
|
|
599
|
+
// MAIN ORIGIN ONLY: this route is intentionally absent from
|
|
600
|
+
// startCanvasServer's allowlist (DDR-054) — the untrusted canvas iframe
|
|
601
|
+
// origin must never reach a file-write/-delete endpoint. Validation lives in
|
|
602
|
+
// api.createCanvas / api.deleteCanvas (containment + group allowlist).
|
|
603
|
+
if (req.method === 'DELETE') {
|
|
604
|
+
const file = new URL(req.url).searchParams.get('file');
|
|
605
|
+
const result = await api.deleteCanvas({ file });
|
|
606
|
+
if (!result.ok) {
|
|
607
|
+
return Response.json(
|
|
608
|
+
{ ok: false, error: result.error },
|
|
609
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return Response.json(
|
|
613
|
+
{
|
|
614
|
+
ok: true,
|
|
615
|
+
rel: result.rel,
|
|
616
|
+
slug: result.slug,
|
|
617
|
+
trashed: result.trashed,
|
|
618
|
+
trashDir: result.trashDir,
|
|
619
|
+
},
|
|
620
|
+
{ status: 200, headers: { 'Cache-Control': 'no-store' } }
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
624
|
+
const body = await readJson<{ name?: unknown; kind?: unknown; group?: unknown }>(
|
|
625
|
+
req,
|
|
626
|
+
8 * 1024
|
|
627
|
+
);
|
|
628
|
+
if (!body) return new Response('body required', { status: 400 });
|
|
629
|
+
const result = await api.createCanvas(body);
|
|
630
|
+
if (!result.ok) {
|
|
631
|
+
return Response.json(
|
|
632
|
+
{ ok: false, error: result.error },
|
|
633
|
+
{ status: result.status, headers: { 'Cache-Control': 'no-store' } }
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return Response.json(
|
|
637
|
+
{ ok: true, file: result.file, rel: result.rel, slug: result.slug },
|
|
638
|
+
{ status: 201, headers: { 'Cache-Control': 'no-store' } }
|
|
639
|
+
);
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
'/_api/asset': async (req: Request) => {
|
|
643
|
+
// Phase 23 — binary image upload from the canvas (drag-drop / paste / the
|
|
644
|
+
// a11y file picker). POST raw image bytes → content-addressed write under
|
|
645
|
+
// <designRoot>/assets/<sha8>.<ext>, returns 201 { path }. This route is on
|
|
646
|
+
// the canvas-origin allowlist (CANVAS_SAFE_API below) — a bigger grant than
|
|
647
|
+
// the inert annotation-SVG write (binary, disk) — so the caps in
|
|
648
|
+
// api.saveAsset (magic-byte sniff, 10 MB ceiling, content-addressed name,
|
|
649
|
+
// traversal guard, no-SVG, dedupe) are the load-bearing trust mitigation,
|
|
650
|
+
// NOT optional. See DDR (Task 9).
|
|
651
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
652
|
+
// Early reject on a declared oversize body (the post-read length check in
|
|
653
|
+
// saveAsset is the authoritative gate — Content-Length can be omitted/lied).
|
|
654
|
+
const declared = Number(req.headers.get('content-length') || '0');
|
|
655
|
+
if (Number.isFinite(declared) && declared > 10 * 1024 * 1024) {
|
|
656
|
+
return Response.json(
|
|
657
|
+
{ ok: false, error: 'asset exceeds the 10 MB cap' },
|
|
658
|
+
{ status: 413, headers: { 'Cache-Control': 'no-store' } }
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
let bytes: Uint8Array;
|
|
662
|
+
try {
|
|
663
|
+
bytes = new Uint8Array(await req.arrayBuffer());
|
|
664
|
+
} catch {
|
|
665
|
+
return new Response('could not read request body', { status: 400 });
|
|
666
|
+
}
|
|
667
|
+
const result = await api.saveAsset(bytes);
|
|
668
|
+
if (!result.ok) {
|
|
669
|
+
return Response.json(
|
|
670
|
+
{ ok: false, error: result.error },
|
|
671
|
+
{ status: result.status ?? 400, headers: { 'Cache-Control': 'no-store' } }
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
return Response.json(
|
|
675
|
+
{ path: result.path },
|
|
676
|
+
{ status: 201, headers: { 'Cache-Control': 'no-store' } }
|
|
677
|
+
);
|
|
678
|
+
},
|
|
679
|
+
|
|
585
680
|
'/_api/export-history': async (req: Request) => {
|
|
586
681
|
// Phase 6.5 T10 — read-only recent-exports feed for the dialog's
|
|
587
682
|
// Recent tab. Writes happen as a side-effect of `/_api/export`.
|
|
@@ -791,6 +886,11 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
791
886
|
headers: {
|
|
792
887
|
'Content-Type': MIME[e] || 'application/octet-stream',
|
|
793
888
|
'Cache-Control': 'no-store',
|
|
889
|
+
// DDR-088 follow-up — never let a browser MIME-sniff a served file
|
|
890
|
+
// (e.g. an uploaded GIF/WEBP polyglot) into a richer type. Assets are
|
|
891
|
+
// only referenced via <image href> + the canvas CSP blocks script, so
|
|
892
|
+
// this is defense-in-depth on the static lane.
|
|
893
|
+
'X-Content-Type-Options': 'nosniff',
|
|
794
894
|
},
|
|
795
895
|
});
|
|
796
896
|
} catch (err) {
|
|
@@ -837,6 +937,7 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
837
937
|
'/_api/git-user', // presence display name
|
|
838
938
|
'/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
|
|
839
939
|
'/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
|
|
940
|
+
'/_api/asset', // Phase 23 — capped binary image upload (sniff+10MB+sha8 name+no-SVG)
|
|
840
941
|
'/_api/git-committers', // @mention autocomplete
|
|
841
942
|
'/_api/ai', // AI-activity banner
|
|
842
943
|
'/_comments', // per-file comment list (renders pins)
|