@agent-native/core 0.80.8 → 0.80.10
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 +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/corpus/core/docs/content/template-design.mdx +9 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/mcp.ts +10 -0
- package/corpus/core/src/cli/skills.ts +103 -36
- package/corpus/core/src/client/error-format.ts +25 -0
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
- package/corpus/core/src/client/guided-questions.tsx +32 -4
- package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
- package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
- package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
- package/corpus/core/src/client/session-replay.ts +34 -0
- package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
- package/corpus/core/src/coding-tools/index.ts +1 -1
- package/corpus/core/src/mcp/index.ts +2 -0
- package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
- package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
- package/corpus/core/src/onboarding/types.ts +7 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
- package/corpus/core/src/server/agents-bundle.ts +35 -7
- package/corpus/core/src/styles/agent-native.css +5 -0
- package/corpus/core/src/templates/default/app/global.css +35 -35
- package/corpus/templates/analytics/AGENTS.md +6 -0
- package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
- package/corpus/templates/analytics/app/global.css +144 -40
- package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
- package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
- package/corpus/templates/assets/app/global.css +99 -35
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.tsx +4 -4
- package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/brain/app/global.css +63 -35
- package/corpus/templates/brain/app/routes/search.tsx +1 -1
- package/corpus/templates/brain/app/routes/settings.tsx +1 -1
- package/corpus/templates/brain/app/routes/sources.tsx +2 -2
- package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
- package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
- package/corpus/templates/calendar/app/global.css +69 -35
- package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/global.css +35 -35
- package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/AGENTS.md +13 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
- package/corpus/templates/clips/actions/view-screen.ts +2 -2
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/global.css +47 -35
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
- package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
- package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
- package/corpus/templates/clips/desktop/src/app.tsx +290 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
- package/corpus/templates/clips/desktop/src/styles.css +67 -52
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
- package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
- package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/content/app/global.css +29 -15
- package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
- package/corpus/templates/design/AGENTS.md +20 -12
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
- package/corpus/templates/design/actions/generate-design.ts +6 -21
- package/corpus/templates/design/actions/generate-screens.ts +43 -2
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-design.ts +1 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/present-design-variants.ts +355 -30
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/actions/view-screen.ts +75 -31
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
- package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
- package/corpus/templates/design/app/components/design/types.ts +9 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/global.css +77 -49
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
- package/corpus/templates/design/app/i18n-data.ts +314 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
- package/corpus/templates/design/app/root.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/README.md +3 -3
- package/corpus/templates/design/e2e/global-setup.ts +11 -22
- package/corpus/templates/design/e2e/helpers.ts +128 -11
- package/corpus/templates/design/playwright.config.ts +9 -3
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
- package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1141 -93
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/generation-session.ts +41 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/dispatch/app/global.css +79 -35
- package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
- package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
- package/corpus/templates/forms/app/global.css +64 -35
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
- package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
- package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
- package/corpus/templates/macros/app/global.css +25 -9
- package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
- package/corpus/templates/mail/app/global.css +42 -36
- package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
- package/corpus/templates/plan/app/global.css +74 -42
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/slides/app/global.css +49 -33
- package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/videos/app/global.css +35 -35
- package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +10 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +95 -33
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +3 -4
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/guided-questions.d.ts +4 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +16 -7
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/onboarding/OnboardingPanel.js +26 -1
- package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.js +2 -1
- package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +31 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +14 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +36 -7
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/coding-tools/index.js +1 -1
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +6 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
- package/dist/mcp/screen-memory-stdio.js +213 -0
- package/dist/mcp/screen-memory-stdio.js.map +1 -0
- package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
- package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
- package/dist/mcp-client/builtin-capabilities.js +10 -0
- package/dist/mcp-client/builtin-capabilities.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -0
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -0
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +61 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
- package/dist/mcp-client/screen-memory-local.js +336 -0
- package/dist/mcp-client/screen-memory-local.js.map +1 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/types.d.ts +7 -0
- package/dist/onboarding/types.d.ts.map +1 -1
- package/dist/onboarding/types.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +26 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agents-bundle.d.ts.map +1 -1
- package/dist/server/agents-bundle.js +22 -7
- package/dist/server/agents-bundle.js.map +1 -1
- package/dist/styles/agent-native.css +5 -0
- package/dist/templates/default/app/global.css +35 -35
- package/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/docs/content/template-design.mdx +9 -2
- package/package.json +1 -1
- package/src/templates/default/app/global.css +35 -35
- package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
- package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
- package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
// i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
|
|
2
|
+
/**
|
|
3
|
+
* MotionDock — bottom motion timeline dock for the Design Studio (§6.3).
|
|
4
|
+
*
|
|
5
|
+
* Matches the motion artboard at
|
|
6
|
+
* https://plan.agent-native.com/plans/plan-88dc4a09fb0c46bc:
|
|
7
|
+
* - Full-width collapsible dock beneath the canvas.
|
|
8
|
+
* - Left sidebar: animated layer rows with property sub-rows.
|
|
9
|
+
* - Center: time ruler + diamond keyframes on a track grid.
|
|
10
|
+
* - Playhead: draggable; scrubbing sends a preview-only `motion-preview`
|
|
11
|
+
* postMessage to the canvas iframe — NEVER writes to DB.
|
|
12
|
+
* - Top toolbar: play/pause, duration input, auto-keyframe toggle, Write to CSS.
|
|
13
|
+
*
|
|
14
|
+
* Write to CSS calls the parent's `onApply` prop, which should invoke
|
|
15
|
+
* `apply-motion-edit` through `useActionMutation`.
|
|
16
|
+
*
|
|
17
|
+
* All times are normalised to [0, 1] internally; the ruler maps them to px.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
MotionTrack,
|
|
22
|
+
MotionKeyframe,
|
|
23
|
+
MotionEase,
|
|
24
|
+
MotionPropertyPreset,
|
|
25
|
+
} from "@shared/motion-timeline";
|
|
26
|
+
import {
|
|
27
|
+
MOTION_PROPERTY_PRESETS,
|
|
28
|
+
createMotionTrackFromPreset,
|
|
29
|
+
hasTrackFor,
|
|
30
|
+
} from "@shared/motion-timeline";
|
|
31
|
+
import {
|
|
32
|
+
IconPlayerPlay,
|
|
33
|
+
IconPlayerPause,
|
|
34
|
+
IconPlayerStop,
|
|
35
|
+
IconDiamond,
|
|
36
|
+
IconChevronDown,
|
|
37
|
+
IconChevronRight,
|
|
38
|
+
IconPlus,
|
|
39
|
+
IconTrash,
|
|
40
|
+
IconCode,
|
|
41
|
+
IconRefresh,
|
|
42
|
+
IconBolt,
|
|
43
|
+
IconLayersSubtract,
|
|
44
|
+
} from "@tabler/icons-react";
|
|
45
|
+
import {
|
|
46
|
+
useCallback,
|
|
47
|
+
useEffect,
|
|
48
|
+
useRef,
|
|
49
|
+
useState,
|
|
50
|
+
type PointerEvent as ReactPointerEvent,
|
|
51
|
+
} from "react";
|
|
52
|
+
|
|
53
|
+
import { Button } from "@/components/ui/button";
|
|
54
|
+
import {
|
|
55
|
+
DropdownMenu,
|
|
56
|
+
DropdownMenuContent,
|
|
57
|
+
DropdownMenuItem,
|
|
58
|
+
DropdownMenuLabel,
|
|
59
|
+
DropdownMenuSeparator,
|
|
60
|
+
DropdownMenuTrigger,
|
|
61
|
+
} from "@/components/ui/dropdown-menu";
|
|
62
|
+
import { Input } from "@/components/ui/input";
|
|
63
|
+
import {
|
|
64
|
+
Tooltip,
|
|
65
|
+
TooltipContent,
|
|
66
|
+
TooltipTrigger,
|
|
67
|
+
} from "@/components/ui/tooltip";
|
|
68
|
+
import { cn } from "@/lib/utils";
|
|
69
|
+
|
|
70
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const RULER_HEIGHT = 24; // px
|
|
73
|
+
const ROW_HEIGHT = 32; // px
|
|
74
|
+
const LAYER_SIDEBAR_WIDTH = 200; // px
|
|
75
|
+
const PLAYHEAD_WIDTH = 2; // px
|
|
76
|
+
const MIN_DOCK_HEIGHT = 160; // px
|
|
77
|
+
const DEFAULT_DOCK_HEIGHT = 280; // px
|
|
78
|
+
|
|
79
|
+
/** Named easing presets for the keyframe editor. */
|
|
80
|
+
const EASE_PRESETS: { label: string; value: MotionEase }[] = [
|
|
81
|
+
{ label: "Linear", value: "linear" },
|
|
82
|
+
{ label: "Ease", value: "ease" },
|
|
83
|
+
{ label: "Ease In", value: "ease-in" },
|
|
84
|
+
{ label: "Ease Out", value: "ease-out" },
|
|
85
|
+
{ label: "Ease In/Out", value: "ease-in-out" },
|
|
86
|
+
{ label: "Spring", value: "cubic-bezier(0.34,1.56,0.64,1)" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface MotionDockTrack extends MotionTrack {
|
|
92
|
+
/** Human-readable label derived from data-agent-native-layer-name or nodeId. */
|
|
93
|
+
label: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface MotionDockProps {
|
|
97
|
+
/** Tracks to display. Each track maps to one layer row. */
|
|
98
|
+
tracks: MotionDockTrack[];
|
|
99
|
+
/** Total animation duration in milliseconds. */
|
|
100
|
+
durationMs: number;
|
|
101
|
+
/** Default easing applied to keyframes that omit ease. */
|
|
102
|
+
defaultEase?: MotionEase;
|
|
103
|
+
/** Controlled open state. */
|
|
104
|
+
open?: boolean;
|
|
105
|
+
/** Called when the user toggles the dock open/closed. */
|
|
106
|
+
onOpenChange?: (open: boolean) => void;
|
|
107
|
+
/** Called when a track is modified (add/move/delete keyframe or change value). */
|
|
108
|
+
onTracksChange?: (tracks: MotionDockTrack[]) => void;
|
|
109
|
+
/** Called when durationMs is edited. */
|
|
110
|
+
onDurationChange?: (ms: number) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Called when "Write to CSS" is clicked. The parent should call
|
|
113
|
+
* `apply-motion-edit` via useActionMutation.
|
|
114
|
+
*/
|
|
115
|
+
onApply?: (tracks: MotionDockTrack[], durationMs: number) => void;
|
|
116
|
+
/**
|
|
117
|
+
* Reference to the canvas iframe element. Used to send preview postMessages.
|
|
118
|
+
* If not provided, preview messages are skipped (no crash).
|
|
119
|
+
*/
|
|
120
|
+
canvasIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
121
|
+
/** Whether the parent apply mutation is in flight. */
|
|
122
|
+
applying?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The currently-selected canvas element, if any. Required to create the FIRST
|
|
125
|
+
* track for a layer: the picker animates this node's
|
|
126
|
+
* `data-agent-native-node-id`. `null` when nothing is selected — the create
|
|
127
|
+
* affordance is then disabled with a hint to select an element.
|
|
128
|
+
*/
|
|
129
|
+
selectedTarget?: { nodeId: string; label: string } | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export function MotionDock({
|
|
135
|
+
tracks,
|
|
136
|
+
durationMs,
|
|
137
|
+
defaultEase = "ease",
|
|
138
|
+
open: openProp,
|
|
139
|
+
onOpenChange,
|
|
140
|
+
onTracksChange,
|
|
141
|
+
onDurationChange,
|
|
142
|
+
onApply,
|
|
143
|
+
canvasIframeRef,
|
|
144
|
+
applying = false,
|
|
145
|
+
selectedTarget = null,
|
|
146
|
+
}: MotionDockProps) {
|
|
147
|
+
// Controlled / uncontrolled open state.
|
|
148
|
+
const [openInternal, setOpenInternal] = useState(false);
|
|
149
|
+
const isOpen = openProp !== undefined ? openProp : openInternal;
|
|
150
|
+
const setOpen = useCallback(
|
|
151
|
+
(v: boolean) => {
|
|
152
|
+
setOpenInternal(v);
|
|
153
|
+
onOpenChange?.(v);
|
|
154
|
+
},
|
|
155
|
+
[onOpenChange],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Playhead position: normalised [0, 1].
|
|
159
|
+
const [playhead, setPlayhead] = useState(0);
|
|
160
|
+
const [playing, setPlaying] = useState(false);
|
|
161
|
+
const playRafRef = useRef<number | null>(null);
|
|
162
|
+
const playStartRef = useRef<{ wallMs: number; startT: number } | null>(null);
|
|
163
|
+
|
|
164
|
+
// Auto-keyframe mode: clicking the canvas at a time scrubs without writing.
|
|
165
|
+
const [autoKeyframe, setAutoKeyframe] = useState(false);
|
|
166
|
+
|
|
167
|
+
// Dock height (resizable via the top drag handle).
|
|
168
|
+
const [dockHeight, setDockHeight] = useState(DEFAULT_DOCK_HEIGHT);
|
|
169
|
+
const resizingRef = useRef(false);
|
|
170
|
+
const resizeStartRef = useRef<{ y: number; h: number } | null>(null);
|
|
171
|
+
|
|
172
|
+
// Expanded layers in the sidebar.
|
|
173
|
+
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(
|
|
174
|
+
() => new Set(tracks.map((t) => t.targetNodeId)),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Ruler / track area ref for pointer math.
|
|
178
|
+
const trackAreaRef = useRef<HTMLDivElement>(null);
|
|
179
|
+
|
|
180
|
+
// Local duration input state.
|
|
181
|
+
const [durationInput, setDurationInput] = useState(String(durationMs));
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
setDurationInput(String(durationMs));
|
|
184
|
+
}, [durationMs]);
|
|
185
|
+
|
|
186
|
+
// ── Preview postMessage ──────────────────────────────────────────────────
|
|
187
|
+
const sendPreview = useCallback(
|
|
188
|
+
(t: number) => {
|
|
189
|
+
const iframe = canvasIframeRef?.current;
|
|
190
|
+
if (!iframe?.contentWindow) return;
|
|
191
|
+
try {
|
|
192
|
+
iframe.contentWindow.postMessage(
|
|
193
|
+
{ type: "motion-preview", t, durationMs },
|
|
194
|
+
"*",
|
|
195
|
+
);
|
|
196
|
+
} catch {
|
|
197
|
+
// Best-effort preview; never throw.
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[canvasIframeRef, durationMs],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// ── Playback ─────────────────────────────────────────────────────────────
|
|
204
|
+
const stopPlayback = useCallback(() => {
|
|
205
|
+
if (playRafRef.current !== null) {
|
|
206
|
+
cancelAnimationFrame(playRafRef.current);
|
|
207
|
+
playRafRef.current = null;
|
|
208
|
+
}
|
|
209
|
+
playStartRef.current = null;
|
|
210
|
+
setPlaying(false);
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const startPlayback = useCallback(() => {
|
|
214
|
+
stopPlayback();
|
|
215
|
+
const startT = playhead >= 1 ? 0 : playhead;
|
|
216
|
+
playStartRef.current = { wallMs: performance.now(), startT };
|
|
217
|
+
setPlaying(true);
|
|
218
|
+
|
|
219
|
+
const tick = (now: number) => {
|
|
220
|
+
if (!playStartRef.current) return;
|
|
221
|
+
const elapsed = now - playStartRef.current.wallMs;
|
|
222
|
+
const t = Math.min(1, playStartRef.current.startT + elapsed / durationMs);
|
|
223
|
+
setPlayhead(t);
|
|
224
|
+
sendPreview(t);
|
|
225
|
+
if (t < 1) {
|
|
226
|
+
playRafRef.current = requestAnimationFrame(tick);
|
|
227
|
+
} else {
|
|
228
|
+
stopPlayback();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
playRafRef.current = requestAnimationFrame(tick);
|
|
232
|
+
}, [durationMs, playhead, sendPreview, stopPlayback]);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
return () => {
|
|
236
|
+
if (playRafRef.current !== null) cancelAnimationFrame(playRafRef.current);
|
|
237
|
+
};
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
// ── Playhead drag ─────────────────────────────────────────────────────────
|
|
241
|
+
const isDraggingPlayhead = useRef(false);
|
|
242
|
+
|
|
243
|
+
const handleRulerPointerDown = useCallback(
|
|
244
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
245
|
+
if (!trackAreaRef.current) return;
|
|
246
|
+
isDraggingPlayhead.current = true;
|
|
247
|
+
stopPlayback();
|
|
248
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
249
|
+
const rect = trackAreaRef.current.getBoundingClientRect();
|
|
250
|
+
const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
251
|
+
setPlayhead(t);
|
|
252
|
+
sendPreview(t);
|
|
253
|
+
},
|
|
254
|
+
[sendPreview, stopPlayback],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const handleRulerPointerMove = useCallback(
|
|
258
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
259
|
+
if (!isDraggingPlayhead.current || !trackAreaRef.current) return;
|
|
260
|
+
const rect = trackAreaRef.current.getBoundingClientRect();
|
|
261
|
+
const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
262
|
+
setPlayhead(t);
|
|
263
|
+
sendPreview(t);
|
|
264
|
+
},
|
|
265
|
+
[sendPreview],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const handleRulerPointerUp = useCallback(() => {
|
|
269
|
+
isDraggingPlayhead.current = false;
|
|
270
|
+
}, []);
|
|
271
|
+
|
|
272
|
+
// ── Dock resize drag ─────────────────────────────────────────────────────
|
|
273
|
+
const handleResizePointerDown = useCallback(
|
|
274
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
275
|
+
resizingRef.current = true;
|
|
276
|
+
resizeStartRef.current = { y: e.clientY, h: dockHeight };
|
|
277
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
278
|
+
},
|
|
279
|
+
[dockHeight],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const handleResizePointerMove = useCallback(
|
|
283
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
284
|
+
if (!resizingRef.current || !resizeStartRef.current) return;
|
|
285
|
+
const delta = resizeStartRef.current.y - e.clientY;
|
|
286
|
+
setDockHeight(
|
|
287
|
+
Math.max(MIN_DOCK_HEIGHT, resizeStartRef.current.h + delta),
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
[],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const handleResizePointerUp = useCallback(() => {
|
|
294
|
+
resizingRef.current = false;
|
|
295
|
+
resizeStartRef.current = null;
|
|
296
|
+
}, []);
|
|
297
|
+
|
|
298
|
+
// ── Keyframe helpers ─────────────────────────────────────────────────────
|
|
299
|
+
const updateTrack = useCallback(
|
|
300
|
+
(
|
|
301
|
+
nodeId: string,
|
|
302
|
+
property: string,
|
|
303
|
+
updater: (track: MotionDockTrack) => MotionDockTrack,
|
|
304
|
+
) => {
|
|
305
|
+
if (!onTracksChange) return;
|
|
306
|
+
onTracksChange(
|
|
307
|
+
tracks.map((tr) =>
|
|
308
|
+
tr.targetNodeId === nodeId && tr.property === property
|
|
309
|
+
? updater(tr)
|
|
310
|
+
: tr,
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
[tracks, onTracksChange],
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const addKeyframe = useCallback(
|
|
318
|
+
(track: MotionDockTrack) => {
|
|
319
|
+
const newKf: MotionKeyframe = {
|
|
320
|
+
t: playhead,
|
|
321
|
+
value: "0",
|
|
322
|
+
ease: defaultEase,
|
|
323
|
+
};
|
|
324
|
+
updateTrack(track.targetNodeId, track.property, (tr) => ({
|
|
325
|
+
...tr,
|
|
326
|
+
keyframes: [...tr.keyframes, newKf].sort((a, b) => a.t - b.t),
|
|
327
|
+
}));
|
|
328
|
+
},
|
|
329
|
+
[defaultEase, playhead, updateTrack],
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// ── Create a brand-new track (the "first track" path) ──────────────────────
|
|
333
|
+
// This is the entry point that turns the dock from a dead end into a working
|
|
334
|
+
// editor: with an element selected and no track yet, the user picks a property
|
|
335
|
+
// preset and we seed a two-keyframe track. Once at least one track exists,
|
|
336
|
+
// "Write to CSS" enables. Idempotent per (nodeId, property) — picking the same
|
|
337
|
+
// property twice just re-expands the existing track instead of duplicating.
|
|
338
|
+
const createTrack = useCallback(
|
|
339
|
+
(preset: MotionPropertyPreset) => {
|
|
340
|
+
if (!onTracksChange || !selectedTarget) return;
|
|
341
|
+
const { nodeId, label } = selectedTarget;
|
|
342
|
+
|
|
343
|
+
// Always expand the target layer so the new track row is visible.
|
|
344
|
+
setExpandedNodeIds((prev) => {
|
|
345
|
+
const next = new Set(prev);
|
|
346
|
+
next.add(nodeId);
|
|
347
|
+
return next;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (hasTrackFor(tracks, nodeId, preset.property)) {
|
|
351
|
+
// Track already exists — do not duplicate; the expand above surfaces it.
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const seeded = createMotionTrackFromPreset(nodeId, preset, defaultEase);
|
|
356
|
+
const newTrack: MotionDockTrack = { ...seeded, label };
|
|
357
|
+
onTracksChange([...tracks, newTrack]);
|
|
358
|
+
},
|
|
359
|
+
[defaultEase, onTracksChange, selectedTarget, tracks],
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const deleteKeyframe = useCallback(
|
|
363
|
+
(track: MotionDockTrack, kf: MotionKeyframe) => {
|
|
364
|
+
updateTrack(track.targetNodeId, track.property, (tr) => ({
|
|
365
|
+
...tr,
|
|
366
|
+
keyframes: tr.keyframes.filter((k) => k !== kf),
|
|
367
|
+
}));
|
|
368
|
+
},
|
|
369
|
+
[updateTrack],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// ── Ruler tick marks ──────────────────────────────────────────────────────
|
|
373
|
+
function rulerTicks(): { t: number; label: string }[] {
|
|
374
|
+
const count = 10;
|
|
375
|
+
return Array.from({ length: count + 1 }, (_, i) => {
|
|
376
|
+
const t = i / count;
|
|
377
|
+
const ms = Math.round(t * durationMs);
|
|
378
|
+
const s = (ms / 1000).toFixed(1);
|
|
379
|
+
return { t, label: `${s}s` };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Group tracks by layer (nodeId) ────────────────────────────────────────
|
|
384
|
+
type LayerGroup = {
|
|
385
|
+
nodeId: string;
|
|
386
|
+
label: string;
|
|
387
|
+
tracks: MotionDockTrack[];
|
|
388
|
+
};
|
|
389
|
+
const layers: LayerGroup[] = tracks.reduce<LayerGroup[]>((acc, track) => {
|
|
390
|
+
const existing = acc.find((g) => g.nodeId === track.targetNodeId);
|
|
391
|
+
if (existing) {
|
|
392
|
+
existing.tracks.push(track);
|
|
393
|
+
} else {
|
|
394
|
+
acc.push({
|
|
395
|
+
nodeId: track.targetNodeId,
|
|
396
|
+
label: track.label,
|
|
397
|
+
tracks: [track],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return acc;
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
403
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
404
|
+
return (
|
|
405
|
+
<div
|
|
406
|
+
className={cn(
|
|
407
|
+
"flex flex-col border-t border-border bg-background transition-all duration-150 select-none",
|
|
408
|
+
isOpen ? "" : "h-8",
|
|
409
|
+
)}
|
|
410
|
+
style={isOpen ? { height: dockHeight } : undefined}
|
|
411
|
+
>
|
|
412
|
+
{/* Resize handle */}
|
|
413
|
+
{isOpen && (
|
|
414
|
+
<div
|
|
415
|
+
className="absolute -top-1 left-0 right-0 h-2 cursor-ns-resize z-10"
|
|
416
|
+
onPointerDown={handleResizePointerDown}
|
|
417
|
+
onPointerMove={handleResizePointerMove}
|
|
418
|
+
onPointerUp={handleResizePointerUp}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{/* Dock toolbar */}
|
|
423
|
+
<div className="flex h-8 shrink-0 items-center gap-1 border-b border-border px-2">
|
|
424
|
+
{/* Collapse toggle */}
|
|
425
|
+
<Button
|
|
426
|
+
type="button"
|
|
427
|
+
variant="ghost"
|
|
428
|
+
size="icon"
|
|
429
|
+
className="size-6 shrink-0"
|
|
430
|
+
onClick={() => setOpen(!isOpen)}
|
|
431
|
+
aria-label={isOpen ? "Collapse motion dock" : "Expand motion dock"}
|
|
432
|
+
>
|
|
433
|
+
{isOpen ? (
|
|
434
|
+
<IconChevronDown className="size-3.5" />
|
|
435
|
+
) : (
|
|
436
|
+
<IconChevronRight className="size-3.5" />
|
|
437
|
+
)}
|
|
438
|
+
</Button>
|
|
439
|
+
|
|
440
|
+
<span className="text-[11px] font-medium tracking-wide text-muted-foreground uppercase mr-1">
|
|
441
|
+
Motion
|
|
442
|
+
</span>
|
|
443
|
+
|
|
444
|
+
{isOpen && (
|
|
445
|
+
<>
|
|
446
|
+
{/* Play / Pause */}
|
|
447
|
+
<Tooltip>
|
|
448
|
+
<TooltipTrigger asChild>
|
|
449
|
+
<Button
|
|
450
|
+
type="button"
|
|
451
|
+
variant="ghost"
|
|
452
|
+
size="icon"
|
|
453
|
+
className="size-6 shrink-0"
|
|
454
|
+
onClick={playing ? stopPlayback : startPlayback}
|
|
455
|
+
aria-label={playing ? "Pause" : "Play"}
|
|
456
|
+
>
|
|
457
|
+
{playing ? (
|
|
458
|
+
<IconPlayerPause className="size-3.5" />
|
|
459
|
+
) : (
|
|
460
|
+
<IconPlayerPlay className="size-3.5" />
|
|
461
|
+
)}
|
|
462
|
+
</Button>
|
|
463
|
+
</TooltipTrigger>
|
|
464
|
+
<TooltipContent side="top">
|
|
465
|
+
{playing ? "Pause" : "Play"}
|
|
466
|
+
</TooltipContent>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
|
|
469
|
+
{/* Stop / reset */}
|
|
470
|
+
<Tooltip>
|
|
471
|
+
<TooltipTrigger asChild>
|
|
472
|
+
<Button
|
|
473
|
+
type="button"
|
|
474
|
+
variant="ghost"
|
|
475
|
+
size="icon"
|
|
476
|
+
className="size-6 shrink-0"
|
|
477
|
+
onClick={() => {
|
|
478
|
+
stopPlayback();
|
|
479
|
+
setPlayhead(0);
|
|
480
|
+
sendPreview(0);
|
|
481
|
+
}}
|
|
482
|
+
aria-label="Reset playhead"
|
|
483
|
+
>
|
|
484
|
+
<IconPlayerStop className="size-3.5" />
|
|
485
|
+
</Button>
|
|
486
|
+
</TooltipTrigger>
|
|
487
|
+
<TooltipContent side="top">Reset</TooltipContent>
|
|
488
|
+
</Tooltip>
|
|
489
|
+
|
|
490
|
+
{/* Duration */}
|
|
491
|
+
<div className="flex items-center gap-1 ml-1">
|
|
492
|
+
<span className="text-[10px] text-muted-foreground">
|
|
493
|
+
Duration
|
|
494
|
+
</span>
|
|
495
|
+
<Input
|
|
496
|
+
type="number"
|
|
497
|
+
min={50}
|
|
498
|
+
step={50}
|
|
499
|
+
value={durationInput}
|
|
500
|
+
onChange={(e) => setDurationInput(e.target.value)}
|
|
501
|
+
onBlur={() => {
|
|
502
|
+
const ms = parseInt(durationInput, 10);
|
|
503
|
+
if (!isNaN(ms) && ms >= 50) {
|
|
504
|
+
onDurationChange?.(ms);
|
|
505
|
+
} else {
|
|
506
|
+
setDurationInput(String(durationMs));
|
|
507
|
+
}
|
|
508
|
+
}}
|
|
509
|
+
className="h-5 w-16 px-1 text-[11px]"
|
|
510
|
+
aria-label="Duration in ms"
|
|
511
|
+
/>
|
|
512
|
+
<span className="text-[10px] text-muted-foreground">ms</span>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
{/* Add track — the "create first track" entry point. */}
|
|
516
|
+
<div className="ml-2">
|
|
517
|
+
<AddTrackMenu
|
|
518
|
+
selectedTarget={selectedTarget}
|
|
519
|
+
onCreateTrack={createTrack}
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div className="ml-auto flex items-center gap-1">
|
|
524
|
+
{/* Auto-keyframe toggle */}
|
|
525
|
+
<Tooltip>
|
|
526
|
+
<TooltipTrigger asChild>
|
|
527
|
+
<Button
|
|
528
|
+
type="button"
|
|
529
|
+
variant={autoKeyframe ? "secondary" : "ghost"}
|
|
530
|
+
size="icon"
|
|
531
|
+
className={cn(
|
|
532
|
+
"size-6 shrink-0",
|
|
533
|
+
autoKeyframe && "text-primary",
|
|
534
|
+
)}
|
|
535
|
+
onClick={() => setAutoKeyframe((v) => !v)}
|
|
536
|
+
aria-label="Toggle auto-keyframe"
|
|
537
|
+
aria-pressed={autoKeyframe}
|
|
538
|
+
>
|
|
539
|
+
<IconBolt className="size-3.5" />
|
|
540
|
+
</Button>
|
|
541
|
+
</TooltipTrigger>
|
|
542
|
+
<TooltipContent side="top">Auto-keyframe</TooltipContent>
|
|
543
|
+
</Tooltip>
|
|
544
|
+
|
|
545
|
+
{/* Write to CSS */}
|
|
546
|
+
<Tooltip>
|
|
547
|
+
<TooltipTrigger asChild>
|
|
548
|
+
<Button
|
|
549
|
+
type="button"
|
|
550
|
+
size="sm"
|
|
551
|
+
className="h-6 px-2.5 text-[11px] gap-1"
|
|
552
|
+
disabled={applying || tracks.length === 0}
|
|
553
|
+
onClick={() => onApply?.(tracks, durationMs)}
|
|
554
|
+
>
|
|
555
|
+
{applying ? (
|
|
556
|
+
<IconRefresh className="size-3 animate-spin" />
|
|
557
|
+
) : (
|
|
558
|
+
<IconCode className="size-3" />
|
|
559
|
+
)}
|
|
560
|
+
Write to CSS
|
|
561
|
+
</Button>
|
|
562
|
+
</TooltipTrigger>
|
|
563
|
+
<TooltipContent side="top">
|
|
564
|
+
Compile timeline → managed <style
|
|
565
|
+
data-agent-native-motion> block
|
|
566
|
+
</TooltipContent>
|
|
567
|
+
</Tooltip>
|
|
568
|
+
</div>
|
|
569
|
+
</>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
{/* Dock body */}
|
|
574
|
+
{isOpen && (
|
|
575
|
+
<div className="flex flex-1 overflow-hidden">
|
|
576
|
+
{/* Layer sidebar */}
|
|
577
|
+
<div
|
|
578
|
+
className="flex flex-col shrink-0 border-r border-border overflow-y-auto"
|
|
579
|
+
style={{ width: LAYER_SIDEBAR_WIDTH }}
|
|
580
|
+
>
|
|
581
|
+
{/* Ruler spacer */}
|
|
582
|
+
<div
|
|
583
|
+
style={{ height: RULER_HEIGHT }}
|
|
584
|
+
className="border-b border-border"
|
|
585
|
+
/>
|
|
586
|
+
|
|
587
|
+
{layers.length === 0 ? (
|
|
588
|
+
<div className="flex flex-col items-center justify-center flex-1 gap-3 text-center px-4 py-6">
|
|
589
|
+
<IconLayersSubtract className="size-5 text-muted-foreground/40" />
|
|
590
|
+
{selectedTarget ? (
|
|
591
|
+
<>
|
|
592
|
+
<p className="text-[11px] text-muted-foreground/70 leading-snug">
|
|
593
|
+
Animate{" "}
|
|
594
|
+
<span className="font-medium text-foreground/80">
|
|
595
|
+
{selectedTarget.label}
|
|
596
|
+
</span>
|
|
597
|
+
. Pick a property to add the first track.
|
|
598
|
+
</p>
|
|
599
|
+
<AddTrackMenu
|
|
600
|
+
selectedTarget={selectedTarget}
|
|
601
|
+
onCreateTrack={createTrack}
|
|
602
|
+
variant="cta"
|
|
603
|
+
/>
|
|
604
|
+
</>
|
|
605
|
+
) : (
|
|
606
|
+
<p className="text-[11px] text-muted-foreground/70 leading-snug">
|
|
607
|
+
Select an element on the canvas, then add a track to animate
|
|
608
|
+
it.
|
|
609
|
+
</p>
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
) : (
|
|
613
|
+
layers.map((layer) => (
|
|
614
|
+
<LayerGroup
|
|
615
|
+
key={layer.nodeId}
|
|
616
|
+
layer={layer}
|
|
617
|
+
expanded={expandedNodeIds.has(layer.nodeId)}
|
|
618
|
+
onToggleExpand={() =>
|
|
619
|
+
setExpandedNodeIds((prev) => {
|
|
620
|
+
const next = new Set(prev);
|
|
621
|
+
if (next.has(layer.nodeId)) next.delete(layer.nodeId);
|
|
622
|
+
else next.add(layer.nodeId);
|
|
623
|
+
return next;
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
onAddKeyframe={(track) => addKeyframe(track)}
|
|
627
|
+
/>
|
|
628
|
+
))
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
{/* Timeline / track area */}
|
|
633
|
+
<div className="flex flex-col flex-1 overflow-hidden">
|
|
634
|
+
{/* Ruler + playhead drag */}
|
|
635
|
+
<div
|
|
636
|
+
ref={trackAreaRef}
|
|
637
|
+
className="relative shrink-0 border-b border-border cursor-col-resize"
|
|
638
|
+
style={{ height: RULER_HEIGHT }}
|
|
639
|
+
onPointerDown={handleRulerPointerDown}
|
|
640
|
+
onPointerMove={handleRulerPointerMove}
|
|
641
|
+
onPointerUp={handleRulerPointerUp}
|
|
642
|
+
>
|
|
643
|
+
{/* Tick marks */}
|
|
644
|
+
{rulerTicks().map(({ t, label }) => (
|
|
645
|
+
<div
|
|
646
|
+
key={t}
|
|
647
|
+
className="absolute top-0 flex flex-col items-center pointer-events-none"
|
|
648
|
+
style={{ left: `${t * 100}%`, transform: "translateX(-50%)" }}
|
|
649
|
+
>
|
|
650
|
+
<span className="text-[9px] text-muted-foreground/60 leading-none mt-1">
|
|
651
|
+
{label}
|
|
652
|
+
</span>
|
|
653
|
+
<div className="w-px h-2 bg-border mt-0.5" />
|
|
654
|
+
</div>
|
|
655
|
+
))}
|
|
656
|
+
|
|
657
|
+
{/* Playhead */}
|
|
658
|
+
<PlayheadLine t={playhead} height={RULER_HEIGHT} inRuler />
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
{/* Track rows with keyframe diamonds */}
|
|
662
|
+
<div className="relative flex-1 overflow-y-auto">
|
|
663
|
+
{layers.map((layer) => (
|
|
664
|
+
<LayerTrackRows
|
|
665
|
+
key={layer.nodeId}
|
|
666
|
+
layer={layer}
|
|
667
|
+
expanded={expandedNodeIds.has(layer.nodeId)}
|
|
668
|
+
playhead={playhead}
|
|
669
|
+
onDeleteKeyframe={deleteKeyframe}
|
|
670
|
+
onMoveKeyframe={(track, kf, newT) => {
|
|
671
|
+
updateTrack(track.targetNodeId, track.property, (tr) => ({
|
|
672
|
+
...tr,
|
|
673
|
+
keyframes: tr.keyframes.map((k) =>
|
|
674
|
+
k === kf ? { ...k, t: newT } : k,
|
|
675
|
+
),
|
|
676
|
+
}));
|
|
677
|
+
}}
|
|
678
|
+
/>
|
|
679
|
+
))}
|
|
680
|
+
|
|
681
|
+
{/* Playhead across track rows */}
|
|
682
|
+
<PlayheadLine
|
|
683
|
+
t={playhead}
|
|
684
|
+
height={layers.reduce(
|
|
685
|
+
(sum, layer) =>
|
|
686
|
+
sum +
|
|
687
|
+
ROW_HEIGHT +
|
|
688
|
+
(expandedNodeIds.has(layer.nodeId)
|
|
689
|
+
? layer.tracks.length * ROW_HEIGHT
|
|
690
|
+
: 0),
|
|
691
|
+
0,
|
|
692
|
+
)}
|
|
693
|
+
/>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
)}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ─── Sub-components ────────────────────────────────────────────────────────────
|
|
703
|
+
|
|
704
|
+
interface AddTrackMenuProps {
|
|
705
|
+
selectedTarget: { nodeId: string; label: string } | null;
|
|
706
|
+
onCreateTrack: (preset: MotionPropertyPreset) => void;
|
|
707
|
+
/** "toolbar" (compact button) or "cta" (prominent empty-state button). */
|
|
708
|
+
variant?: "toolbar" | "cta";
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Property-preset dropdown that creates a new motion track for the selected
|
|
713
|
+
* element. Disabled (with a hint) when nothing is selected, which is the only
|
|
714
|
+
* state where a first track cannot be created.
|
|
715
|
+
*/
|
|
716
|
+
function AddTrackMenu({
|
|
717
|
+
selectedTarget,
|
|
718
|
+
onCreateTrack,
|
|
719
|
+
variant = "toolbar",
|
|
720
|
+
}: AddTrackMenuProps) {
|
|
721
|
+
const disabled = !selectedTarget;
|
|
722
|
+
const trigger =
|
|
723
|
+
variant === "cta" ? (
|
|
724
|
+
<Button
|
|
725
|
+
type="button"
|
|
726
|
+
size="sm"
|
|
727
|
+
variant="secondary"
|
|
728
|
+
className="h-7 gap-1 text-[11px]"
|
|
729
|
+
disabled={disabled}
|
|
730
|
+
>
|
|
731
|
+
<IconPlus className="size-3.5" />
|
|
732
|
+
Add track
|
|
733
|
+
</Button>
|
|
734
|
+
) : (
|
|
735
|
+
<Button
|
|
736
|
+
type="button"
|
|
737
|
+
size="sm"
|
|
738
|
+
variant="ghost"
|
|
739
|
+
className="h-6 gap-1 px-2 text-[11px]"
|
|
740
|
+
disabled={disabled}
|
|
741
|
+
>
|
|
742
|
+
<IconPlus className="size-3.5" />
|
|
743
|
+
Add track
|
|
744
|
+
</Button>
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
// When disabled, render a tooltip-wrapped static button instead of a menu so
|
|
748
|
+
// the user learns they need a selection first.
|
|
749
|
+
if (disabled) {
|
|
750
|
+
return (
|
|
751
|
+
<Tooltip>
|
|
752
|
+
<TooltipTrigger asChild>
|
|
753
|
+
<span className="inline-flex">{trigger}</span>
|
|
754
|
+
</TooltipTrigger>
|
|
755
|
+
<TooltipContent side="top">
|
|
756
|
+
Select an element on the canvas first
|
|
757
|
+
</TooltipContent>
|
|
758
|
+
</Tooltip>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
<DropdownMenu>
|
|
764
|
+
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
|
765
|
+
<DropdownMenuContent align="start" className="w-52">
|
|
766
|
+
<DropdownMenuLabel className="truncate text-[10px] text-muted-foreground">
|
|
767
|
+
Animate “{selectedTarget.label}”
|
|
768
|
+
</DropdownMenuLabel>
|
|
769
|
+
<DropdownMenuSeparator />
|
|
770
|
+
{MOTION_PROPERTY_PRESETS.map((preset) => (
|
|
771
|
+
<DropdownMenuItem
|
|
772
|
+
key={`${preset.property}-${preset.label}`}
|
|
773
|
+
className="text-[12px]"
|
|
774
|
+
onSelect={() => onCreateTrack(preset)}
|
|
775
|
+
>
|
|
776
|
+
<IconDiamond className="size-3 text-primary/70" />
|
|
777
|
+
{preset.label}
|
|
778
|
+
</DropdownMenuItem>
|
|
779
|
+
))}
|
|
780
|
+
</DropdownMenuContent>
|
|
781
|
+
</DropdownMenu>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
interface PlayheadLineProps {
|
|
786
|
+
t: number;
|
|
787
|
+
height: number;
|
|
788
|
+
inRuler?: boolean;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function PlayheadLine({ t, height, inRuler }: PlayheadLineProps) {
|
|
792
|
+
return (
|
|
793
|
+
<div
|
|
794
|
+
className={cn(
|
|
795
|
+
"absolute top-0 pointer-events-none z-10",
|
|
796
|
+
inRuler ? "bg-primary/80" : "bg-primary/60",
|
|
797
|
+
)}
|
|
798
|
+
style={{
|
|
799
|
+
left: `${t * 100}%`,
|
|
800
|
+
width: PLAYHEAD_WIDTH,
|
|
801
|
+
height,
|
|
802
|
+
transform: "translateX(-50%)",
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
805
|
+
{inRuler && (
|
|
806
|
+
<div className="absolute -top-0 left-1/2 -translate-x-1/2 size-2 rounded-sm bg-primary" />
|
|
807
|
+
)}
|
|
808
|
+
</div>
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
interface LayerGroupProps {
|
|
813
|
+
layer: { nodeId: string; label: string; tracks: MotionDockTrack[] };
|
|
814
|
+
expanded: boolean;
|
|
815
|
+
onToggleExpand: () => void;
|
|
816
|
+
onAddKeyframe: (track: MotionDockTrack) => void;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function LayerGroup({
|
|
820
|
+
layer,
|
|
821
|
+
expanded,
|
|
822
|
+
onToggleExpand,
|
|
823
|
+
onAddKeyframe,
|
|
824
|
+
}: LayerGroupProps) {
|
|
825
|
+
return (
|
|
826
|
+
<>
|
|
827
|
+
{/* Layer header row */}
|
|
828
|
+
<div
|
|
829
|
+
className="flex items-center gap-1 px-2 hover:bg-accent/40 cursor-pointer"
|
|
830
|
+
style={{ height: ROW_HEIGHT }}
|
|
831
|
+
onClick={onToggleExpand}
|
|
832
|
+
role="button"
|
|
833
|
+
tabIndex={0}
|
|
834
|
+
onKeyDown={(e) => {
|
|
835
|
+
if (e.key === "Enter" || e.key === " ") onToggleExpand();
|
|
836
|
+
}}
|
|
837
|
+
aria-expanded={expanded}
|
|
838
|
+
>
|
|
839
|
+
<span className="shrink-0 text-muted-foreground">
|
|
840
|
+
{expanded ? (
|
|
841
|
+
<IconChevronDown className="size-3" />
|
|
842
|
+
) : (
|
|
843
|
+
<IconChevronRight className="size-3" />
|
|
844
|
+
)}
|
|
845
|
+
</span>
|
|
846
|
+
<span
|
|
847
|
+
className="flex-1 truncate text-[11px] font-medium"
|
|
848
|
+
title={layer.label}
|
|
849
|
+
>
|
|
850
|
+
{layer.label}
|
|
851
|
+
</span>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
{/* Property sub-rows */}
|
|
855
|
+
{expanded &&
|
|
856
|
+
layer.tracks.map((track) => (
|
|
857
|
+
<div
|
|
858
|
+
key={`${track.targetNodeId}-${track.property}`}
|
|
859
|
+
className="group flex items-center gap-1 pl-5 pr-2 hover:bg-accent/20"
|
|
860
|
+
style={{ height: ROW_HEIGHT }}
|
|
861
|
+
>
|
|
862
|
+
<IconDiamond className="size-2.5 shrink-0 text-primary/70" />
|
|
863
|
+
<span className="flex-1 truncate text-[10px] text-muted-foreground">
|
|
864
|
+
{track.property}
|
|
865
|
+
</span>
|
|
866
|
+
<Tooltip>
|
|
867
|
+
<TooltipTrigger asChild>
|
|
868
|
+
<Button
|
|
869
|
+
type="button"
|
|
870
|
+
variant="ghost"
|
|
871
|
+
size="icon"
|
|
872
|
+
className="size-5 shrink-0 opacity-0 group-hover:opacity-100"
|
|
873
|
+
onClick={(e) => {
|
|
874
|
+
e.stopPropagation();
|
|
875
|
+
onAddKeyframe(track);
|
|
876
|
+
}}
|
|
877
|
+
aria-label="Add keyframe"
|
|
878
|
+
>
|
|
879
|
+
<IconPlus className="size-3" />
|
|
880
|
+
</Button>
|
|
881
|
+
</TooltipTrigger>
|
|
882
|
+
<TooltipContent side="right">
|
|
883
|
+
Add keyframe at playhead
|
|
884
|
+
</TooltipContent>
|
|
885
|
+
</Tooltip>
|
|
886
|
+
</div>
|
|
887
|
+
))}
|
|
888
|
+
</>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
interface LayerTrackRowsProps {
|
|
893
|
+
layer: { nodeId: string; label: string; tracks: MotionDockTrack[] };
|
|
894
|
+
expanded: boolean;
|
|
895
|
+
playhead: number;
|
|
896
|
+
onDeleteKeyframe: (track: MotionDockTrack, kf: MotionKeyframe) => void;
|
|
897
|
+
onMoveKeyframe: (
|
|
898
|
+
track: MotionDockTrack,
|
|
899
|
+
kf: MotionKeyframe,
|
|
900
|
+
newT: number,
|
|
901
|
+
) => void;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function LayerTrackRows({
|
|
905
|
+
layer,
|
|
906
|
+
expanded,
|
|
907
|
+
playhead: _playhead,
|
|
908
|
+
onDeleteKeyframe,
|
|
909
|
+
onMoveKeyframe,
|
|
910
|
+
}: LayerTrackRowsProps) {
|
|
911
|
+
return (
|
|
912
|
+
<>
|
|
913
|
+
{/* Layer header spacer row */}
|
|
914
|
+
<div
|
|
915
|
+
className="relative border-b border-border/40"
|
|
916
|
+
style={{ height: ROW_HEIGHT }}
|
|
917
|
+
/>
|
|
918
|
+
|
|
919
|
+
{/* Property track rows */}
|
|
920
|
+
{expanded &&
|
|
921
|
+
layer.tracks.map((track) => (
|
|
922
|
+
<TrackRow
|
|
923
|
+
key={`${track.targetNodeId}-${track.property}`}
|
|
924
|
+
track={track}
|
|
925
|
+
onDeleteKeyframe={(kf) => onDeleteKeyframe(track, kf)}
|
|
926
|
+
onMoveKeyframe={(kf, newT) => onMoveKeyframe(track, kf, newT)}
|
|
927
|
+
/>
|
|
928
|
+
))}
|
|
929
|
+
</>
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
interface TrackRowProps {
|
|
934
|
+
track: MotionDockTrack;
|
|
935
|
+
onDeleteKeyframe: (kf: MotionKeyframe) => void;
|
|
936
|
+
onMoveKeyframe: (kf: MotionKeyframe, newT: number) => void;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function TrackRow({ track, onDeleteKeyframe, onMoveKeyframe }: TrackRowProps) {
|
|
940
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
941
|
+
const dragging = useRef<{
|
|
942
|
+
kf: MotionKeyframe;
|
|
943
|
+
startX: number;
|
|
944
|
+
startT: number;
|
|
945
|
+
} | null>(null);
|
|
946
|
+
|
|
947
|
+
const handleDiamondPointerDown = useCallback(
|
|
948
|
+
(e: ReactPointerEvent<SVGSVGElement>, kf: MotionKeyframe) => {
|
|
949
|
+
e.stopPropagation();
|
|
950
|
+
dragging.current = { kf, startX: e.clientX, startT: kf.t };
|
|
951
|
+
(e.target as Element).setPointerCapture(e.pointerId);
|
|
952
|
+
},
|
|
953
|
+
[],
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const handlePointerMove = useCallback(
|
|
957
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
958
|
+
if (!dragging.current || !rowRef.current) return;
|
|
959
|
+
const rect = rowRef.current.getBoundingClientRect();
|
|
960
|
+
const dx = e.clientX - dragging.current.startX;
|
|
961
|
+
const dt = dx / rect.width;
|
|
962
|
+
const newT = Math.max(0, Math.min(1, dragging.current.startT + dt));
|
|
963
|
+
onMoveKeyframe(dragging.current.kf, newT);
|
|
964
|
+
},
|
|
965
|
+
[onMoveKeyframe],
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
const handlePointerUp = useCallback(() => {
|
|
969
|
+
dragging.current = null;
|
|
970
|
+
}, []);
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<div
|
|
974
|
+
ref={rowRef}
|
|
975
|
+
className="relative border-b border-border/30"
|
|
976
|
+
style={{ height: ROW_HEIGHT }}
|
|
977
|
+
onPointerMove={handlePointerMove}
|
|
978
|
+
onPointerUp={handlePointerUp}
|
|
979
|
+
>
|
|
980
|
+
{/* Track baseline */}
|
|
981
|
+
<div className="absolute inset-y-1/2 left-0 right-0 h-px bg-border/50" />
|
|
982
|
+
|
|
983
|
+
{/* Keyframe diamonds */}
|
|
984
|
+
{track.keyframes.map((kf, i) => (
|
|
985
|
+
<KeyframeDiamond
|
|
986
|
+
key={i}
|
|
987
|
+
kf={kf}
|
|
988
|
+
onPointerDown={(e) => handleDiamondPointerDown(e, kf)}
|
|
989
|
+
onDelete={() => onDeleteKeyframe(kf)}
|
|
990
|
+
/>
|
|
991
|
+
))}
|
|
992
|
+
</div>
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
interface KeyframeDiamondProps {
|
|
997
|
+
kf: MotionKeyframe;
|
|
998
|
+
onPointerDown: (e: ReactPointerEvent<SVGSVGElement>) => void;
|
|
999
|
+
onDelete: () => void;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function KeyframeDiamond({
|
|
1003
|
+
kf,
|
|
1004
|
+
onPointerDown,
|
|
1005
|
+
onDelete,
|
|
1006
|
+
}: KeyframeDiamondProps) {
|
|
1007
|
+
const [hovered, setHovered] = useState(false);
|
|
1008
|
+
|
|
1009
|
+
return (
|
|
1010
|
+
<div
|
|
1011
|
+
className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 group"
|
|
1012
|
+
style={{ left: `${kf.t * 100}%` }}
|
|
1013
|
+
>
|
|
1014
|
+
<Tooltip>
|
|
1015
|
+
<TooltipTrigger asChild>
|
|
1016
|
+
<svg
|
|
1017
|
+
width={10}
|
|
1018
|
+
height={10}
|
|
1019
|
+
viewBox="0 0 10 10"
|
|
1020
|
+
className="cursor-grab active:cursor-grabbing"
|
|
1021
|
+
onPointerDown={onPointerDown}
|
|
1022
|
+
onPointerEnter={() => setHovered(true)}
|
|
1023
|
+
onPointerLeave={() => setHovered(false)}
|
|
1024
|
+
aria-label={`Keyframe at ${Math.round(kf.t * 100)}%`}
|
|
1025
|
+
>
|
|
1026
|
+
<rect
|
|
1027
|
+
x={1}
|
|
1028
|
+
y={1}
|
|
1029
|
+
width={8}
|
|
1030
|
+
height={8}
|
|
1031
|
+
rx={1}
|
|
1032
|
+
transform="rotate(45 5 5)"
|
|
1033
|
+
className={cn(
|
|
1034
|
+
"transition-colors",
|
|
1035
|
+
hovered
|
|
1036
|
+
? "fill-primary stroke-primary-foreground"
|
|
1037
|
+
: "fill-primary/70 stroke-primary/30",
|
|
1038
|
+
)}
|
|
1039
|
+
strokeWidth={1}
|
|
1040
|
+
/>
|
|
1041
|
+
</svg>
|
|
1042
|
+
</TooltipTrigger>
|
|
1043
|
+
<TooltipContent
|
|
1044
|
+
side="top"
|
|
1045
|
+
className="flex items-center gap-1 text-[10px]"
|
|
1046
|
+
>
|
|
1047
|
+
<span>
|
|
1048
|
+
{Math.round(kf.t * 100)}% — {kf.value}
|
|
1049
|
+
</span>
|
|
1050
|
+
<button
|
|
1051
|
+
type="button"
|
|
1052
|
+
className="ml-1 text-destructive hover:text-destructive/80"
|
|
1053
|
+
onClick={onDelete}
|
|
1054
|
+
aria-label="Delete keyframe"
|
|
1055
|
+
>
|
|
1056
|
+
<IconTrash className="size-3" />
|
|
1057
|
+
</button>
|
|
1058
|
+
</TooltipContent>
|
|
1059
|
+
</Tooltip>
|
|
1060
|
+
</div>
|
|
1061
|
+
);
|
|
1062
|
+
}
|