@agent-native/core 0.90.1 → 0.90.2
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
- package/corpus/core/src/client/use-action.ts +39 -2
- package/corpus/core/src/client/use-db-sync.ts +44 -18
- package/corpus/core/src/db/create-get-db.ts +32 -2
- package/corpus/core/src/server/framework-request-handler.ts +52 -4
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
- package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
- package/corpus/templates/assets/AGENTS.md +23 -4
- package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
- package/corpus/templates/assets/actions/_helpers.ts +8 -1
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
- package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
- package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
- package/corpus/templates/assets/actions/generate-image.ts +342 -25
- package/corpus/templates/assets/actions/get-library.ts +4 -3
- package/corpus/templates/assets/actions/navigate.ts +2 -1
- package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
- package/corpus/templates/assets/actions/update-asset.ts +1 -0
- package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
- package/corpus/templates/assets/actions/view-screen.ts +8 -1
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
- package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
- package/corpus/templates/assets/app/i18n-data.ts +650 -0
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
- package/corpus/templates/assets/app/routes/library.tsx +43 -1
- package/corpus/templates/assets/server/lib/generation.ts +366 -44
- package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
- package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
- package/corpus/templates/assets/shared/api.ts +47 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
- package/corpus/templates/clips/app/routes/record.tsx +157 -68
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -5
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +20 -3
- package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
- package/corpus/templates/content/AGENTS.md +52 -52
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
- package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
- package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
- package/corpus/templates/content/actions/_database-utils.ts +44 -4
- package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
- package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
- package/corpus/templates/content/actions/delete-content-database.ts +1 -0
- package/corpus/templates/content/actions/get-document.ts +11 -2
- package/corpus/templates/content/actions/list-content-databases.ts +57 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
- package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
- package/corpus/templates/content/actions/restore-content-database.ts +1 -0
- package/corpus/templates/content/actions/update-document.ts +43 -0
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
- package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
- package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
- package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
- package/corpus/templates/content/app/global.css +41 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
- package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
- package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
- package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
- package/corpus/templates/content/app/i18n-data.ts +355 -0
- package/corpus/templates/content/app/root.tsx +22 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
- package/corpus/templates/content/shared/api.ts +13 -0
- package/corpus/templates/content/shared/builder-mdx.ts +16 -3
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
- package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
- package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
- package/corpus/templates/design/AGENTS.md +9 -2
- package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
- package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
- package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
- package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
- package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
- package/corpus/templates/design/actions/create-component.ts +29 -60
- package/corpus/templates/design/actions/create-file.ts +9 -2
- package/corpus/templates/design/actions/duplicate-design.ts +31 -2
- package/corpus/templates/design/actions/edit-design.ts +36 -38
- package/corpus/templates/design/actions/generate-design-title.ts +87 -0
- package/corpus/templates/design/actions/generate-design.ts +53 -18
- package/corpus/templates/design/actions/generate-screens.ts +108 -4
- package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
- package/corpus/templates/design/actions/insert-asset.ts +34 -29
- package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
- package/corpus/templates/design/actions/present-design-variants.ts +7 -2
- package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
- package/corpus/templates/design/actions/update-file.ts +190 -75
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
- package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
- package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
- package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
- package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
- package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
- package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
- package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
- package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
- package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
- package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
- package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
- package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
- package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
- package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
- package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
- package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
- package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
- package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
- package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
- package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
- package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
- package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
- package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
- package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
- package/corpus/templates/design/app/components/design/types.ts +15 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
- package/corpus/templates/design/app/global.css +8 -8
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
- package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
- package/corpus/templates/design/app/i18n-data.ts +1664 -152
- package/corpus/templates/design/app/i18n-motion.ts +563 -0
- package/corpus/templates/design/app/lib/design-import.ts +73 -0
- package/corpus/templates/design/app/pages/Index.tsx +41 -22
- package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
- package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
- package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
- package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
- package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
- package/corpus/templates/design/server/source-workspace.ts +139 -59
- package/corpus/templates/design/shared/board-file.ts +11 -2
- package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
- package/corpus/templates/design/shared/canvas-math.ts +354 -35
- package/corpus/templates/design/shared/code-layer.ts +277 -8
- package/corpus/templates/design/shared/design-state.ts +11 -4
- package/corpus/templates/design/shared/interaction-states.ts +548 -0
- package/corpus/templates/design/shared/motion-compiler.ts +186 -23
- package/corpus/templates/design/shared/motion-easing.ts +294 -0
- package/corpus/templates/design/shared/motion-timeline.ts +400 -34
- package/corpus/templates/design/shared/prompt-title.ts +95 -0
- package/corpus/templates/design/shared/responsive-classes.ts +448 -10
- package/corpus/templates/design/shared/screen-annotation.ts +46 -0
- package/corpus/templates/design/shared/shader-fills.ts +1009 -0
- package/corpus/templates/design/shared/shader-presets.ts +852 -4
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
- package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
- package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/client/use-action.d.ts +3 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +34 -3
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +4 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +37 -18
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/create-get-db.d.ts +25 -0
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +25 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +37 -1
- package/dist/server/framework-request-handler.js.map +1 -1
- package/package.json +1 -1
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
- package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
|
@@ -14,12 +14,19 @@
|
|
|
14
14
|
* - **No dependencies**: uses djb2 (not crypto) for hashing.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { motionEaseToCss } from "./motion-easing";
|
|
17
18
|
import type {
|
|
18
19
|
MotionEase,
|
|
19
20
|
MotionKeyframe,
|
|
21
|
+
MotionPlaybackMode,
|
|
20
22
|
MotionTimeline,
|
|
21
23
|
MotionTrack,
|
|
22
24
|
} from "./motion-timeline";
|
|
25
|
+
import {
|
|
26
|
+
MOTION_DEFAULT_PLAYBACK_MODE,
|
|
27
|
+
getMotionTrackTiming,
|
|
28
|
+
readTimelinePlaybackMode,
|
|
29
|
+
} from "./motion-timeline";
|
|
23
30
|
|
|
24
31
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
25
32
|
|
|
@@ -54,6 +61,13 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
54
61
|
return { css, hash: djb2(css) };
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// Timeline-level playback mode: explicit field first, then the stamp
|
|
65
|
+
// persisted in the tracks JSON, then the legacy default ("once").
|
|
66
|
+
const playbackMode: MotionPlaybackMode =
|
|
67
|
+
timeline.playbackMode ??
|
|
68
|
+
readTimelinePlaybackMode(tracks) ??
|
|
69
|
+
MOTION_DEFAULT_PLAYBACK_MODE;
|
|
70
|
+
|
|
57
71
|
const kfBlocks: string[] = [];
|
|
58
72
|
const rulesByTarget = new Map<
|
|
59
73
|
string,
|
|
@@ -62,6 +76,8 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
62
76
|
durations: string[];
|
|
63
77
|
timings: string[];
|
|
64
78
|
fillModes: string[];
|
|
79
|
+
delays: string[];
|
|
80
|
+
hasDelay: boolean;
|
|
65
81
|
}
|
|
66
82
|
>();
|
|
67
83
|
|
|
@@ -83,7 +99,8 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
83
99
|
const name = animationName(targetNodeId, property);
|
|
84
100
|
kfBlocks.push(keyframesBlock(name, property, sortedKeyframes, defaultEase));
|
|
85
101
|
|
|
86
|
-
const
|
|
102
|
+
const timing = getMotionTrackTiming(track, durationMs);
|
|
103
|
+
const dur = formatDuration(timing.durationMs);
|
|
87
104
|
const ease = sortedKeyframes[0]?.ease ?? defaultEase;
|
|
88
105
|
assertSafeMotionCssToken(ease, "track ease");
|
|
89
106
|
const targetRule = rulesByTarget.get(targetNodeId) ?? {
|
|
@@ -91,26 +108,53 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
91
108
|
durations: [],
|
|
92
109
|
timings: [],
|
|
93
110
|
fillModes: [],
|
|
111
|
+
delays: [],
|
|
112
|
+
hasDelay: false,
|
|
94
113
|
};
|
|
95
114
|
targetRule.names.push(name);
|
|
96
115
|
targetRule.durations.push(dur);
|
|
97
|
-
targetRule.timings.push(ease);
|
|
116
|
+
targetRule.timings.push(cssEase(ease));
|
|
98
117
|
targetRule.fillModes.push("both");
|
|
118
|
+
targetRule.delays.push(formatDuration(timing.startMs));
|
|
119
|
+
if (timing.startMs > 0) targetRule.hasDelay = true;
|
|
99
120
|
rulesByTarget.set(targetNodeId, targetRule);
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
const sortedTargets = [...rulesByTarget.entries()].sort(([a], [b]) =>
|
|
103
124
|
a.localeCompare(b),
|
|
104
125
|
);
|
|
105
|
-
const ruleBlocks = sortedTargets.map(
|
|
106
|
-
|
|
126
|
+
const ruleBlocks = sortedTargets.map(([targetNodeId, rule]) => {
|
|
127
|
+
const lines = [
|
|
128
|
+
` animation-name: ${rule.names.join(", ")};`,
|
|
129
|
+
` animation-duration: ${rule.durations.join(", ")};`,
|
|
130
|
+
` animation-timing-function: ${rule.timings.join(", ")};`,
|
|
131
|
+
` animation-fill-mode: ${rule.fillModes.join(", ")};`,
|
|
132
|
+
];
|
|
133
|
+
// Emit optional lines only when used, keeping legacy timelines
|
|
134
|
+
// byte-identical to the previous compiler output.
|
|
135
|
+
if (rule.hasDelay) {
|
|
136
|
+
lines.push(` animation-delay: ${rule.delays.join(", ")};`);
|
|
137
|
+
}
|
|
138
|
+
if (playbackMode !== "once") {
|
|
139
|
+
lines.push(
|
|
140
|
+
` animation-iteration-count: ${rule.names
|
|
141
|
+
.map(() => "infinite")
|
|
142
|
+
.join(", ")};`,
|
|
143
|
+
);
|
|
144
|
+
if (playbackMode === "ping-pong") {
|
|
145
|
+
lines.push(
|
|
146
|
+
` animation-direction: ${rule.names
|
|
147
|
+
.map(() => "alternate")
|
|
148
|
+
.join(", ")};`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
107
153
|
`[data-agent-native-node-id="${escAttr(targetNodeId)}"] {\n` +
|
|
108
|
-
|
|
109
|
-
`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`}`,
|
|
113
|
-
);
|
|
154
|
+
`${lines.join("\n")}\n` +
|
|
155
|
+
`}`
|
|
156
|
+
);
|
|
157
|
+
});
|
|
114
158
|
|
|
115
159
|
const css = [
|
|
116
160
|
...kfBlocks,
|
|
@@ -130,7 +174,8 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
130
174
|
*/
|
|
131
175
|
export function parse(css: string): MotionTrack[] {
|
|
132
176
|
const tracks: MotionTrack[] = [];
|
|
133
|
-
const
|
|
177
|
+
const rules = parseAnimationRules(css);
|
|
178
|
+
const timelineDurationMs = timelineSpanFromRules(rules);
|
|
134
179
|
const kfRe = /@keyframes\s+(an-motion-[^\s{]+)\s*\{/g;
|
|
135
180
|
let m: RegExpExecArray | null;
|
|
136
181
|
|
|
@@ -143,16 +188,60 @@ export function parse(css: string): MotionTrack[] {
|
|
|
143
188
|
const body = extractBlock(css, bodyStart);
|
|
144
189
|
if (body === null) continue;
|
|
145
190
|
|
|
146
|
-
|
|
147
|
-
|
|
191
|
+
const info = rules.byName.get(fullName);
|
|
192
|
+
const track: MotionTrack = {
|
|
193
|
+
targetNodeId: info?.targetNodeId ?? decoded.targetNodeId,
|
|
148
194
|
property: decoded.property,
|
|
149
195
|
keyframes: parseKeyframeBody(body, decoded.property),
|
|
150
|
-
}
|
|
196
|
+
};
|
|
197
|
+
if (info?.delayMs !== undefined && info.delayMs > 0) {
|
|
198
|
+
track.delayMs = info.delayMs;
|
|
199
|
+
}
|
|
200
|
+
// Only surface an explicit per-track duration when it differs from the
|
|
201
|
+
// timeline duration (the first animation-duration in the CSS, which
|
|
202
|
+
// CSS-recovery also uses as the recovered timeline duration).
|
|
203
|
+
if (
|
|
204
|
+
info?.durationMs !== undefined &&
|
|
205
|
+
timelineDurationMs !== null &&
|
|
206
|
+
info.durationMs !== timelineDurationMs
|
|
207
|
+
) {
|
|
208
|
+
track.durationMs = info.durationMs;
|
|
209
|
+
}
|
|
210
|
+
tracks.push(track);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
tracks.length > 0 &&
|
|
215
|
+
rules.playbackMode &&
|
|
216
|
+
rules.playbackMode !== "once"
|
|
217
|
+
) {
|
|
218
|
+
tracks[0] = { ...tracks[0], timelinePlaybackMode: rules.playbackMode };
|
|
151
219
|
}
|
|
152
220
|
|
|
153
221
|
return tracks;
|
|
154
222
|
}
|
|
155
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Recover the timeline playback mode from a managed motion CSS body:
|
|
226
|
+
* `animation-iteration-count: infinite` + `animation-direction: alternate`
|
|
227
|
+
* → "ping-pong"; infinite alone → "loop"; finite/absent → "once". Returns
|
|
228
|
+
* `null` when the CSS contains no element animation rules at all.
|
|
229
|
+
*/
|
|
230
|
+
export function parsePlaybackMode(css: string): MotionPlaybackMode | null {
|
|
231
|
+
return parseAnimationRules(css).playbackMode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Recover the timeline's total span from a managed motion CSS body: the
|
|
236
|
+
* maximum `animation-delay + animation-duration` across all compiled rules
|
|
237
|
+
* (i.e. when the last track finishes). More robust than the first
|
|
238
|
+
* `animation-duration` when tracks carry per-track offsets/durations.
|
|
239
|
+
* Returns `null` when the CSS has no parsable durations.
|
|
240
|
+
*/
|
|
241
|
+
export function parseTimelineSpanMs(css: string): number | null {
|
|
242
|
+
return timelineSpanFromRules(parseAnimationRules(css));
|
|
243
|
+
}
|
|
244
|
+
|
|
156
245
|
/**
|
|
157
246
|
* Extract the CSS body from a managed `<style data-agent-native-motion>` block
|
|
158
247
|
* inside an HTML document. Returns `null` when the document has no managed block
|
|
@@ -317,13 +406,28 @@ function keyframesBlock(
|
|
|
317
406
|
return (
|
|
318
407
|
` ${pct} {\n` +
|
|
319
408
|
` ${property}: ${kf.value};\n` +
|
|
320
|
-
` animation-timing-function: ${ease};\n` +
|
|
409
|
+
` animation-timing-function: ${cssEase(ease)};\n` +
|
|
321
410
|
` }`
|
|
322
411
|
);
|
|
323
412
|
});
|
|
324
413
|
return `@keyframes ${name} {\n${stops.join("\n")}\n}`;
|
|
325
414
|
}
|
|
326
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Convert a model ease token to its CSS form for emission. `spring(...)`
|
|
418
|
+
* tokens (not valid CSS) compile to sampled `linear(...)` stop lists; all
|
|
419
|
+
* other tokens pass through unchanged. The converted output is re-validated
|
|
420
|
+
* so nothing unsafe can enter the managed stylesheet.
|
|
421
|
+
*/
|
|
422
|
+
function cssEase(ease: MotionEase): string {
|
|
423
|
+
const raw = String(ease);
|
|
424
|
+
const converted = motionEaseToCss(raw);
|
|
425
|
+
if (converted !== raw) {
|
|
426
|
+
assertSafeMotionCssToken(converted, "compiled spring ease");
|
|
427
|
+
}
|
|
428
|
+
return converted;
|
|
429
|
+
}
|
|
430
|
+
|
|
327
431
|
/**
|
|
328
432
|
* Build the `@media (prefers-reduced-motion: reduce)` block.
|
|
329
433
|
* Always emitted so managed blocks are easily identified by parsers.
|
|
@@ -381,24 +485,83 @@ function unescAttr(value: string): string {
|
|
|
381
485
|
return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
382
486
|
}
|
|
383
487
|
|
|
384
|
-
|
|
385
|
-
|
|
488
|
+
interface ParsedAnimationRuleEntry {
|
|
489
|
+
targetNodeId: string;
|
|
490
|
+
/** Recovered `animation-delay` for this animation name, ms. */
|
|
491
|
+
delayMs?: number;
|
|
492
|
+
/** Recovered `animation-duration` for this animation name, ms. */
|
|
493
|
+
durationMs?: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
interface ParsedAnimationRules {
|
|
497
|
+
byName: Map<string, ParsedAnimationRuleEntry>;
|
|
498
|
+
/** Recovered playback mode, or null when no element rules exist. */
|
|
499
|
+
playbackMode: MotionPlaybackMode | null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Parse a CSS `<time>` (e.g. "0.4s", "250ms") into milliseconds, or null. */
|
|
503
|
+
function parseCssTimeMs(value: string): number | null {
|
|
504
|
+
const m = /^([\d.]+)(ms|s)$/.exec(value.trim());
|
|
505
|
+
if (!m) return null;
|
|
506
|
+
const n = parseFloat(m[1]);
|
|
507
|
+
if (!Number.isFinite(n)) return null;
|
|
508
|
+
return Math.round(m[2] === "ms" ? n : n * 1000);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Max (delay + duration) across all parsed rules, or null when none. */
|
|
512
|
+
function timelineSpanFromRules(rules: ParsedAnimationRules): number | null {
|
|
513
|
+
let span: number | null = null;
|
|
514
|
+
for (const entry of rules.byName.values()) {
|
|
515
|
+
if (entry.durationMs === undefined) continue;
|
|
516
|
+
const end = (entry.delayMs ?? 0) + entry.durationMs;
|
|
517
|
+
if (span === null || end > span) span = end;
|
|
518
|
+
}
|
|
519
|
+
return span !== null && span > 0 ? span : null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function parseAnimationRules(css: string): ParsedAnimationRules {
|
|
523
|
+
const byName = new Map<string, ParsedAnimationRuleEntry>();
|
|
524
|
+
let playbackMode: MotionPlaybackMode | null = null;
|
|
386
525
|
const ruleRe =
|
|
387
526
|
/\[data-agent-native-node-id="((?:\\.|[^"\\])*)"\]\s*\{([^}]*)\}/g;
|
|
388
527
|
let m: RegExpExecArray | null;
|
|
389
528
|
|
|
529
|
+
const listOf = (body: string, prop: string): string[] => {
|
|
530
|
+
const match = body.match(
|
|
531
|
+
new RegExp(`${prop.replace(/-/g, "\\-")}\\s*:\\s*([^;]+)`),
|
|
532
|
+
);
|
|
533
|
+
if (!match) return [];
|
|
534
|
+
return match[1].split(",").map((part) => part.trim());
|
|
535
|
+
};
|
|
536
|
+
|
|
390
537
|
while ((m = ruleRe.exec(css)) !== null) {
|
|
391
538
|
const targetNodeId = unescAttr(m[1]);
|
|
392
539
|
const body = m[2];
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
540
|
+
const names = listOf(body, "animation-name");
|
|
541
|
+
if (names.length === 0) continue;
|
|
542
|
+
const durations = listOf(body, "animation-duration");
|
|
543
|
+
const delays = listOf(body, "animation-delay");
|
|
544
|
+
const iterations = listOf(body, "animation-iteration-count");
|
|
545
|
+
const directions = listOf(body, "animation-direction");
|
|
546
|
+
|
|
547
|
+
if (playbackMode === null) {
|
|
548
|
+
const infinite = iterations[0] === "infinite";
|
|
549
|
+
const alternate = directions[0] === "alternate";
|
|
550
|
+
playbackMode = infinite ? (alternate ? "ping-pong" : "loop") : "once";
|
|
398
551
|
}
|
|
552
|
+
|
|
553
|
+
names.forEach((name, index) => {
|
|
554
|
+
if (!name) return;
|
|
555
|
+
const entry: ParsedAnimationRuleEntry = { targetNodeId };
|
|
556
|
+
const duration = parseCssTimeMs(durations[index] ?? durations[0] ?? "");
|
|
557
|
+
if (duration !== null) entry.durationMs = duration;
|
|
558
|
+
const delay = parseCssTimeMs(delays[index] ?? delays[0] ?? "");
|
|
559
|
+
if (delay !== null) entry.delayMs = delay;
|
|
560
|
+
byName.set(name, entry);
|
|
561
|
+
});
|
|
399
562
|
}
|
|
400
563
|
|
|
401
|
-
return
|
|
564
|
+
return { byName, playbackMode };
|
|
402
565
|
}
|
|
403
566
|
|
|
404
567
|
/**
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion easing engine — Figma Motion parity (curves + springs).
|
|
3
|
+
*
|
|
4
|
+
* Pure, dependency-free helpers shared by the MotionDock easing panel, the
|
|
5
|
+
* shared timeline evaluator, and the motion compiler:
|
|
6
|
+
*
|
|
7
|
+
* - **Curve presets** matching Figma Motion's easing panel verbatim
|
|
8
|
+
* (Hold, Linear, Ease in, Ease out, Ease in and out, the three "back"
|
|
9
|
+
* overshoot curves, and Custom bezier).
|
|
10
|
+
* - **Spring easing** stored in the timeline model as a compact
|
|
11
|
+
* `spring(bounce[, settle])` token (bounce ∈ [0, 1], like Figma's single
|
|
12
|
+
* normalized "Bounce" control) and compiled to pure CSS via the
|
|
13
|
+
* `linear(...)` timing function — sampled from a real damped-oscillator
|
|
14
|
+
* solution, so the persisted artifact stays plain CSS.
|
|
15
|
+
* - **`linear(...)` evaluation** so scrub previews and CSS-recovered
|
|
16
|
+
* timelines replay spring easings exactly as the compiled stylesheet does.
|
|
17
|
+
*
|
|
18
|
+
* The canvas preview bridge (app/components/design/bridge/
|
|
19
|
+
* motion-preview.bridge.ts) carries a dependency-free copy of the sampling
|
|
20
|
+
* and evaluation algorithms below — keep the two in sync.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ─── Curve presets (Figma Motion easing panel, Curve tab) ────────────────────
|
|
24
|
+
|
|
25
|
+
export interface MotionCurvePreset {
|
|
26
|
+
/** Menu label, verbatim from the Figma Motion easing panel. */
|
|
27
|
+
label: string;
|
|
28
|
+
/** CSS timing-function value stored on the keyframe. */
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bezier-family presets exactly as labeled in Figma Motion's easing panel.
|
|
34
|
+
*
|
|
35
|
+
* - "Hold" jumps immediately to the segment's end value and stays
|
|
36
|
+
* (a discontinuous step) → CSS `step-start`.
|
|
37
|
+
* - "Ease in and out" is the live-verified (.42, 0, .58, 1).
|
|
38
|
+
* - The three "back" curves are overshoot beziers (control-point y outside
|
|
39
|
+
* [0, 1]); Figma's exact numbers are not published, so these use the
|
|
40
|
+
* canonical easeInBack / easeOutBack / easeInOutBack values.
|
|
41
|
+
* - "Custom bezier" is not listed here: the easing panel switches to editable
|
|
42
|
+
* x1,y1,x2,y2 fields + a draggable curve for any unrecognised
|
|
43
|
+
* `cubic-bezier(...)` value.
|
|
44
|
+
*/
|
|
45
|
+
export const MOTION_CURVE_PRESETS: MotionCurvePreset[] = [
|
|
46
|
+
{ label: "Hold", value: "step-start" },
|
|
47
|
+
{ label: "Linear", value: "linear" },
|
|
48
|
+
{ label: "Ease in", value: "cubic-bezier(0.42, 0, 1, 1)" },
|
|
49
|
+
{ label: "Ease out", value: "cubic-bezier(0, 0, 0.58, 1)" },
|
|
50
|
+
{ label: "Ease in and out", value: "cubic-bezier(0.42, 0, 0.58, 1)" },
|
|
51
|
+
{ label: "Ease in back", value: "cubic-bezier(0.36, 0, 0.66, -0.56)" },
|
|
52
|
+
{ label: "Ease out back", value: "cubic-bezier(0.34, 1.56, 0.64, 1)" },
|
|
53
|
+
{
|
|
54
|
+
label: "Ease in and out back",
|
|
55
|
+
value: "cubic-bezier(0.68, -0.6, 0.32, 1.6)",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// ─── Spring model ─────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Editable spring parameters, matching Figma Motion's normalized spring UI:
|
|
63
|
+
* a single "Bounce" value in [0, 1]. `settle` is the fraction of the segment
|
|
64
|
+
* by which the spring reaches rest (1 = uses the whole segment) — it lets the
|
|
65
|
+
* named presets differ in perceived speed while remaining segment-normalized.
|
|
66
|
+
*/
|
|
67
|
+
export interface MotionSpring {
|
|
68
|
+
/** Normalized bounciness in [0, 1]. 0 = no overshoot. */
|
|
69
|
+
bounce: number;
|
|
70
|
+
/** Fraction of the segment at which the spring settles, in (0, 1]. */
|
|
71
|
+
settle: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Default bounce for "Custom spring" (matches Figma's default of 0.25). */
|
|
75
|
+
export const MOTION_SPRING_DEFAULT_BOUNCE = 0.25;
|
|
76
|
+
|
|
77
|
+
export interface MotionSpringPreset {
|
|
78
|
+
/** Menu label, verbatim from the Figma Motion easing panel (Spring tab). */
|
|
79
|
+
label: string;
|
|
80
|
+
spring: MotionSpring;
|
|
81
|
+
/** The `spring(...)` token stored on the keyframe for this preset. */
|
|
82
|
+
value: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function roundParam(n: number): number {
|
|
86
|
+
return Math.round(n * 1000) / 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Serialize spring params into the timeline's `spring(...)` ease token. */
|
|
90
|
+
export function springToken(spring: MotionSpring): string {
|
|
91
|
+
const bounce = roundParam(clamp01(spring.bounce));
|
|
92
|
+
const settle = roundParam(Math.min(1, Math.max(0.05, spring.settle)));
|
|
93
|
+
return settle === 1 ? `spring(${bounce})` : `spring(${bounce}, ${settle})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Spring presets as named in Figma Motion's easing panel. "Bouncy" uses the
|
|
98
|
+
* live-verified bounce of 0.69; Gentle/Quick/Slow expose no numbers in the
|
|
99
|
+
* Figma UI, so their params are tuned approximations of the described feel
|
|
100
|
+
* (Gentle: soft, no overshoot; Quick: fast settle; Slow: full-length glide).
|
|
101
|
+
*/
|
|
102
|
+
export const MOTION_SPRING_PRESETS: MotionSpringPreset[] = [
|
|
103
|
+
{ label: "Gentle", spring: { bounce: 0, settle: 0.8 } },
|
|
104
|
+
{ label: "Quick", spring: { bounce: 0.2, settle: 0.5 } },
|
|
105
|
+
{ label: "Bouncy", spring: { bounce: 0.69, settle: 1 } },
|
|
106
|
+
{ label: "Slow", spring: { bounce: 0, settle: 1 } },
|
|
107
|
+
].map((preset) => ({ ...preset, value: springToken(preset.spring) }));
|
|
108
|
+
|
|
109
|
+
function clamp01(x: number): number {
|
|
110
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse a `spring(bounce[, settle])` ease token (or the bare keyword
|
|
115
|
+
* `spring`, which maps to the default custom spring). Returns `null` when the
|
|
116
|
+
* string is not a spring token.
|
|
117
|
+
*/
|
|
118
|
+
export function parseSpringToken(ease: string): MotionSpring | null {
|
|
119
|
+
const raw = String(ease ?? "").trim();
|
|
120
|
+
if (/^spring$/i.test(raw)) {
|
|
121
|
+
return { bounce: MOTION_SPRING_DEFAULT_BOUNCE, settle: 1 };
|
|
122
|
+
}
|
|
123
|
+
const m = /^spring\(\s*([+-]?[\d.]+)\s*(?:,\s*([+-]?[\d.]+)\s*)?\)$/i.exec(
|
|
124
|
+
raw,
|
|
125
|
+
);
|
|
126
|
+
if (!m) return null;
|
|
127
|
+
const bounce = parseFloat(m[1]);
|
|
128
|
+
if (!Number.isFinite(bounce)) return null;
|
|
129
|
+
let settle = m[2] === undefined ? 1 : parseFloat(m[2]);
|
|
130
|
+
if (!Number.isFinite(settle)) return null;
|
|
131
|
+
settle = Math.min(1, Math.max(0.05, settle));
|
|
132
|
+
return { bounce: clamp01(bounce), settle };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sample a normalized spring at progress `x ∈ [0, 1]`.
|
|
137
|
+
*
|
|
138
|
+
* Damped-oscillator position response scaled so the spring starts at 0,
|
|
139
|
+
* ends at 1, and is at rest by `x = settle`:
|
|
140
|
+
* - damping ratio ζ = 1 − bounce (underdamped when bounce > 0)
|
|
141
|
+
* - natural frequency ω chosen so the decay envelope ≈ 0 at the settle point
|
|
142
|
+
*
|
|
143
|
+
* Output may overshoot above 1 for bounce > 0 (by design — that is the
|
|
144
|
+
* bounce). Values at or after `settle` are exactly 1.
|
|
145
|
+
*/
|
|
146
|
+
export function sampleSpring(spring: MotionSpring, x: number): number {
|
|
147
|
+
if (x <= 0) return 0;
|
|
148
|
+
const settle = Math.min(1, Math.max(0.05, spring.settle));
|
|
149
|
+
const u = x / settle;
|
|
150
|
+
if (u >= 1) return 1;
|
|
151
|
+
const zeta = Math.min(1, Math.max(0.02, 1 - clamp01(spring.bounce)));
|
|
152
|
+
// Envelope e^(-ζω) ≈ EPS at u = 1 so the motion is visually at rest there.
|
|
153
|
+
const EPS = 0.001;
|
|
154
|
+
const omega = Math.log(1 / EPS) / zeta;
|
|
155
|
+
if (zeta >= 1) {
|
|
156
|
+
// Critically damped: y = 1 − e^(−ωu)(1 + ωu).
|
|
157
|
+
return 1 - Math.exp(-omega * u) * (1 + omega * u);
|
|
158
|
+
}
|
|
159
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
160
|
+
const decay = Math.exp(-zeta * omega * u);
|
|
161
|
+
return (
|
|
162
|
+
1 -
|
|
163
|
+
decay *
|
|
164
|
+
(Math.cos(omegaD * u) + ((zeta * omega) / omegaD) * Math.sin(omegaD * u))
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatStop(n: number): string {
|
|
169
|
+
if (!Number.isFinite(n)) return "0";
|
|
170
|
+
return (Math.round(n * 10000) / 10000).toString();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compile a spring into a CSS `linear(...)` timing function by sampling the
|
|
175
|
+
* spring curve at evenly spaced stops. Deterministic: the same spring always
|
|
176
|
+
* produces byte-identical CSS. Bouncier springs get more stops so the
|
|
177
|
+
* oscillation survives the piecewise-linear approximation.
|
|
178
|
+
*/
|
|
179
|
+
export function springToCssLinear(spring: MotionSpring): string {
|
|
180
|
+
const bounce = clamp01(spring.bounce);
|
|
181
|
+
const samples = 16 + Math.round(bounce * 34); // 16 … 50 interior steps
|
|
182
|
+
const stops: string[] = [];
|
|
183
|
+
for (let i = 0; i <= samples; i++) {
|
|
184
|
+
const x = i / samples;
|
|
185
|
+
stops.push(formatStop(i === samples ? 1 : sampleSpring(spring, x)));
|
|
186
|
+
}
|
|
187
|
+
return `linear(${stops.join(", ")})`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convert a timeline ease token into a CSS-valid timing function.
|
|
192
|
+
* `spring(...)` tokens compile to `linear(...)`; everything else (keywords,
|
|
193
|
+
* `cubic-bezier(...)`, `steps(...)`, `linear(...)`) passes through untouched.
|
|
194
|
+
*/
|
|
195
|
+
export function motionEaseToCss(ease: string): string {
|
|
196
|
+
const spring = parseSpringToken(ease);
|
|
197
|
+
return spring ? springToCssLinear(spring) : ease;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── CSS linear() evaluation ─────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
interface LinearStop {
|
|
203
|
+
value: number;
|
|
204
|
+
/** Input position in [0, 1], or null when the stop omitted a percentage. */
|
|
205
|
+
position: number | null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse the argument list of a CSS `linear(...)` timing function. Each entry
|
|
210
|
+
* is `<number> <percentage>{0,2}`; a stop with two percentages expands into
|
|
211
|
+
* two stops. Returns `null` when `raw` is not a linear() function or has
|
|
212
|
+
* fewer than two stops.
|
|
213
|
+
*/
|
|
214
|
+
export function parseCssLinearStops(raw: string): LinearStop[] | null {
|
|
215
|
+
const m = /^linear\(([^)]*)\)$/i.exec(String(raw ?? "").trim());
|
|
216
|
+
if (!m) return null;
|
|
217
|
+
const entries = m[1]
|
|
218
|
+
.split(",")
|
|
219
|
+
.map((entry) => entry.trim())
|
|
220
|
+
.filter((entry) => entry.length > 0);
|
|
221
|
+
if (entries.length < 2) return null;
|
|
222
|
+
const stops: LinearStop[] = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const parts = entry.split(/\s+/);
|
|
225
|
+
const value = parseFloat(parts[0]);
|
|
226
|
+
if (!Number.isFinite(value)) return null;
|
|
227
|
+
const positions: number[] = [];
|
|
228
|
+
for (let i = 1; i < parts.length && i <= 2; i++) {
|
|
229
|
+
if (!/%$/.test(parts[i])) return null;
|
|
230
|
+
const pct = parseFloat(parts[i]);
|
|
231
|
+
if (!Number.isFinite(pct)) return null;
|
|
232
|
+
positions.push(pct / 100);
|
|
233
|
+
}
|
|
234
|
+
if (positions.length === 0) {
|
|
235
|
+
stops.push({ value, position: null });
|
|
236
|
+
} else {
|
|
237
|
+
for (const position of positions) stops.push({ value, position });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Resolve omitted positions per the CSS spec: first defaults to 0, last to
|
|
241
|
+
// 1, interior stops distribute evenly between the surrounding known
|
|
242
|
+
// positions. Positions are also forced monotonic.
|
|
243
|
+
if (stops[0].position === null) stops[0].position = 0;
|
|
244
|
+
const last = stops[stops.length - 1];
|
|
245
|
+
if (last.position === null) last.position = 1;
|
|
246
|
+
let runningMax = stops[0].position as number;
|
|
247
|
+
for (let i = 0; i < stops.length; i++) {
|
|
248
|
+
const pos = stops[i].position;
|
|
249
|
+
if (pos !== null) {
|
|
250
|
+
const clamped = Math.max(runningMax, pos);
|
|
251
|
+
stops[i].position = clamped;
|
|
252
|
+
runningMax = clamped;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Find the next stop with a known position.
|
|
256
|
+
let nextIdx = i + 1;
|
|
257
|
+
while (nextIdx < stops.length && stops[nextIdx].position === null) {
|
|
258
|
+
nextIdx++;
|
|
259
|
+
}
|
|
260
|
+
const prevPos = runningMax;
|
|
261
|
+
const nextPos = Math.max(prevPos, stops[nextIdx].position as number);
|
|
262
|
+
const span = nextIdx - (i - 1);
|
|
263
|
+
for (let j = i; j < nextIdx; j++) {
|
|
264
|
+
stops[j].position =
|
|
265
|
+
prevPos + ((j - (i - 1)) / span) * (nextPos - prevPos);
|
|
266
|
+
}
|
|
267
|
+
i = nextIdx - 1;
|
|
268
|
+
runningMax = nextPos;
|
|
269
|
+
}
|
|
270
|
+
return stops;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Evaluate a CSS `linear(...)` timing function at progress `x ∈ [0, 1]`.
|
|
275
|
+
* Returns `null` when `raw` is not a valid linear() function (so callers can
|
|
276
|
+
* fall through to other easing forms).
|
|
277
|
+
*/
|
|
278
|
+
export function evaluateCssLinear(raw: string, x: number): number | null {
|
|
279
|
+
const stops = parseCssLinearStops(raw);
|
|
280
|
+
if (!stops) return null;
|
|
281
|
+
const clamped = clamp01(x);
|
|
282
|
+
if (clamped <= (stops[0].position as number)) return stops[0].value;
|
|
283
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
284
|
+
const a = stops[i];
|
|
285
|
+
const b = stops[i + 1];
|
|
286
|
+
const aPos = a.position as number;
|
|
287
|
+
const bPos = b.position as number;
|
|
288
|
+
if (clamped > bPos) continue;
|
|
289
|
+
if (bPos === aPos) return b.value;
|
|
290
|
+
const ratio = (clamped - aPos) / (bPos - aPos);
|
|
291
|
+
return a.value + (b.value - a.value) * ratio;
|
|
292
|
+
}
|
|
293
|
+
return stops[stops.length - 1].value;
|
|
294
|
+
}
|