@agent-native/core 0.90.1 → 0.90.3

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.
Files changed (714) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/agent-native-toolkit.mdx +87 -80
  4. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +89 -80
  5. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +89 -80
  6. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +89 -80
  7. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +89 -80
  8. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +89 -80
  9. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +89 -80
  10. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +89 -80
  11. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +89 -80
  12. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +89 -80
  13. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +89 -80
  14. package/corpus/core/docs/content/toolkit-agent-ux.mdx +43 -0
  15. package/corpus/core/docs/content/toolkit-app-adapters.mdx +9 -0
  16. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +12 -0
  17. package/corpus/core/docs/content/toolkit-collaboration.mdx +74 -0
  18. package/corpus/core/docs/content/toolkit-command-navigation.mdx +106 -0
  19. package/corpus/core/docs/content/toolkit-comments-review.mdx +98 -0
  20. package/corpus/core/docs/content/toolkit-history.mdx +95 -0
  21. package/corpus/core/docs/content/toolkit-observability.mdx +41 -0
  22. package/corpus/core/docs/content/toolkit-org-team.mdx +80 -0
  23. package/corpus/core/docs/content/toolkit-resources.mdx +57 -0
  24. package/corpus/core/docs/content/toolkit-settings.mdx +65 -0
  25. package/corpus/core/docs/content/toolkit-setup-connections.mdx +109 -0
  26. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +18 -6
  27. package/corpus/core/docs/content/toolkit-sharing.mdx +81 -0
  28. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +9 -0
  29. package/corpus/core/docs/content/toolkit-ui.mdx +9 -0
  30. package/corpus/core/package.json +35 -1
  31. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +19 -1
  32. package/corpus/core/src/agent/engine/anthropic-engine.ts +25 -1
  33. package/corpus/core/src/agent/engine/registry.ts +23 -16
  34. package/corpus/core/src/agent/production-agent.ts +37 -6
  35. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  36. package/corpus/core/src/client/command-navigation/index.ts +15 -0
  37. package/corpus/core/src/client/comments-review/index.ts +1 -0
  38. package/corpus/core/src/client/history/VersionHistoryPanel.tsx +145 -0
  39. package/corpus/core/src/client/history/index.ts +18 -0
  40. package/corpus/core/src/client/history/use-history.ts +86 -0
  41. package/corpus/core/src/client/index.ts +76 -9
  42. package/corpus/core/src/client/org/OrgSwitcher.tsx +6 -6
  43. package/corpus/core/src/client/org/RequireActiveOrg.tsx +2 -2
  44. package/corpus/core/src/client/org/TeamPage.tsx +3 -3
  45. package/corpus/core/src/client/org/hooks.ts +32 -0
  46. package/corpus/core/src/client/org/index.ts +30 -1
  47. package/corpus/core/src/client/org-team/index.ts +1 -0
  48. package/corpus/core/src/client/review/ReviewStatusBadge.tsx +39 -0
  49. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +278 -0
  50. package/corpus/core/src/client/review/index.ts +25 -0
  51. package/corpus/core/src/client/review/use-review.ts +114 -0
  52. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
  53. package/corpus/core/src/client/settings/index.ts +11 -1
  54. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +94 -0
  55. package/corpus/core/src/client/setup-connections/ProviderReadinessBadge.tsx +73 -0
  56. package/corpus/core/src/client/setup-connections/SetupConnectionsPage.tsx +94 -0
  57. package/corpus/core/src/client/setup-connections/index.ts +46 -0
  58. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -14
  59. package/corpus/core/src/client/sharing/ShareDialog.tsx +3 -3
  60. package/corpus/core/src/client/use-action.ts +39 -2
  61. package/corpus/core/src/client/use-db-sync.ts +44 -18
  62. package/corpus/core/src/command-navigation/actions.ts +100 -0
  63. package/corpus/core/src/command-navigation/index.ts +12 -0
  64. package/corpus/core/src/comments-review/index.ts +1 -0
  65. package/corpus/core/src/db/create-get-db.ts +32 -2
  66. package/corpus/core/src/file-upload/builder.ts +24 -3
  67. package/corpus/core/src/file-upload/types.ts +3 -1
  68. package/corpus/core/src/history/actions/create-resource-version.ts +89 -0
  69. package/corpus/core/src/history/actions/get-resource-version.ts +60 -0
  70. package/corpus/core/src/history/actions/list-resource-history.ts +53 -0
  71. package/corpus/core/src/history/actions/list-resource-versions.ts +43 -0
  72. package/corpus/core/src/history/actions/restore-resource-version.ts +96 -0
  73. package/corpus/core/src/history/index.ts +27 -0
  74. package/corpus/core/src/history/registry.ts +99 -0
  75. package/corpus/core/src/history/store.ts +348 -0
  76. package/corpus/core/src/history/types.ts +69 -0
  77. package/corpus/core/src/index.ts +10 -0
  78. package/corpus/core/src/navigation/index.ts +170 -0
  79. package/corpus/core/src/org/index.ts +8 -0
  80. package/corpus/core/src/org/permissions.ts +30 -0
  81. package/corpus/core/src/org-team/index.ts +1 -0
  82. package/corpus/core/src/private-blob/registry.ts +1 -0
  83. package/corpus/core/src/review/actions/consume-review-feedback.ts +38 -0
  84. package/corpus/core/src/review/actions/create-review-comment.ts +107 -0
  85. package/corpus/core/src/review/actions/delete-review-comment.ts +55 -0
  86. package/corpus/core/src/review/actions/get-review-feedback.ts +54 -0
  87. package/corpus/core/src/review/actions/list-review-comments.ts +53 -0
  88. package/corpus/core/src/review/actions/reply-review-comment.ts +107 -0
  89. package/corpus/core/src/review/actions/resolve-review-thread.ts +60 -0
  90. package/corpus/core/src/review/actions/set-review-status.ts +58 -0
  91. package/corpus/core/src/review/index.ts +36 -0
  92. package/corpus/core/src/review/mentions.ts +44 -0
  93. package/corpus/core/src/review/registry.ts +99 -0
  94. package/corpus/core/src/review/store.ts +687 -0
  95. package/corpus/core/src/review/types.ts +97 -0
  96. package/corpus/core/src/server/action-discovery.ts +54 -0
  97. package/corpus/core/src/server/agent-engine-api-key-route.ts +5 -0
  98. package/corpus/core/src/server/credential-provider.ts +108 -0
  99. package/corpus/core/src/server/framework-request-handler.ts +52 -4
  100. package/corpus/core/src/setup-connections/index.ts +89 -0
  101. package/corpus/core/src/setup-connections/onboarding.ts +47 -0
  102. package/corpus/core/src/transcription/builder-transcription.ts +9 -2
  103. package/corpus/core/src/vite/action-types-plugin.ts +52 -0
  104. package/corpus/templates/analytics/app/components/layout/Layout.tsx +2 -1
  105. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -2
  106. package/corpus/templates/analytics/app/global.css +22 -10
  107. package/corpus/templates/analytics/app/i18n/zh-TW.ts +6 -1
  108. package/corpus/templates/analytics/app/i18n-data.ts +81 -13
  109. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  110. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  111. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +528 -108
  112. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +466 -183
  113. package/corpus/templates/analytics/changelog/2026-07-07-session-replays-load-sooner-show-clearer-activity-timelines-.md +6 -0
  114. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
  115. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
  116. package/corpus/templates/assets/AGENTS.md +23 -4
  117. package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
  118. package/corpus/templates/assets/actions/_helpers.ts +8 -1
  119. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
  120. package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
  121. package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
  122. package/corpus/templates/assets/actions/generate-image.ts +342 -25
  123. package/corpus/templates/assets/actions/get-library.ts +4 -3
  124. package/corpus/templates/assets/actions/navigate.ts +2 -1
  125. package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
  126. package/corpus/templates/assets/actions/update-asset.ts +1 -0
  127. package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
  128. package/corpus/templates/assets/actions/view-screen.ts +8 -1
  129. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
  130. package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
  131. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
  132. package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
  133. package/corpus/templates/assets/app/i18n-data.ts +650 -0
  134. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
  135. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
  136. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
  137. package/corpus/templates/assets/app/routes/library.tsx +43 -1
  138. package/corpus/templates/assets/server/lib/generation.ts +366 -44
  139. package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
  140. package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
  141. package/corpus/templates/assets/shared/api.ts +47 -0
  142. package/corpus/templates/brain/app/routes/settings.tsx +2 -2
  143. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +32 -6
  144. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +120 -40
  145. package/corpus/templates/calendar/app/hooks/use-people.ts +55 -0
  146. package/corpus/templates/calendar/changelog/2026-07-07-guest-suggestions-now-find-coworkers-from-your-google-worksp.md +6 -0
  147. package/corpus/templates/calendar/changelog/2026-07-07-zoom-stays-selected-after-connecting-it-from-a-draft-event.md +6 -0
  148. package/corpus/templates/clips/actions/create-recording.ts +5 -1
  149. package/corpus/templates/clips/actions/finalize-recording.ts +140 -1
  150. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  151. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +1 -0
  152. package/corpus/templates/clips/actions/request-transcript.ts +31 -0
  153. package/corpus/templates/clips/actions/set-thumbnail.ts +2 -0
  154. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
  155. package/corpus/templates/clips/app/components/library/organization-switcher.tsx +2 -2
  156. package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
  157. package/corpus/templates/clips/app/components/player/scrubber-position.ts +9 -0
  158. package/corpus/templates/clips/app/components/player/scrubber.tsx +5 -2
  159. package/corpus/templates/clips/app/components/player/share-dialog.tsx +69 -41
  160. package/corpus/templates/clips/app/components/player/video-player.tsx +38 -1
  161. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
  162. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +3 -3
  163. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +9 -19
  164. package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
  165. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -3
  166. package/corpus/templates/clips/app/routes/record.tsx +178 -80
  167. package/corpus/templates/clips/changelog/2026-07-02-recordings-now-wait-for-storage-media-to-serve-before-finishing.md +6 -0
  168. package/corpus/templates/clips/changelog/2026-07-07-a-pending-app-update-no-longer-blocks-screen-recording-updat.md +6 -0
  169. package/corpus/templates/clips/changelog/2026-07-07-chrome-extension-recordings-keep-audio-after-refreshing-tab.md +6 -0
  170. package/corpus/templates/clips/changelog/2026-07-07-clip-playback-progress-now-starts-and-finishes-at-the-right-times.md +6 -0
  171. package/corpus/templates/clips/changelog/2026-07-07-sharing-public-clips-now-stays-focused-on-the-normal-link-wi.md +6 -0
  172. package/corpus/templates/clips/chrome-extension/src/background.ts +18 -0
  173. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +23 -8
  174. package/corpus/templates/clips/desktop/src/app.tsx +40 -3
  175. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +19 -7
  176. package/corpus/templates/clips/desktop/src/lib/permissions.ts +2 -0
  177. package/corpus/templates/clips/desktop/src/lib/updater.ts +25 -2
  178. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +7 -0
  179. package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +1 -0
  180. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +23 -6
  181. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +28 -3
  182. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +13 -6
  183. package/corpus/templates/clips/shared/finalize-recovery.ts +35 -3
  184. package/corpus/templates/content/AGENTS.md +52 -52
  185. package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
  186. package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
  187. package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
  188. package/corpus/templates/content/actions/_database-utils.ts +44 -4
  189. package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
  190. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
  191. package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
  192. package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
  193. package/corpus/templates/content/actions/delete-content-database.ts +1 -0
  194. package/corpus/templates/content/actions/delete-document.ts +337 -52
  195. package/corpus/templates/content/actions/get-document.ts +11 -2
  196. package/corpus/templates/content/actions/list-content-databases.ts +57 -9
  197. package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
  198. package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
  199. package/corpus/templates/content/actions/restore-content-database.ts +1 -0
  200. package/corpus/templates/content/actions/update-document.ts +43 -0
  201. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
  202. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
  203. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
  204. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
  205. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
  206. package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
  207. package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
  208. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
  209. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
  210. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
  211. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
  212. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
  213. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
  214. package/corpus/templates/content/app/global.css +41 -0
  215. package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
  216. package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
  217. package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
  218. package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
  219. package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
  220. package/corpus/templates/content/app/i18n-data.ts +355 -0
  221. package/corpus/templates/content/app/root.tsx +22 -0
  222. package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
  223. package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
  224. package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
  225. package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
  226. package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
  227. package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
  228. package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
  229. package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
  230. package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
  231. package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
  232. package/corpus/templates/content/changelog/2026-07-07-permanently-deleting-trashed-databases-now-finishes-reliably.md +6 -0
  233. package/corpus/templates/content/shared/api.ts +13 -0
  234. package/corpus/templates/content/shared/builder-mdx.ts +16 -3
  235. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
  236. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
  237. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
  238. package/corpus/templates/design/AGENTS.md +9 -2
  239. package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
  240. package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
  241. package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
  242. package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
  243. package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
  244. package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
  245. package/corpus/templates/design/actions/create-component.ts +29 -60
  246. package/corpus/templates/design/actions/create-file.ts +9 -2
  247. package/corpus/templates/design/actions/duplicate-design.ts +31 -2
  248. package/corpus/templates/design/actions/edit-design.ts +36 -38
  249. package/corpus/templates/design/actions/generate-design-title.ts +87 -0
  250. package/corpus/templates/design/actions/generate-design.ts +53 -18
  251. package/corpus/templates/design/actions/generate-screens.ts +108 -4
  252. package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
  253. package/corpus/templates/design/actions/insert-asset.ts +34 -29
  254. package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
  255. package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
  256. package/corpus/templates/design/actions/present-design-variants.ts +7 -2
  257. package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
  258. package/corpus/templates/design/actions/update-file.ts +190 -75
  259. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
  260. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
  261. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
  262. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
  263. package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
  264. package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
  265. package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
  266. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
  267. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
  268. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
  269. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
  270. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
  271. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
  272. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
  273. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
  274. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
  275. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
  276. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
  277. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
  278. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
  279. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
  280. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
  281. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
  282. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
  283. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
  284. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
  285. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
  286. package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
  287. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
  288. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
  289. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
  290. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
  291. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
  292. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
  293. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
  294. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
  295. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
  296. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
  297. package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
  298. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
  299. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
  300. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
  301. package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
  302. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
  303. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
  304. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
  305. package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
  306. package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
  307. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
  308. package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
  309. package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
  310. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
  311. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
  312. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
  313. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
  314. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
  315. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
  316. package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
  317. package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
  318. package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
  319. package/corpus/templates/design/app/components/design/types.ts +15 -0
  320. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
  321. package/corpus/templates/design/app/global.css +8 -8
  322. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
  323. package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
  324. package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
  325. package/corpus/templates/design/app/i18n-data.ts +1664 -152
  326. package/corpus/templates/design/app/i18n-motion.ts +563 -0
  327. package/corpus/templates/design/app/lib/design-import.ts +73 -0
  328. package/corpus/templates/design/app/pages/Index.tsx +41 -22
  329. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
  330. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
  331. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
  332. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
  333. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
  334. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
  335. package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
  336. package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
  337. package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
  338. package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
  339. package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
  340. package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
  341. package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
  342. package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
  343. package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
  344. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
  345. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
  346. package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
  347. package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
  348. package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
  349. package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
  350. package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
  351. package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
  352. package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
  353. package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
  354. package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
  355. package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
  356. package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
  357. package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
  358. package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
  359. package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
  360. package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
  361. package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
  362. package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
  363. package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
  364. package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
  365. package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
  366. package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
  367. package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
  368. package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
  369. package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
  370. package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
  371. package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
  372. package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
  373. package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
  374. package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
  375. package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
  376. package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
  377. package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
  378. package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
  379. package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
  380. package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
  381. package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
  382. package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
  383. package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
  384. package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
  385. package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
  386. package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
  387. package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
  388. package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
  389. package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
  390. package/corpus/templates/design/server/source-workspace.ts +139 -59
  391. package/corpus/templates/design/shared/board-file.ts +11 -2
  392. package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
  393. package/corpus/templates/design/shared/canvas-math.ts +354 -35
  394. package/corpus/templates/design/shared/code-layer.ts +277 -8
  395. package/corpus/templates/design/shared/design-state.ts +11 -4
  396. package/corpus/templates/design/shared/interaction-states.ts +548 -0
  397. package/corpus/templates/design/shared/motion-compiler.ts +186 -23
  398. package/corpus/templates/design/shared/motion-easing.ts +294 -0
  399. package/corpus/templates/design/shared/motion-timeline.ts +400 -34
  400. package/corpus/templates/design/shared/prompt-title.ts +95 -0
  401. package/corpus/templates/design/shared/responsive-classes.ts +448 -10
  402. package/corpus/templates/design/shared/screen-annotation.ts +46 -0
  403. package/corpus/templates/design/shared/shader-fills.ts +1009 -0
  404. package/corpus/templates/design/shared/shader-presets.ts +852 -4
  405. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
  406. package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
  407. package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
  408. package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
  409. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  410. package/dist/agent/engine/ai-sdk-engine.js +15 -1
  411. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  412. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  413. package/dist/agent/engine/anthropic-engine.js +20 -1
  414. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  415. package/dist/agent/engine/registry.d.ts.map +1 -1
  416. package/dist/agent/engine/registry.js +22 -14
  417. package/dist/agent/engine/registry.js.map +1 -1
  418. package/dist/agent/production-agent.d.ts.map +1 -1
  419. package/dist/agent/production-agent.js +27 -7
  420. package/dist/agent/production-agent.js.map +1 -1
  421. package/dist/client/AgentPanel.js +1 -1
  422. package/dist/client/AgentPanel.js.map +1 -1
  423. package/dist/client/command-navigation/index.d.ts +4 -0
  424. package/dist/client/command-navigation/index.d.ts.map +1 -0
  425. package/dist/client/command-navigation/index.js +4 -0
  426. package/dist/client/command-navigation/index.js.map +1 -0
  427. package/dist/client/comments-review/index.d.ts +2 -0
  428. package/dist/client/comments-review/index.d.ts.map +1 -0
  429. package/dist/client/comments-review/index.js +2 -0
  430. package/dist/client/comments-review/index.js.map +1 -0
  431. package/dist/client/history/VersionHistoryPanel.d.ts +18 -0
  432. package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -0
  433. package/dist/client/history/VersionHistoryPanel.js +24 -0
  434. package/dist/client/history/VersionHistoryPanel.js.map +1 -0
  435. package/dist/client/history/index.d.ts +3 -0
  436. package/dist/client/history/index.d.ts.map +1 -0
  437. package/dist/client/history/index.js +3 -0
  438. package/dist/client/history/index.js.map +1 -0
  439. package/dist/client/history/use-history.d.ts +46 -0
  440. package/dist/client/history/use-history.d.ts.map +1 -0
  441. package/dist/client/history/use-history.js +18 -0
  442. package/dist/client/history/use-history.js.map +1 -0
  443. package/dist/client/index.d.ts +6 -4
  444. package/dist/client/index.d.ts.map +1 -1
  445. package/dist/client/index.js +6 -4
  446. package/dist/client/index.js.map +1 -1
  447. package/dist/client/org/OrgSwitcher.js +5 -5
  448. package/dist/client/org/OrgSwitcher.js.map +1 -1
  449. package/dist/client/org/RequireActiveOrg.js +2 -2
  450. package/dist/client/org/RequireActiveOrg.js.map +1 -1
  451. package/dist/client/org/TeamPage.js +3 -3
  452. package/dist/client/org/TeamPage.js.map +1 -1
  453. package/dist/client/org/hooks.d.ts +12 -1
  454. package/dist/client/org/hooks.d.ts.map +1 -1
  455. package/dist/client/org/hooks.js +15 -0
  456. package/dist/client/org/hooks.js.map +1 -1
  457. package/dist/client/org/index.d.ts +4 -2
  458. package/dist/client/org/index.d.ts.map +1 -1
  459. package/dist/client/org/index.js +3 -1
  460. package/dist/client/org/index.js.map +1 -1
  461. package/dist/client/org-team/index.d.ts +2 -0
  462. package/dist/client/org-team/index.d.ts.map +1 -0
  463. package/dist/client/org-team/index.js +2 -0
  464. package/dist/client/org-team/index.js.map +1 -0
  465. package/dist/client/review/ReviewStatusBadge.d.ts +7 -0
  466. package/dist/client/review/ReviewStatusBadge.d.ts.map +1 -0
  467. package/dist/client/review/ReviewStatusBadge.js +19 -0
  468. package/dist/client/review/ReviewStatusBadge.js.map +1 -0
  469. package/dist/client/review/ReviewThreadPanel.d.ts +20 -0
  470. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -0
  471. package/dist/client/review/ReviewThreadPanel.js +84 -0
  472. package/dist/client/review/ReviewThreadPanel.js.map +1 -0
  473. package/dist/client/review/index.d.ts +4 -0
  474. package/dist/client/review/index.d.ts.map +1 -0
  475. package/dist/client/review/index.js +4 -0
  476. package/dist/client/review/index.js.map +1 -0
  477. package/dist/client/review/use-review.d.ts +68 -0
  478. package/dist/client/review/use-review.d.ts.map +1 -0
  479. package/dist/client/review/use-review.js +22 -0
  480. package/dist/client/review/use-review.js.map +1 -0
  481. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  482. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
  483. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  484. package/dist/client/settings/index.d.ts +1 -1
  485. package/dist/client/settings/index.d.ts.map +1 -1
  486. package/dist/client/settings/index.js +1 -1
  487. package/dist/client/settings/index.js.map +1 -1
  488. package/dist/client/setup-connections/BuilderConnectCard.d.ts +9 -0
  489. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -0
  490. package/dist/client/setup-connections/BuilderConnectCard.js +25 -0
  491. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -0
  492. package/dist/client/setup-connections/ProviderReadinessBadge.d.ts +8 -0
  493. package/dist/client/setup-connections/ProviderReadinessBadge.d.ts.map +1 -0
  494. package/dist/client/setup-connections/ProviderReadinessBadge.js +32 -0
  495. package/dist/client/setup-connections/ProviderReadinessBadge.js.map +1 -0
  496. package/dist/client/setup-connections/SetupConnectionsPage.d.ts +17 -0
  497. package/dist/client/setup-connections/SetupConnectionsPage.d.ts.map +1 -0
  498. package/dist/client/setup-connections/SetupConnectionsPage.js +10 -0
  499. package/dist/client/setup-connections/SetupConnectionsPage.js.map +1 -0
  500. package/dist/client/setup-connections/index.d.ts +7 -0
  501. package/dist/client/setup-connections/index.d.ts.map +1 -0
  502. package/dist/client/setup-connections/index.js +7 -0
  503. package/dist/client/setup-connections/index.js.map +1 -0
  504. package/dist/client/sharing/ShareButton.d.ts +4 -4
  505. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  506. package/dist/client/sharing/ShareButton.js +13 -7
  507. package/dist/client/sharing/ShareButton.js.map +1 -1
  508. package/dist/client/sharing/ShareDialog.js +3 -3
  509. package/dist/client/sharing/ShareDialog.js.map +1 -1
  510. package/dist/client/use-action.d.ts +3 -0
  511. package/dist/client/use-action.d.ts.map +1 -1
  512. package/dist/client/use-action.js +34 -3
  513. package/dist/client/use-action.js.map +1 -1
  514. package/dist/client/use-db-sync.d.ts +4 -0
  515. package/dist/client/use-db-sync.d.ts.map +1 -1
  516. package/dist/client/use-db-sync.js +37 -18
  517. package/dist/client/use-db-sync.js.map +1 -1
  518. package/dist/collab/routes.d.ts +1 -1
  519. package/dist/command-navigation/actions.d.ts +25 -0
  520. package/dist/command-navigation/actions.d.ts.map +1 -0
  521. package/dist/command-navigation/actions.js +64 -0
  522. package/dist/command-navigation/actions.js.map +1 -0
  523. package/dist/command-navigation/index.d.ts +4 -0
  524. package/dist/command-navigation/index.d.ts.map +1 -0
  525. package/dist/command-navigation/index.js +4 -0
  526. package/dist/command-navigation/index.js.map +1 -0
  527. package/dist/comments-review/index.d.ts +2 -0
  528. package/dist/comments-review/index.d.ts.map +1 -0
  529. package/dist/comments-review/index.js +2 -0
  530. package/dist/comments-review/index.js.map +1 -0
  531. package/dist/db/create-get-db.d.ts +25 -0
  532. package/dist/db/create-get-db.d.ts.map +1 -1
  533. package/dist/db/create-get-db.js +25 -2
  534. package/dist/db/create-get-db.js.map +1 -1
  535. package/dist/file-upload/builder.d.ts.map +1 -1
  536. package/dist/file-upload/builder.js +18 -2
  537. package/dist/file-upload/builder.js.map +1 -1
  538. package/dist/file-upload/types.d.ts +3 -0
  539. package/dist/file-upload/types.d.ts.map +1 -1
  540. package/dist/file-upload/types.js.map +1 -1
  541. package/dist/history/actions/create-resource-version.d.ts +10 -0
  542. package/dist/history/actions/create-resource-version.d.ts.map +1 -0
  543. package/dist/history/actions/create-resource-version.js +65 -0
  544. package/dist/history/actions/create-resource-version.js.map +1 -0
  545. package/dist/history/actions/get-resource-version.d.ts +10 -0
  546. package/dist/history/actions/get-resource-version.d.ts.map +1 -0
  547. package/dist/history/actions/get-resource-version.js +38 -0
  548. package/dist/history/actions/get-resource-version.js.map +1 -0
  549. package/dist/history/actions/list-resource-history.d.ts +10 -0
  550. package/dist/history/actions/list-resource-history.d.ts.map +1 -0
  551. package/dist/history/actions/list-resource-history.js +45 -0
  552. package/dist/history/actions/list-resource-history.js.map +1 -0
  553. package/dist/history/actions/list-resource-versions.d.ts +10 -0
  554. package/dist/history/actions/list-resource-versions.d.ts.map +1 -0
  555. package/dist/history/actions/list-resource-versions.js +35 -0
  556. package/dist/history/actions/list-resource-versions.js.map +1 -0
  557. package/dist/history/actions/restore-resource-version.d.ts +11 -0
  558. package/dist/history/actions/restore-resource-version.d.ts.map +1 -0
  559. package/dist/history/actions/restore-resource-version.js +59 -0
  560. package/dist/history/actions/restore-resource-version.js.map +1 -0
  561. package/dist/history/index.d.ts +4 -0
  562. package/dist/history/index.d.ts.map +1 -0
  563. package/dist/history/index.js +3 -0
  564. package/dist/history/index.js.map +1 -0
  565. package/dist/history/registry.d.ts +10 -0
  566. package/dist/history/registry.d.ts.map +1 -0
  567. package/dist/history/registry.js +61 -0
  568. package/dist/history/registry.js.map +1 -0
  569. package/dist/history/store.d.ts +34 -0
  570. package/dist/history/store.d.ts.map +1 -0
  571. package/dist/history/store.js +265 -0
  572. package/dist/history/store.js.map +1 -0
  573. package/dist/history/types.d.ts +55 -0
  574. package/dist/history/types.d.ts.map +1 -0
  575. package/dist/history/types.js +2 -0
  576. package/dist/history/types.js.map +1 -0
  577. package/dist/index.d.ts +6 -0
  578. package/dist/index.d.ts.map +1 -1
  579. package/dist/index.js +6 -0
  580. package/dist/index.js.map +1 -1
  581. package/dist/navigation/index.d.ts +53 -0
  582. package/dist/navigation/index.d.ts.map +1 -0
  583. package/dist/navigation/index.js +105 -0
  584. package/dist/navigation/index.js.map +1 -0
  585. package/dist/observability/routes.d.ts +3 -3
  586. package/dist/org/index.d.ts +1 -0
  587. package/dist/org/index.d.ts.map +1 -1
  588. package/dist/org/index.js +1 -0
  589. package/dist/org/index.js.map +1 -1
  590. package/dist/org/permissions.d.ts +7 -0
  591. package/dist/org/permissions.d.ts.map +1 -0
  592. package/dist/org/permissions.js +21 -0
  593. package/dist/org/permissions.js.map +1 -0
  594. package/dist/org-team/index.d.ts +2 -0
  595. package/dist/org-team/index.d.ts.map +1 -0
  596. package/dist/org-team/index.js +2 -0
  597. package/dist/org-team/index.js.map +1 -0
  598. package/dist/private-blob/registry.d.ts.map +1 -1
  599. package/dist/private-blob/registry.js +1 -0
  600. package/dist/private-blob/registry.js.map +1 -1
  601. package/dist/resources/handlers.d.ts +2 -2
  602. package/dist/review/actions/consume-review-feedback.d.ts +9 -0
  603. package/dist/review/actions/consume-review-feedback.d.ts.map +1 -0
  604. package/dist/review/actions/consume-review-feedback.js +29 -0
  605. package/dist/review/actions/consume-review-feedback.js.map +1 -0
  606. package/dist/review/actions/create-review-comment.d.ts +18 -0
  607. package/dist/review/actions/create-review-comment.d.ts.map +1 -0
  608. package/dist/review/actions/create-review-comment.js +81 -0
  609. package/dist/review/actions/create-review-comment.js.map +1 -0
  610. package/dist/review/actions/delete-review-comment.d.ts +10 -0
  611. package/dist/review/actions/delete-review-comment.d.ts.map +1 -0
  612. package/dist/review/actions/delete-review-comment.js +45 -0
  613. package/dist/review/actions/delete-review-comment.js.map +1 -0
  614. package/dist/review/actions/get-review-feedback.d.ts +10 -0
  615. package/dist/review/actions/get-review-feedback.d.ts.map +1 -0
  616. package/dist/review/actions/get-review-feedback.js +45 -0
  617. package/dist/review/actions/get-review-feedback.js.map +1 -0
  618. package/dist/review/actions/list-review-comments.d.ts +13 -0
  619. package/dist/review/actions/list-review-comments.d.ts.map +1 -0
  620. package/dist/review/actions/list-review-comments.js +44 -0
  621. package/dist/review/actions/list-review-comments.js.map +1 -0
  622. package/dist/review/actions/reply-review-comment.d.ts +16 -0
  623. package/dist/review/actions/reply-review-comment.d.ts.map +1 -0
  624. package/dist/review/actions/reply-review-comment.js +84 -0
  625. package/dist/review/actions/reply-review-comment.js.map +1 -0
  626. package/dist/review/actions/resolve-review-thread.d.ts +11 -0
  627. package/dist/review/actions/resolve-review-thread.d.ts.map +1 -0
  628. package/dist/review/actions/resolve-review-thread.js +46 -0
  629. package/dist/review/actions/resolve-review-thread.js.map +1 -0
  630. package/dist/review/actions/set-review-status.d.ts +9 -0
  631. package/dist/review/actions/set-review-status.d.ts.map +1 -0
  632. package/dist/review/actions/set-review-status.js +43 -0
  633. package/dist/review/actions/set-review-status.js.map +1 -0
  634. package/dist/review/index.d.ts +5 -0
  635. package/dist/review/index.d.ts.map +1 -0
  636. package/dist/review/index.js +4 -0
  637. package/dist/review/index.js.map +1 -0
  638. package/dist/review/mentions.d.ts +4 -0
  639. package/dist/review/mentions.d.ts.map +1 -0
  640. package/dist/review/mentions.js +39 -0
  641. package/dist/review/mentions.js.map +1 -0
  642. package/dist/review/registry.d.ts +10 -0
  643. package/dist/review/registry.d.ts.map +1 -0
  644. package/dist/review/registry.js +61 -0
  645. package/dist/review/registry.js.map +1 -0
  646. package/dist/review/store.d.ts +63 -0
  647. package/dist/review/store.d.ts.map +1 -0
  648. package/dist/review/store.js +558 -0
  649. package/dist/review/store.js.map +1 -0
  650. package/dist/review/types.d.ts +77 -0
  651. package/dist/review/types.d.ts.map +1 -0
  652. package/dist/review/types.js +2 -0
  653. package/dist/review/types.js.map +1 -0
  654. package/dist/secrets/routes.d.ts +9 -9
  655. package/dist/server/action-discovery.d.ts.map +1 -1
  656. package/dist/server/action-discovery.js +54 -0
  657. package/dist/server/action-discovery.js.map +1 -1
  658. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  659. package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
  660. package/dist/server/agent-engine-api-key-route.js +5 -0
  661. package/dist/server/agent-engine-api-key-route.js.map +1 -1
  662. package/dist/server/credential-provider.d.ts +26 -0
  663. package/dist/server/credential-provider.d.ts.map +1 -1
  664. package/dist/server/credential-provider.js +82 -0
  665. package/dist/server/credential-provider.js.map +1 -1
  666. package/dist/server/framework-request-handler.d.ts.map +1 -1
  667. package/dist/server/framework-request-handler.js +37 -1
  668. package/dist/server/framework-request-handler.js.map +1 -1
  669. package/dist/setup-connections/index.d.ts +6 -0
  670. package/dist/setup-connections/index.d.ts.map +1 -0
  671. package/dist/setup-connections/index.js +6 -0
  672. package/dist/setup-connections/index.js.map +1 -0
  673. package/dist/setup-connections/onboarding.d.ts +14 -0
  674. package/dist/setup-connections/onboarding.d.ts.map +1 -0
  675. package/dist/setup-connections/onboarding.js +23 -0
  676. package/dist/setup-connections/onboarding.js.map +1 -0
  677. package/dist/transcription/builder-transcription.d.ts +1 -0
  678. package/dist/transcription/builder-transcription.d.ts.map +1 -1
  679. package/dist/transcription/builder-transcription.js +5 -2
  680. package/dist/transcription/builder-transcription.js.map +1 -1
  681. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  682. package/dist/vite/action-types-plugin.js +52 -0
  683. package/dist/vite/action-types-plugin.js.map +1 -1
  684. package/docs/content/agent-native-toolkit.mdx +87 -80
  685. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +89 -80
  686. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +89 -80
  687. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +89 -80
  688. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +89 -80
  689. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +89 -80
  690. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +89 -80
  691. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +89 -80
  692. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +89 -80
  693. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +89 -80
  694. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +89 -80
  695. package/docs/content/toolkit-agent-ux.mdx +43 -0
  696. package/docs/content/toolkit-app-adapters.mdx +9 -0
  697. package/docs/content/toolkit-collaboration-ui.mdx +12 -0
  698. package/docs/content/toolkit-collaboration.mdx +74 -0
  699. package/docs/content/toolkit-command-navigation.mdx +106 -0
  700. package/docs/content/toolkit-comments-review.mdx +98 -0
  701. package/docs/content/toolkit-history.mdx +95 -0
  702. package/docs/content/toolkit-observability.mdx +41 -0
  703. package/docs/content/toolkit-org-team.mdx +80 -0
  704. package/docs/content/toolkit-resources.mdx +57 -0
  705. package/docs/content/toolkit-settings.mdx +65 -0
  706. package/docs/content/toolkit-setup-connections.mdx +109 -0
  707. package/docs/content/toolkit-sharing-ui.mdx +18 -6
  708. package/docs/content/toolkit-sharing.mdx +81 -0
  709. package/docs/content/toolkit-shell-hooks.mdx +9 -0
  710. package/docs/content/toolkit-ui.mdx +9 -0
  711. package/package.json +36 -2
  712. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
  713. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
  714. package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
@@ -1,4 +1,3 @@
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
1
  /**
3
2
  * MotionDock — bottom motion timeline dock for the Design Studio (§6.3).
4
3
  *
@@ -17,29 +16,48 @@
17
16
  * All times are normalised to [0, 1] internally; the ruler maps them to px.
18
17
  */
19
18
 
19
+ import { useT } from "@agent-native/core/client";
20
+ import {
21
+ MOTION_CURVE_PRESETS,
22
+ MOTION_SPRING_DEFAULT_BOUNCE,
23
+ MOTION_SPRING_PRESETS,
24
+ parseSpringToken,
25
+ sampleSpring,
26
+ springToken,
27
+ } from "@shared/motion-easing";
20
28
  import type {
21
29
  MotionTrack,
22
30
  MotionKeyframe,
23
31
  MotionEase,
32
+ MotionPlaybackMode,
24
33
  MotionPropertyPreset,
25
34
  } from "@shared/motion-timeline";
26
35
  import {
36
+ MOTION_DEFAULT_PLAYBACK_MODE,
27
37
  MOTION_PROPERTY_PRESETS,
28
38
  createMotionTrackFromPreset,
39
+ getMotionTrackTiming,
29
40
  hasTrackFor,
41
+ readTimelinePlaybackMode,
30
42
  sampleMotionKeyframesAt,
31
43
  sortMotionKeyframes,
44
+ timelineTimeToTrackTime,
32
45
  upsertMotionKeyframeAtTime,
46
+ withTimelinePlaybackMode,
33
47
  } from "@shared/motion-timeline";
34
48
  import {
35
49
  IconPlayerPlay,
36
50
  IconPlayerPause,
37
51
  IconPlayerStop,
52
+ IconArrowsLeftRight,
38
53
  IconCheck,
39
54
  IconDiamond,
55
+ IconDiamondFilled,
40
56
  IconChevronDown,
41
57
  IconChevronRight,
42
58
  IconPlus,
59
+ IconRepeat,
60
+ IconRepeatOnce,
43
61
  IconTrash,
44
62
  IconRefresh,
45
63
  IconBolt,
@@ -50,6 +68,7 @@ import {
50
68
  useEffect,
51
69
  useRef,
52
70
  useState,
71
+ type KeyboardEvent as ReactKeyboardEvent,
53
72
  type PointerEvent as ReactPointerEvent,
54
73
  type TransitionEvent as ReactTransitionEvent,
55
74
  } from "react";
@@ -62,9 +81,18 @@ import {
62
81
  DropdownMenuItem,
63
82
  DropdownMenuLabel,
64
83
  DropdownMenuSeparator,
84
+ DropdownMenuSub,
85
+ DropdownMenuSubContent,
86
+ DropdownMenuSubTrigger,
65
87
  DropdownMenuTrigger,
66
88
  } from "@/components/ui/dropdown-menu";
67
89
  import { Input } from "@/components/ui/input";
90
+ import {
91
+ Popover,
92
+ PopoverContent,
93
+ PopoverTrigger,
94
+ } from "@/components/ui/popover";
95
+ import { Slider } from "@/components/ui/slider";
68
96
  import {
69
97
  Tooltip,
70
98
  TooltipContent,
@@ -81,16 +109,57 @@ const PLAYHEAD_WIDTH = 2; // px
81
109
  const MIN_DOCK_HEIGHT = 160; // px
82
110
  const DEFAULT_DOCK_HEIGHT = 280; // px
83
111
 
84
- /** Named easing presets for the keyframe editor. */
85
- const EASE_PRESETS: { label: string; value: MotionEase }[] = [
86
- { label: "Linear", value: "linear" },
87
- { label: "Ease", value: "ease" },
88
- { label: "Ease In", value: "ease-in" },
89
- { label: "Ease Out", value: "ease-out" },
90
- { label: "Ease In/Out", value: "ease-in-out" },
91
- { label: "Spring", value: "cubic-bezier(0.34,1.56,0.64,1)" },
112
+ /**
113
+ * Playback-mode cycling order + chrome, matching Figma Motion's cycling
114
+ * toolbar button (Loop / Once / Ping-pong).
115
+ */
116
+ const PLAYBACK_MODES: {
117
+ mode: MotionPlaybackMode;
118
+ labelKey: "loop" | "once" | "pingPong";
119
+ Icon: typeof IconRepeat;
120
+ }[] = [
121
+ { mode: "loop", labelKey: "loop", Icon: IconRepeat },
122
+ { mode: "once", labelKey: "once", Icon: IconRepeatOnce },
123
+ { mode: "ping-pong", labelKey: "pingPong", Icon: IconArrowsLeftRight },
92
124
  ];
93
125
 
126
+ /**
127
+ * Row-identity key for a track. Uses the unit-separator delimiter (same
128
+ * convention as apply-motion-edit's motionTrackKey) so distinct
129
+ * (nodeId, property) pairs can never collide.
130
+ */
131
+ function trackKey(track: MotionTrack): string {
132
+ return `${track.targetNodeId}\u001f${track.property}`;
133
+ }
134
+
135
+ /** Compact ruler tick label: "250ms" under a second, "1.5s" above. */
136
+ function formatMsTick(ms: number): string {
137
+ if (ms < 1000) return `${ms}ms`;
138
+ const s = (ms / 1000).toFixed(2).replace(/\.?0+$/, "");
139
+ return `${s}s`;
140
+ }
141
+
142
+ /** Human label for an ease value: preset name when recognised, else raw. */
143
+ function easeLabel(
144
+ ease: MotionEase | undefined,
145
+ t: (key: string) => string,
146
+ ): string {
147
+ if (ease === undefined) return t("designEditor.motion.defaultEase");
148
+ const curve = MOTION_CURVE_PRESETS.find((preset) => preset.value === ease);
149
+ if (curve) return curve.label;
150
+ const springPreset = MOTION_SPRING_PRESETS.find(
151
+ (preset) => preset.value === ease,
152
+ );
153
+ if (springPreset) return springPreset.label;
154
+ if (parseSpringToken(String(ease))) {
155
+ return t("designEditor.motion.customSpring");
156
+ }
157
+ if (/^cubic-bezier\(/.test(String(ease))) {
158
+ return t("designEditor.motion.customBezier");
159
+ }
160
+ return String(ease);
161
+ }
162
+
94
163
  // ─── Types ────────────────────────────────────────────────────────────────────
95
164
 
96
165
  export interface MotionDockTrack extends MotionTrack {
@@ -115,6 +184,15 @@ export interface MotionDockProps {
115
184
  onTracksChange?: (tracks: MotionDockTrack[]) => void;
116
185
  /** Called when durationMs is edited. */
117
186
  onDurationChange?: (ms: number) => void;
187
+ /**
188
+ * Controlled playback mode (Loop / Once / Ping-pong). When omitted, the
189
+ * dock reads the mode stamped in `tracks` (timelinePlaybackMode on the
190
+ * first track) and persists changes by re-stamping the tracks through
191
+ * `onTracksChange` — no extra parent wiring needed.
192
+ */
193
+ playbackMode?: MotionPlaybackMode;
194
+ /** Called when the playback-mode cycling button changes the mode. */
195
+ onPlaybackModeChange?: (mode: MotionPlaybackMode) => void;
118
196
  /**
119
197
  * Reference to the canvas iframe element. Used to send preview postMessages.
120
198
  * If not provided, preview messages are skipped (no crash).
@@ -165,6 +243,8 @@ export function MotionDock({
165
243
  onExitComplete,
166
244
  onTracksChange,
167
245
  onDurationChange,
246
+ playbackMode: playbackModeProp,
247
+ onPlaybackModeChange,
168
248
  canvasIframeRef,
169
249
  applying = false,
170
250
  autoKeyframe: autoKeyframeProp,
@@ -174,6 +254,8 @@ export function MotionDock({
174
254
  livePlayheadRef,
175
255
  selectedTarget = null,
176
256
  }: MotionDockProps) {
257
+ const t = useT();
258
+
177
259
  // Controlled / uncontrolled open state.
178
260
  const [openInternal, setOpenInternal] = useState(false);
179
261
  const isOpen = openProp !== undefined ? openProp : openInternal;
@@ -218,6 +300,36 @@ export function MotionDock({
218
300
  },
219
301
  [autoKeyframe, onAutoKeyframeChange],
220
302
  );
303
+
304
+ // Playback mode (Loop / Once / Ping-pong). Controlled by the parent when
305
+ // provided; otherwise read from the stamp persisted in the tracks JSON.
306
+ // Cycling the button re-stamps the tracks through onTracksChange, so the
307
+ // mode persists via the parent's existing autosave without extra wiring.
308
+ const [playbackModeInternal, setPlaybackModeInternal] =
309
+ useState<MotionPlaybackMode | null>(null);
310
+ const playbackMode: MotionPlaybackMode =
311
+ playbackModeProp ??
312
+ readTimelinePlaybackMode(tracks) ??
313
+ playbackModeInternal ??
314
+ MOTION_DEFAULT_PLAYBACK_MODE;
315
+ const cyclePlaybackMode = useCallback(() => {
316
+ const index = PLAYBACK_MODES.findIndex((m) => m.mode === playbackMode);
317
+ const next = PLAYBACK_MODES[(index + 1) % PLAYBACK_MODES.length].mode;
318
+ setPlaybackModeInternal(next);
319
+ onPlaybackModeChange?.(next);
320
+ if (onTracksChange && tracks.length > 0) {
321
+ onTracksChange(withTimelinePlaybackMode(tracks, next));
322
+ }
323
+ }, [onPlaybackModeChange, onTracksChange, playbackMode, tracks]);
324
+
325
+ // Selected property row — the target of the toolbar's add-keyframe ◆
326
+ // button (Figma keys the selected track at the playhead).
327
+ const [selectedTrackKey, setSelectedTrackKey] = useState<string | null>(null);
328
+ const selectedTrack =
329
+ tracks.find((track) => trackKey(track) === selectedTrackKey) ?? null;
330
+
331
+ // Current-time field draft (null = displaying the live playhead).
332
+ const [timeDraft, setTimeDraft] = useState<string | null>(null);
221
333
  /** Internal high-frequency playhead update — does NOT notify the parent. */
222
334
  const setPlayheadLocal = useCallback(
223
335
  (next: number) => {
@@ -309,20 +421,40 @@ export function MotionDock({
309
421
  playStartRef.current = { wallMs: performance.now(), startT };
310
422
  setPlaying(true);
311
423
 
424
+ // Loop and ping-pong run until explicitly paused; once stops at the end
425
+ // (matching the compiled CSS's animation-iteration-count / -direction).
312
426
  const tick = (now: number) => {
313
427
  if (!playStartRef.current) return;
314
428
  const elapsed = now - playStartRef.current.wallMs;
315
- const t = Math.min(1, playStartRef.current.startT + elapsed / durationMs);
429
+ const progressed = playStartRef.current.startT + elapsed / durationMs;
430
+ let t: number;
431
+ let done = false;
432
+ if (playbackMode === "loop") {
433
+ t = progressed % 1;
434
+ } else if (playbackMode === "ping-pong") {
435
+ const phase = progressed % 2;
436
+ t = phase <= 1 ? phase : 2 - phase;
437
+ } else {
438
+ t = Math.min(1, progressed);
439
+ done = progressed >= 1;
440
+ }
316
441
  setPlayheadLocal(t);
317
442
  sendPreview(t);
318
- if (t < 1) {
443
+ if (!done) {
319
444
  playRafRef.current = requestAnimationFrame(tick);
320
445
  } else {
321
446
  stopPlayback();
322
447
  }
323
448
  };
324
449
  playRafRef.current = requestAnimationFrame(tick);
325
- }, [durationMs, playhead, sendPreview, setPlayheadLocal, stopPlayback]);
450
+ }, [
451
+ durationMs,
452
+ playbackMode,
453
+ playhead,
454
+ sendPreview,
455
+ setPlayheadLocal,
456
+ stopPlayback,
457
+ ]);
326
458
 
327
459
  useEffect(() => {
328
460
  return () => {
@@ -414,16 +546,23 @@ export function MotionDock({
414
546
 
415
547
  const addKeyframe = useCallback(
416
548
  (track: MotionDockTrack) => {
417
- // Seed the new keyframe with the track's interpolated value at the
418
- // playhead (what the preview currently shows) a hardcoded "0" is
419
- // invalid CSS for transform/filter/color tracks and snaps the preview.
549
+ // Map the timeline playhead into the track's own span (tracks may be
550
+ // offset/scaled via delayMs/durationMs), then seed the new keyframe
551
+ // with the track's interpolated value at that local time (what the
552
+ // preview currently shows) — a hardcoded "0" is invalid CSS for
553
+ // transform/filter/color tracks and snaps the preview.
554
+ const localT = timelineTimeToTrackTime(
555
+ track,
556
+ playhead * durationMs,
557
+ durationMs,
558
+ );
420
559
  const sampled = sampleMotionKeyframesAt(
421
560
  track.keyframes,
422
- playhead,
561
+ localT,
423
562
  defaultEase,
424
563
  );
425
564
  const newKf: MotionKeyframe = {
426
- t: playhead,
565
+ t: localT,
427
566
  value: sampled || "0",
428
567
  ease: defaultEase,
429
568
  };
@@ -435,7 +574,7 @@ export function MotionDock({
435
574
  keyframes: upsertMotionKeyframeAtTime(tr.keyframes, newKf),
436
575
  }));
437
576
  },
438
- [defaultEase, playhead, updateTrack],
577
+ [defaultEase, durationMs, playhead, updateTrack],
439
578
  );
440
579
 
441
580
  // ── Create a brand-new track (the "first track" path) ──────────────────────
@@ -462,14 +601,24 @@ export function MotionDock({
462
601
  // presets can target the same property, e.g. slide + scale are both
463
602
  // "transform").
464
603
  toast.info(
465
- `A "${preset.property}" track already exists for ${label}. Edit its keyframes in the timeline instead.`,
604
+ t("designEditor.motion.trackExists", {
605
+ property: preset.property,
606
+ label,
607
+ }),
466
608
  );
467
609
  return;
468
610
  }
469
611
 
470
612
  const seeded = createMotionTrackFromPreset(nodeId, preset, defaultEase);
471
613
  const newTrack: MotionDockTrack = { ...seeded, label };
472
- onTracksChange([...tracks, newTrack]);
614
+ // Figma parity: brand-new timelines default to Loop. Existing
615
+ // timelines keep whatever mode is already stamped/resolved.
616
+ const nextTracks =
617
+ tracks.length === 0
618
+ ? withTimelinePlaybackMode([newTrack], "loop")
619
+ : [...tracks, newTrack];
620
+ onTracksChange(nextTracks);
621
+ setSelectedTrackKey(trackKey(newTrack));
473
622
  },
474
623
  [defaultEase, onTracksChange, selectedTarget, tracks],
475
624
  );
@@ -537,15 +686,105 @@ export function MotionDock({
537
686
  [updateTrack],
538
687
  );
539
688
 
540
- // ── Ruler tick marks ──────────────────────────────────────────────────────
689
+ // ── Layer span-bar gestures (drag to offset, edge-drag to scale) ─────────
690
+ // Given the layer's NEW span, remap every track of that layer
691
+ // proportionally from the OLD span so per-track relative offsets survive.
692
+ const updateLayerSpan = useCallback(
693
+ (nodeId: string, nextSpan: { startMs: number; durationMs: number }) => {
694
+ if (!onTracksChange) return;
695
+ const timingsByKey = new Map<
696
+ string,
697
+ { startMs: number; endMs: number; durationMs: number }
698
+ >();
699
+ for (const tr of tracks) {
700
+ if (tr.targetNodeId === nodeId) {
701
+ timingsByKey.set(trackKey(tr), getMotionTrackTiming(tr, durationMs));
702
+ }
703
+ }
704
+ if (timingsByKey.size === 0) return;
705
+ const timings = [...timingsByKey.values()];
706
+ const spanStart = Math.min(...timings.map((t) => t.startMs));
707
+ const spanEnd = Math.max(...timings.map((t) => t.endMs));
708
+ const spanDur = Math.max(1, spanEnd - spanStart);
709
+ const factor = Math.max(0.01, nextSpan.durationMs / spanDur);
710
+ onTracksChange(
711
+ tracks.map((tr) => {
712
+ const timing = timingsByKey.get(trackKey(tr));
713
+ if (!timing) return tr;
714
+ const startMs = Math.max(
715
+ 0,
716
+ Math.round(
717
+ nextSpan.startMs + (timing.startMs - spanStart) * factor,
718
+ ),
719
+ );
720
+ const trackDur = Math.max(20, Math.round(timing.durationMs * factor));
721
+ const out: MotionDockTrack = { ...tr };
722
+ if (startMs > 0) out.delayMs = startMs;
723
+ else delete out.delayMs;
724
+ if (trackDur !== durationMs) out.durationMs = trackDur;
725
+ else delete out.durationMs;
726
+ return out;
727
+ }),
728
+ );
729
+ },
730
+ [durationMs, onTracksChange, tracks],
731
+ );
732
+
733
+ // ── Current-time field ────────────────────────────────────────────────────
734
+ const commitTimeDraft = useCallback(() => {
735
+ if (timeDraft === null) return;
736
+ const ms = parseInt(timeDraft, 10);
737
+ setTimeDraft(null);
738
+ if (isNaN(ms)) return;
739
+ const t = Math.max(0, Math.min(1, ms / durationMs));
740
+ stopPlayback();
741
+ setPlayheadLocal(t);
742
+ commitPlayhead();
743
+ sendPreview(t);
744
+ }, [
745
+ commitPlayhead,
746
+ durationMs,
747
+ sendPreview,
748
+ setPlayheadLocal,
749
+ stopPlayback,
750
+ timeDraft,
751
+ ]);
752
+
753
+ // ── Space plays/pauses while the dock has focus (Figma parity) ───────────
754
+ const handleDockKeyDown = useCallback(
755
+ (e: ReactKeyboardEvent<HTMLDivElement>) => {
756
+ if (e.code !== "Space") return;
757
+ const target = e.target as HTMLElement | null;
758
+ if (target?.closest("input, textarea, select, [contenteditable]")) {
759
+ return;
760
+ }
761
+ e.preventDefault();
762
+ if (playing) stopPlayback();
763
+ else startPlayback();
764
+ },
765
+ [playing, startPlayback, stopPlayback],
766
+ );
767
+
768
+ // ── Ruler tick marks (ms, nice steps — Figma-style ms ruler) ─────────────
541
769
  function rulerTicks(): { t: number; label: string }[] {
542
- const count = 10;
543
- return Array.from({ length: count + 1 }, (_, i) => {
544
- const t = i / count;
545
- const ms = Math.round(t * durationMs);
546
- const s = (ms / 1000).toFixed(1);
547
- return { t, label: `${s}s` };
548
- });
770
+ const NICE_STEPS = [
771
+ 10, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, 10000,
772
+ ];
773
+ // Plain loop (rather than Array#find with an inline arrow function) so a
774
+ // `<=` comparison never sits directly after a `>` character — the i18n
775
+ // raw-literal guard's regex heuristic treats `>...<` runs as JSX text.
776
+ let step = Math.ceil(durationMs / 10);
777
+ for (const candidate of NICE_STEPS) {
778
+ if (durationMs / candidate <= 10) {
779
+ step = candidate;
780
+ break;
781
+ }
782
+ }
783
+ const ticks: { t: number; label: string }[] = [];
784
+ for (let ms = 0; ms <= durationMs; ms += step) {
785
+ ticks.push({ t: ms / durationMs, label: formatMsTick(ms) });
786
+ }
787
+ return ticks;
549
788
  }
550
789
 
551
790
  // ── Group tracks by layer (nodeId) ────────────────────────────────────────
@@ -579,10 +818,14 @@ export function MotionDock({
579
818
  style={{ height: isOpen ? dockHeight : 0 }}
580
819
  >
581
820
  <div
582
- aria-label="Motion dock"
821
+ aria-label={t("designEditor.motion.dockLabel")}
583
822
  aria-hidden={!isOpen ? true : undefined}
823
+ // Focusable so Space can play/pause while the dock is focused
824
+ // (Figma parity); -1 keeps it out of the tab order.
825
+ tabIndex={-1}
826
+ onKeyDown={handleDockKeyDown}
584
827
  className={cn(
585
- "design-motion-dock absolute inset-x-0 bottom-0 z-40 flex min-h-0 transform-gpu flex-col overflow-hidden border-t bg-background select-none",
828
+ "design-motion-dock absolute inset-x-0 bottom-0 z-40 flex min-h-0 transform-gpu flex-col overflow-hidden border-t bg-background outline-none select-none",
586
829
  isOpen
587
830
  ? "translate-y-0 border-border opacity-100"
588
831
  : "translate-y-full border-transparent pointer-events-none",
@@ -605,7 +848,7 @@ export function MotionDock({
605
848
  type="button"
606
849
  className="-ml-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
607
850
  onClick={() => setOpen(false)}
608
- aria-label="Collapse motion dock"
851
+ aria-label={t("designEditor.motion.collapseDock")}
609
852
  >
610
853
  <IconChevronDown className="size-3.5" />
611
854
  </button>
@@ -620,7 +863,11 @@ export function MotionDock({
620
863
  size="icon"
621
864
  className="size-6 shrink-0"
622
865
  onClick={playing ? stopPlayback : startPlayback}
623
- aria-label={playing ? "Pause" : "Play"}
866
+ aria-label={
867
+ playing
868
+ ? t("designEditor.motion.pause")
869
+ : t("designEditor.motion.play")
870
+ }
624
871
  >
625
872
  {playing ? (
626
873
  <IconPlayerPause className="size-3.5" />
@@ -630,7 +877,9 @@ export function MotionDock({
630
877
  </Button>
631
878
  </TooltipTrigger>
632
879
  <TooltipContent side="top">
633
- {playing ? "Pause" : "Play"}
880
+ {playing
881
+ ? t("designEditor.motion.pause")
882
+ : t("designEditor.motion.play")}
634
883
  </TooltipContent>
635
884
  </Tooltip>
636
885
 
@@ -648,18 +897,74 @@ export function MotionDock({
648
897
  commitPlayhead();
649
898
  sendPreview(0);
650
899
  }}
651
- aria-label="Reset playhead"
900
+ aria-label={t("designEditor.motion.resetPlayhead")}
652
901
  >
653
902
  <IconPlayerStop className="size-3.5" />
654
903
  </Button>
655
904
  </TooltipTrigger>
656
- <TooltipContent side="top">Reset</TooltipContent>
905
+ <TooltipContent side="top">
906
+ {t("designEditor.motion.reset")}
907
+ </TooltipContent>
657
908
  </Tooltip>
658
909
 
910
+ {/* Add keyframe at playhead (selected property row) */}
911
+ <Tooltip>
912
+ <TooltipTrigger asChild>
913
+ <span className="inline-flex">
914
+ <Button
915
+ type="button"
916
+ variant="ghost"
917
+ size="icon"
918
+ className="size-6 shrink-0"
919
+ disabled={!selectedTrack}
920
+ onClick={() => {
921
+ if (selectedTrack) addKeyframe(selectedTrack);
922
+ }}
923
+ aria-label={t("designEditor.motion.addKeyframeAtPlayhead")}
924
+ >
925
+ <IconDiamondFilled className="size-3" />
926
+ </Button>
927
+ </span>
928
+ </TooltipTrigger>
929
+ <TooltipContent side="top">
930
+ {selectedTrack
931
+ ? t("designEditor.motion.addKeyframeAtPlayheadForProperty", {
932
+ property: selectedTrack.property,
933
+ })
934
+ : t("designEditor.motion.selectPropertyRowFirst")}
935
+ </TooltipContent>
936
+ </Tooltip>
937
+
938
+ {/* Current time (ms) — click to jump the playhead */}
939
+ <div className="flex items-center gap-1 ml-1">
940
+ <Input
941
+ type="number"
942
+ min={0}
943
+ max={durationMs}
944
+ step={10}
945
+ value={timeDraft ?? String(Math.round(playhead * durationMs))}
946
+ onFocus={() =>
947
+ setTimeDraft(String(Math.round(playhead * durationMs)))
948
+ }
949
+ onChange={(e) => setTimeDraft(e.target.value)}
950
+ onBlur={commitTimeDraft}
951
+ onKeyDown={(e) => {
952
+ if (e.key === "Enter") {
953
+ e.preventDefault();
954
+ commitTimeDraft();
955
+ (e.target as HTMLInputElement).blur();
956
+ }
957
+ }}
958
+ className="h-5 w-14 px-1 !text-[11px] md:!text-[11px]"
959
+ aria-label={t("designEditor.motion.currentTimeMs")}
960
+ />
961
+ <span className="text-[10px] text-muted-foreground">/</span>
962
+ </div>
963
+
659
964
  {/* Duration */}
660
965
  <div className="flex items-center gap-1 ml-1">
661
966
  <span className="text-[10px] text-muted-foreground">
662
- Duration
967
+ {t("designEditor.motion.duration")}
663
968
  </span>
664
969
  <Input
665
970
  type="number"
@@ -676,12 +981,42 @@ export function MotionDock({
676
981
  }
677
982
  }}
678
983
  className="h-5 w-16 px-1 !text-[11px] md:!text-[11px]"
679
- aria-label="Duration in ms"
984
+ aria-label={t("designEditor.motion.durationMs")}
680
985
  />
681
986
  <span className="text-[10px] text-muted-foreground">ms</span>
682
987
  </div>
683
988
 
684
- {/* Add trackthe "create first track" entry point. */}
989
+ {/* Playback modecycling button (Loop / Once / Ping-pong) */}
990
+ {(() => {
991
+ const current =
992
+ PLAYBACK_MODES.find((m) => m.mode === playbackMode) ??
993
+ PLAYBACK_MODES[0];
994
+ const ModeIcon = current.Icon;
995
+ const modeLabel = t(`designEditor.motion.${current.labelKey}`);
996
+ return (
997
+ <Tooltip>
998
+ <TooltipTrigger asChild>
999
+ <Button
1000
+ type="button"
1001
+ variant="ghost"
1002
+ size="icon"
1003
+ className="size-6 shrink-0"
1004
+ onClick={cyclePlaybackMode}
1005
+ aria-label={t("designEditor.motion.playbackMode", {
1006
+ mode: modeLabel,
1007
+ })}
1008
+ >
1009
+ <ModeIcon className="size-3.5" />
1010
+ </Button>
1011
+ </TooltipTrigger>
1012
+ <TooltipContent side="top">
1013
+ {t("designEditor.motion.playback", { mode: modeLabel })}
1014
+ </TooltipContent>
1015
+ </Tooltip>
1016
+ );
1017
+ })()}
1018
+
1019
+ {/* Add motion — the "create first track" entry point. */}
685
1020
  <div className="ml-2">
686
1021
  <AddTrackMenu
687
1022
  selectedTarget={selectedTarget}
@@ -702,13 +1037,15 @@ export function MotionDock({
702
1037
  autoKeyframe && "text-primary",
703
1038
  )}
704
1039
  onClick={() => setAutoKeyframe((v) => !v)}
705
- aria-label="Toggle auto-keyframe"
1040
+ aria-label={t("designEditor.motion.toggleAutoKeyframe")}
706
1041
  aria-pressed={autoKeyframe}
707
1042
  >
708
1043
  <IconBolt className="size-3.5" />
709
1044
  </Button>
710
1045
  </TooltipTrigger>
711
- <TooltipContent side="top">Auto-keyframe</TooltipContent>
1046
+ <TooltipContent side="top">
1047
+ {t("designEditor.motion.autoKeyframe")}
1048
+ </TooltipContent>
712
1049
  </Tooltip>
713
1050
 
714
1051
  {applying ? (
@@ -716,13 +1053,15 @@ export function MotionDock({
716
1053
  <TooltipTrigger asChild>
717
1054
  <span
718
1055
  role="status"
719
- aria-label="Saving motion"
1056
+ aria-label={t("designEditor.motion.savingMotion")}
720
1057
  className="flex size-6 items-center justify-center rounded text-muted-foreground"
721
1058
  >
722
1059
  <IconRefresh className="size-3 animate-spin" />
723
1060
  </span>
724
1061
  </TooltipTrigger>
725
- <TooltipContent side="top">Saving motion</TooltipContent>
1062
+ <TooltipContent side="top">
1063
+ {t("designEditor.motion.savingMotion")}
1064
+ </TooltipContent>
726
1065
  </Tooltip>
727
1066
  ) : null}
728
1067
  </div>
@@ -748,11 +1087,11 @@ export function MotionDock({
748
1087
  {selectedTarget ? (
749
1088
  <>
750
1089
  <p className="!text-[11px] text-muted-foreground/70 leading-snug">
751
- Animate{" "}
1090
+ {t("designEditor.motion.emptyStateAnimate")}{" "}
752
1091
  <span className="font-medium text-foreground/80">
753
1092
  {selectedTarget.label}
754
1093
  </span>
755
- . Pick a property to add the first track.
1094
+ {t("designEditor.motion.emptyStatePickProperty")}
756
1095
  </p>
757
1096
  <AddTrackMenu
758
1097
  selectedTarget={selectedTarget}
@@ -762,8 +1101,7 @@ export function MotionDock({
762
1101
  </>
763
1102
  ) : (
764
1103
  <p className="!text-[11px] text-muted-foreground/70 leading-snug">
765
- Select an element on the canvas, then add a track to animate
766
- it.
1104
+ {t("designEditor.motion.emptyStateNoSelection")}
767
1105
  </p>
768
1106
  )}
769
1107
  </div>
@@ -782,6 +1120,10 @@ export function MotionDock({
782
1120
  })
783
1121
  }
784
1122
  onAddKeyframe={(track) => addKeyframe(track)}
1123
+ selectedTrackKey={selectedTrackKey}
1124
+ onSelectTrack={(track) =>
1125
+ setSelectedTrackKey(trackKey(track))
1126
+ }
785
1127
  />
786
1128
  ))
787
1129
  )}
@@ -824,10 +1166,17 @@ export function MotionDock({
824
1166
  layer={layer}
825
1167
  expanded={expandedNodeIds.has(layer.nodeId)}
826
1168
  playhead={playhead}
1169
+ timelineDurationMs={durationMs}
1170
+ defaultEase={defaultEase}
1171
+ selectedTrackKey={selectedTrackKey}
1172
+ onSelectTrack={(track) =>
1173
+ setSelectedTrackKey(trackKey(track))
1174
+ }
827
1175
  onDeleteKeyframe={deleteKeyframe}
828
1176
  onMoveKeyframe={moveKeyframe}
829
1177
  onMoveKeyframeEnd={moveKeyframeEnd}
830
1178
  onEaseKeyframe={easeKeyframe}
1179
+ onLayerSpanChange={updateLayerSpan}
831
1180
  />
832
1181
  ))}
833
1182
 
@@ -862,16 +1211,21 @@ interface AddTrackMenuProps {
862
1211
  }
863
1212
 
864
1213
  /**
865
- * Property-preset dropdown that creates a new motion track for the selected
866
- * element. Disabled (with a hint) when nothing is selected, which is the only
867
- * state where a first track cannot be created.
1214
+ * "Add motion" dropdown creates a new motion track for the selected
1215
+ * element. Matches Figma Motion's submenu verbatim: Position / Scale /
1216
+ * Rotation / Opacity directly, with the remaining keyframeable properties
1217
+ * under a "More" submenu (Corner radius, Fill, Stroke paint, Stroke weight,
1218
+ * Drop shadow). Disabled (with a hint) when nothing is selected, which is
1219
+ * the only state where a first track cannot be created.
868
1220
  */
869
1221
  function AddTrackMenu({
870
1222
  selectedTarget,
871
1223
  onCreateTrack,
872
1224
  variant = "toolbar",
873
1225
  }: AddTrackMenuProps) {
1226
+ const t = useT();
874
1227
  const disabled = !selectedTarget;
1228
+ const addMotionLabel = t("designEditor.motion.addMotion");
875
1229
  const trigger =
876
1230
  variant === "cta" ? (
877
1231
  <Button
@@ -882,7 +1236,7 @@ function AddTrackMenu({
882
1236
  disabled={disabled}
883
1237
  >
884
1238
  <IconPlus className="size-3.5" />
885
- Add track
1239
+ {addMotionLabel}
886
1240
  </Button>
887
1241
  ) : (
888
1242
  <Button
@@ -893,7 +1247,7 @@ function AddTrackMenu({
893
1247
  disabled={disabled}
894
1248
  >
895
1249
  <IconPlus className="size-3.5" />
896
- Add track
1250
+ {addMotionLabel}
897
1251
  </Button>
898
1252
  );
899
1253
 
@@ -906,21 +1260,30 @@ function AddTrackMenu({
906
1260
  <span className="inline-flex">{trigger}</span>
907
1261
  </TooltipTrigger>
908
1262
  <TooltipContent side="top">
909
- Select an element on the canvas first
1263
+ {t("designEditor.motion.selectElementFirst")}
910
1264
  </TooltipContent>
911
1265
  </Tooltip>
912
1266
  );
913
1267
  }
914
1268
 
1269
+ const primaryPresets = MOTION_PROPERTY_PRESETS.filter(
1270
+ (preset) => preset.group === "primary",
1271
+ );
1272
+ const morePresets = MOTION_PROPERTY_PRESETS.filter(
1273
+ (preset) => preset.group === "more",
1274
+ );
1275
+
915
1276
  return (
916
1277
  <DropdownMenu>
917
1278
  <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
918
1279
  <DropdownMenuContent align="start" className="w-48 p-1">
919
1280
  <DropdownMenuLabel className="truncate px-2 py-1 text-[10px] font-medium leading-none text-muted-foreground">
920
- Animate {selectedTarget.label}”
1281
+ {t("designEditor.motion.animateLayer", {
1282
+ label: selectedTarget.label,
1283
+ })}
921
1284
  </DropdownMenuLabel>
922
1285
  <DropdownMenuSeparator className="my-1" />
923
- {MOTION_PROPERTY_PRESETS.map((preset) => (
1286
+ {primaryPresets.map((preset) => (
924
1287
  <DropdownMenuItem
925
1288
  key={`${preset.property}-${preset.label}`}
926
1289
  className="h-7 px-2 text-[12px] leading-none"
@@ -929,6 +1292,22 @@ function AddTrackMenu({
929
1292
  {preset.label}
930
1293
  </DropdownMenuItem>
931
1294
  ))}
1295
+ <DropdownMenuSub>
1296
+ <DropdownMenuSubTrigger className="h-7 px-2 text-[12px] leading-none">
1297
+ {t("designEditor.motion.more")}
1298
+ </DropdownMenuSubTrigger>
1299
+ <DropdownMenuSubContent className="w-44 p-1">
1300
+ {morePresets.map((preset) => (
1301
+ <DropdownMenuItem
1302
+ key={`${preset.property}-${preset.label}`}
1303
+ className="h-7 px-2 text-[12px] leading-none"
1304
+ onSelect={() => onCreateTrack(preset)}
1305
+ >
1306
+ {preset.label}
1307
+ </DropdownMenuItem>
1308
+ ))}
1309
+ </DropdownMenuSubContent>
1310
+ </DropdownMenuSub>
932
1311
  </DropdownMenuContent>
933
1312
  </DropdownMenu>
934
1313
  );
@@ -966,6 +1345,9 @@ interface LayerGroupProps {
966
1345
  expanded: boolean;
967
1346
  onToggleExpand: () => void;
968
1347
  onAddKeyframe: (track: MotionDockTrack) => void;
1348
+ /** Key of the selected property row (toolbar ◆ target). */
1349
+ selectedTrackKey: string | null;
1350
+ onSelectTrack: (track: MotionDockTrack) => void;
969
1351
  }
970
1352
 
971
1353
  function LayerGroup({
@@ -973,7 +1355,10 @@ function LayerGroup({
973
1355
  expanded,
974
1356
  onToggleExpand,
975
1357
  onAddKeyframe,
1358
+ selectedTrackKey,
1359
+ onSelectTrack,
976
1360
  }: LayerGroupProps) {
1361
+ const t = useT();
977
1362
  return (
978
1363
  <>
979
1364
  {/* Layer header row */}
@@ -1003,13 +1388,23 @@ function LayerGroup({
1003
1388
  </span>
1004
1389
  </div>
1005
1390
 
1006
- {/* Property sub-rows */}
1391
+ {/* Property sub-rows — click to select (toolbar ◆ keys the selection) */}
1007
1392
  {expanded &&
1008
1393
  layer.tracks.map((track) => (
1009
1394
  <div
1010
1395
  key={`${track.targetNodeId}-${track.property}`}
1011
- className="group flex items-center gap-1 pl-5 pr-2 hover:bg-accent/20"
1396
+ className={cn(
1397
+ "group flex items-center gap-1 pl-5 pr-2 cursor-pointer hover:bg-accent/20",
1398
+ trackKey(track) === selectedTrackKey && "bg-accent/40",
1399
+ )}
1012
1400
  style={{ height: ROW_HEIGHT }}
1401
+ role="button"
1402
+ tabIndex={0}
1403
+ onClick={() => onSelectTrack(track)}
1404
+ onKeyDown={(e) => {
1405
+ if (e.key === "Enter") onSelectTrack(track);
1406
+ }}
1407
+ aria-pressed={trackKey(track) === selectedTrackKey}
1013
1408
  >
1014
1409
  <IconDiamond className="size-2.5 shrink-0 text-primary/70" />
1015
1410
  <span className="flex-1 truncate text-[10px] text-muted-foreground">
@@ -1026,13 +1421,13 @@ function LayerGroup({
1026
1421
  e.stopPropagation();
1027
1422
  onAddKeyframe(track);
1028
1423
  }}
1029
- aria-label="Add keyframe"
1424
+ aria-label={t("designEditor.motion.addKeyframe")}
1030
1425
  >
1031
1426
  <IconPlus className="size-3" />
1032
1427
  </Button>
1033
1428
  </TooltipTrigger>
1034
1429
  <TooltipContent side="right">
1035
- Add keyframe at playhead
1430
+ {t("designEditor.motion.addKeyframeAtPlayhead")}
1036
1431
  </TooltipContent>
1037
1432
  </Tooltip>
1038
1433
  </div>
@@ -1045,6 +1440,10 @@ interface LayerTrackRowsProps {
1045
1440
  layer: { nodeId: string; label: string; tracks: MotionDockTrack[] };
1046
1441
  expanded: boolean;
1047
1442
  playhead: number;
1443
+ timelineDurationMs: number;
1444
+ defaultEase: MotionEase;
1445
+ selectedTrackKey: string | null;
1446
+ onSelectTrack: (track: MotionDockTrack) => void;
1048
1447
  onDeleteKeyframe: (track: MotionDockTrack, index: number) => void;
1049
1448
  onMoveKeyframe: (track: MotionDockTrack, index: number, newT: number) => void;
1050
1449
  onMoveKeyframeEnd: (track: MotionDockTrack) => void;
@@ -1053,24 +1452,56 @@ interface LayerTrackRowsProps {
1053
1452
  index: number,
1054
1453
  ease: MotionEase,
1055
1454
  ) => void;
1455
+ /** Layer span-bar gesture commit: drag to offset, edge-drag to scale. */
1456
+ onLayerSpanChange: (
1457
+ nodeId: string,
1458
+ span: { startMs: number; durationMs: number },
1459
+ ) => void;
1056
1460
  }
1057
1461
 
1058
1462
  function LayerTrackRows({
1059
1463
  layer,
1060
1464
  expanded,
1061
1465
  playhead: _playhead,
1466
+ timelineDurationMs,
1467
+ defaultEase,
1468
+ selectedTrackKey,
1469
+ onSelectTrack,
1062
1470
  onDeleteKeyframe,
1063
1471
  onMoveKeyframe,
1064
1472
  onMoveKeyframeEnd,
1065
1473
  onEaseKeyframe,
1474
+ onLayerSpanChange,
1066
1475
  }: LayerTrackRowsProps) {
1476
+ // Layer span = earliest track start … latest track end (Figma's parent
1477
+ // layer bar). Dragging it offsets every track; edge handles scale them.
1478
+ const timings = layer.tracks.map((track) =>
1479
+ getMotionTrackTiming(track, timelineDurationMs),
1480
+ );
1481
+ const spanStartMs =
1482
+ timings.length > 0 ? Math.min(...timings.map((t) => t.startMs)) : 0;
1483
+ const spanEndMs =
1484
+ timings.length > 0
1485
+ ? Math.max(...timings.map((t) => t.endMs))
1486
+ : timelineDurationMs;
1487
+
1067
1488
  return (
1068
1489
  <>
1069
- {/* Layer header spacer row */}
1490
+ {/* Layer header row: the draggable span bar */}
1070
1491
  <div
1071
1492
  className="relative border-b border-border/40"
1072
1493
  style={{ height: ROW_HEIGHT }}
1073
- />
1494
+ >
1495
+ {layer.tracks.length > 0 && (
1496
+ <LayerSpanBar
1497
+ spanStartMs={spanStartMs}
1498
+ spanEndMs={spanEndMs}
1499
+ timelineDurationMs={timelineDurationMs}
1500
+ label={layer.label}
1501
+ onSpanChange={(span) => onLayerSpanChange(layer.nodeId, span)}
1502
+ />
1503
+ )}
1504
+ </div>
1074
1505
 
1075
1506
  {/* Property track rows */}
1076
1507
  {expanded &&
@@ -1078,6 +1509,10 @@ function LayerTrackRows({
1078
1509
  <TrackRow
1079
1510
  key={`${track.targetNodeId}-${track.property}`}
1080
1511
  track={track}
1512
+ timelineDurationMs={timelineDurationMs}
1513
+ defaultEase={defaultEase}
1514
+ selected={trackKey(track) === selectedTrackKey}
1515
+ onSelect={() => onSelectTrack(track)}
1081
1516
  onDeleteKeyframe={(index) => onDeleteKeyframe(track, index)}
1082
1517
  onMoveKeyframe={(index, newT) => onMoveKeyframe(track, index, newT)}
1083
1518
  onMoveKeyframeEnd={() => onMoveKeyframeEnd(track)}
@@ -1088,8 +1523,122 @@ function LayerTrackRows({
1088
1523
  );
1089
1524
  }
1090
1525
 
1526
+ // ─── Layer span bar (drag = offset, edge-drag = scale) ────────────────────────
1527
+
1528
+ interface LayerSpanBarProps {
1529
+ spanStartMs: number;
1530
+ spanEndMs: number;
1531
+ timelineDurationMs: number;
1532
+ label: string;
1533
+ onSpanChange: (span: { startMs: number; durationMs: number }) => void;
1534
+ }
1535
+
1536
+ function LayerSpanBar({
1537
+ spanStartMs,
1538
+ spanEndMs,
1539
+ timelineDurationMs,
1540
+ label,
1541
+ onSpanChange,
1542
+ }: LayerSpanBarProps) {
1543
+ const t = useT();
1544
+ const barRef = useRef<HTMLDivElement>(null);
1545
+ const gesture = useRef<{
1546
+ kind: "move" | "left" | "right";
1547
+ startX: number;
1548
+ startMs: number;
1549
+ durMs: number;
1550
+ pxPerMs: number;
1551
+ } | null>(null);
1552
+
1553
+ const beginGesture = useCallback(
1554
+ (e: ReactPointerEvent<HTMLElement>, kind: "move" | "left" | "right") => {
1555
+ const row = barRef.current?.parentElement;
1556
+ if (!row) return;
1557
+ e.stopPropagation();
1558
+ const rect = row.getBoundingClientRect();
1559
+ if (rect.width <= 0) return;
1560
+ gesture.current = {
1561
+ kind,
1562
+ startX: e.clientX,
1563
+ startMs: spanStartMs,
1564
+ durMs: Math.max(1, spanEndMs - spanStartMs),
1565
+ pxPerMs: rect.width / timelineDurationMs,
1566
+ };
1567
+ (e.target as Element).setPointerCapture(e.pointerId);
1568
+ },
1569
+ [spanEndMs, spanStartMs, timelineDurationMs],
1570
+ );
1571
+
1572
+ const handlePointerMove = useCallback(
1573
+ (e: ReactPointerEvent<HTMLElement>) => {
1574
+ const g = gesture.current;
1575
+ if (!g) return;
1576
+ const deltaMs = (e.clientX - g.startX) / g.pxPerMs;
1577
+ if (g.kind === "move") {
1578
+ const maxStart = Math.max(0, timelineDurationMs - g.durMs);
1579
+ const startMs = Math.min(Math.max(0, g.startMs + deltaMs), maxStart);
1580
+ onSpanChange({ startMs, durationMs: g.durMs });
1581
+ } else if (g.kind === "right") {
1582
+ const durationMs = Math.max(
1583
+ 50,
1584
+ Math.min(g.durMs + deltaMs, timelineDurationMs - g.startMs),
1585
+ );
1586
+ onSpanChange({ startMs: g.startMs, durationMs });
1587
+ } else {
1588
+ const endMs = g.startMs + g.durMs;
1589
+ const startMs = Math.max(0, Math.min(g.startMs + deltaMs, endMs - 50));
1590
+ onSpanChange({ startMs, durationMs: endMs - startMs });
1591
+ }
1592
+ },
1593
+ [onSpanChange, timelineDurationMs],
1594
+ );
1595
+
1596
+ const endGesture = useCallback(() => {
1597
+ gesture.current = null;
1598
+ }, []);
1599
+
1600
+ const leftPct = (spanStartMs / timelineDurationMs) * 100;
1601
+ const widthPct = Math.max(
1602
+ 0.5,
1603
+ ((spanEndMs - spanStartMs) / timelineDurationMs) * 100,
1604
+ );
1605
+
1606
+ return (
1607
+ <div
1608
+ ref={barRef}
1609
+ className="group absolute top-1/2 h-4 -translate-y-1/2 cursor-grab active:cursor-grabbing rounded-sm bg-primary/20 hover:bg-primary/30 border border-primary/40"
1610
+ style={{ left: `${leftPct}%`, width: `${widthPct}%` }}
1611
+ onPointerDown={(e) => beginGesture(e, "move")}
1612
+ onPointerMove={handlePointerMove}
1613
+ onPointerUp={endGesture}
1614
+ onPointerCancel={endGesture}
1615
+ role="slider"
1616
+ aria-label={t("designEditor.motion.layerAnimationSpan", { label })}
1617
+ aria-valuemin={0}
1618
+ aria-valuemax={timelineDurationMs}
1619
+ aria-valuenow={Math.round(spanStartMs)}
1620
+ aria-valuetext={`${Math.round(spanStartMs)}ms – ${Math.round(spanEndMs)}ms`}
1621
+ tabIndex={-1}
1622
+ >
1623
+ {/* Edge handles: scale the layer's animations */}
1624
+ <div
1625
+ className="absolute left-0 top-0 h-full w-1.5 cursor-ew-resize rounded-l-sm bg-primary/50 opacity-0 group-hover:opacity-100"
1626
+ onPointerDown={(e) => beginGesture(e, "left")}
1627
+ />
1628
+ <div
1629
+ className="absolute right-0 top-0 h-full w-1.5 cursor-ew-resize rounded-r-sm bg-primary/50 opacity-0 group-hover:opacity-100"
1630
+ onPointerDown={(e) => beginGesture(e, "right")}
1631
+ />
1632
+ </div>
1633
+ );
1634
+ }
1635
+
1091
1636
  interface TrackRowProps {
1092
1637
  track: MotionDockTrack;
1638
+ timelineDurationMs: number;
1639
+ defaultEase: MotionEase;
1640
+ selected: boolean;
1641
+ onSelect: () => void;
1093
1642
  onDeleteKeyframe: (index: number) => void;
1094
1643
  onMoveKeyframe: (index: number, newT: number) => void;
1095
1644
  onMoveKeyframeEnd: () => void;
@@ -1098,12 +1647,23 @@ interface TrackRowProps {
1098
1647
 
1099
1648
  function TrackRow({
1100
1649
  track,
1650
+ timelineDurationMs,
1651
+ defaultEase,
1652
+ selected,
1653
+ onSelect,
1101
1654
  onDeleteKeyframe,
1102
1655
  onMoveKeyframe,
1103
1656
  onMoveKeyframeEnd,
1104
1657
  onEaseKeyframe,
1105
1658
  }: TrackRowProps) {
1106
1659
  const rowRef = useRef<HTMLDivElement>(null);
1660
+ // The track's own span within the timeline (delayMs/durationMs offsets).
1661
+ // Keyframe t is normalised to THIS span; positions on the ruler are
1662
+ // (startMs + t * span) / timelineDurationMs.
1663
+ const timing = getMotionTrackTiming(track, timelineDurationMs);
1664
+ const fractionFor = (kfT: number) =>
1665
+ (timing.startMs + kfT * timing.durationMs) / timelineDurationMs;
1666
+
1107
1667
  // Identify the dragged keyframe by its INDEX in track.keyframes, not object
1108
1668
  // identity: the parent replaces keyframe objects on every move (immutable
1109
1669
  // update), so a captured object reference goes stale after the first move
@@ -1122,6 +1682,7 @@ function TrackRow({
1122
1682
  kf: MotionKeyframe,
1123
1683
  ) => {
1124
1684
  e.stopPropagation();
1685
+ onSelect();
1125
1686
  dragging.current = {
1126
1687
  index,
1127
1688
  startX: e.clientX,
@@ -1130,7 +1691,7 @@ function TrackRow({
1130
1691
  };
1131
1692
  (e.target as Element).setPointerCapture(e.pointerId);
1132
1693
  },
1133
- [],
1694
+ [onSelect],
1134
1695
  );
1135
1696
 
1136
1697
  const handlePointerMove = useCallback(
@@ -1138,12 +1699,13 @@ function TrackRow({
1138
1699
  if (!dragging.current || !rowRef.current) return;
1139
1700
  const rect = rowRef.current.getBoundingClientRect();
1140
1701
  const dx = e.clientX - dragging.current.startX;
1141
- const dt = dx / rect.width;
1702
+ // Pixel delta timeline fraction → track-local time delta.
1703
+ const dt = (dx / rect.width) * (timelineDurationMs / timing.durationMs);
1142
1704
  const newT = Math.max(0, Math.min(1, dragging.current.startT + dt));
1143
1705
  dragging.current.moved = true;
1144
1706
  onMoveKeyframe(dragging.current.index, newT);
1145
1707
  },
1146
- [onMoveKeyframe],
1708
+ [onMoveKeyframe, timelineDurationMs, timing.durationMs],
1147
1709
  );
1148
1710
 
1149
1711
  const handlePointerUp = useCallback(() => {
@@ -1154,11 +1716,22 @@ function TrackRow({
1154
1716
  if (wasMoved) onMoveKeyframeEnd();
1155
1717
  }, [onMoveKeyframeEnd]);
1156
1718
 
1719
+ // Segments connect consecutive keyframes; clicking one opens the easing
1720
+ // panel for the transition (stored on the LEAVING keyframe, CSS-style).
1721
+ const orderedIndices = track.keyframes
1722
+ .map((kf, index) => ({ t: kf.t, index }))
1723
+ .sort((a, b) => a.t - b.t)
1724
+ .map((entry) => entry.index);
1725
+
1157
1726
  return (
1158
1727
  <div
1159
1728
  ref={rowRef}
1160
- className="relative border-b border-border/30"
1729
+ className={cn(
1730
+ "relative border-b border-border/30",
1731
+ selected && "bg-accent/20",
1732
+ )}
1161
1733
  style={{ height: ROW_HEIGHT }}
1734
+ onClick={onSelect}
1162
1735
  onPointerMove={handlePointerMove}
1163
1736
  onPointerUp={handlePointerUp}
1164
1737
  onPointerCancel={handlePointerUp}
@@ -1166,14 +1739,34 @@ function TrackRow({
1166
1739
  {/* Track baseline */}
1167
1740
  <div className="absolute inset-y-1/2 left-0 right-0 h-px bg-border/50" />
1168
1741
 
1742
+ {/* Easing segments between consecutive keyframes */}
1743
+ {orderedIndices.slice(0, -1).map((kfIndex, order) => {
1744
+ const from = track.keyframes[kfIndex];
1745
+ const to = track.keyframes[orderedIndices[order + 1]];
1746
+ if (!from || !to) return null;
1747
+ const left = fractionFor(from.t);
1748
+ const width = Math.max(0, fractionFor(to.t) - left);
1749
+ return (
1750
+ <EasingSegment
1751
+ key={`segment-${kfIndex}`}
1752
+ left={left}
1753
+ width={width}
1754
+ ease={from.ease}
1755
+ defaultEase={defaultEase}
1756
+ onEaseChange={(ease) => onEaseKeyframe(kfIndex, ease)}
1757
+ />
1758
+ );
1759
+ })}
1760
+
1169
1761
  {/* Keyframe diamonds */}
1170
1762
  {track.keyframes.map((kf, i) => (
1171
1763
  <KeyframeDiamond
1172
1764
  key={i}
1173
1765
  kf={kf}
1766
+ leftFraction={fractionFor(kf.t)}
1767
+ timeMs={Math.round(timing.startMs + kf.t * timing.durationMs)}
1174
1768
  onPointerDown={(e) => handleDiamondPointerDown(e, i, kf)}
1175
1769
  onDelete={() => onDeleteKeyframe(i)}
1176
- onEaseChange={(ease) => onEaseKeyframe(i, ease)}
1177
1770
  />
1178
1771
  ))}
1179
1772
  </div>
@@ -1182,27 +1775,32 @@ function TrackRow({
1182
1775
 
1183
1776
  interface KeyframeDiamondProps {
1184
1777
  kf: MotionKeyframe;
1778
+ /** Horizontal position as a fraction of the whole timeline (offset-aware). */
1779
+ leftFraction: number;
1780
+ /** Absolute keyframe time on the timeline, ms (for the tooltip). */
1781
+ timeMs: number;
1185
1782
  onPointerDown: (e: ReactPointerEvent<SVGSVGElement>) => void;
1186
1783
  onDelete: () => void;
1187
- onEaseChange: (ease: MotionEase) => void;
1188
1784
  }
1189
1785
 
1786
+ /**
1787
+ * Draggable keyframe diamond. Easing is edited on the SEGMENT between two
1788
+ * diamonds (click the connecting bar), matching Figma Motion.
1789
+ */
1190
1790
  function KeyframeDiamond({
1191
1791
  kf,
1792
+ leftFraction,
1793
+ timeMs,
1192
1794
  onPointerDown,
1193
1795
  onDelete,
1194
- onEaseChange,
1195
1796
  }: KeyframeDiamondProps) {
1797
+ const t = useT();
1196
1798
  const [hovered, setHovered] = useState(false);
1197
- const currentEase = kf.ease ?? "ease";
1198
- const currentPreset = EASE_PRESETS.find(
1199
- (preset) => preset.value === currentEase,
1200
- );
1201
1799
 
1202
1800
  return (
1203
1801
  <div
1204
1802
  className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 group"
1205
- style={{ left: `${kf.t * 100}%` }}
1803
+ style={{ left: `${leftFraction * 100}%` }}
1206
1804
  >
1207
1805
  <Tooltip>
1208
1806
  <TooltipTrigger asChild>
@@ -1214,7 +1812,7 @@ function KeyframeDiamond({
1214
1812
  onPointerDown={onPointerDown}
1215
1813
  onPointerEnter={() => setHovered(true)}
1216
1814
  onPointerLeave={() => setHovered(false)}
1217
- aria-label={`Keyframe at ${Math.round(kf.t * 100)}%`}
1815
+ aria-label={t("designEditor.motion.keyframeAt", { ms: timeMs })}
1218
1816
  >
1219
1817
  <rect
1220
1818
  x={1}
@@ -1238,53 +1836,476 @@ function KeyframeDiamond({
1238
1836
  className="flex items-center gap-1 text-[10px]"
1239
1837
  >
1240
1838
  <span>
1241
- {Math.round(kf.t * 100)}% — {kf.value} —{" "}
1242
- {currentPreset?.label ?? currentEase}
1839
+ {timeMs}ms — {kf.value}
1243
1840
  </span>
1244
1841
  <button
1245
1842
  type="button"
1246
1843
  className="ml-1 text-destructive hover:text-destructive/80"
1247
1844
  onClick={onDelete}
1248
- aria-label="Delete keyframe"
1845
+ aria-label={t("designEditor.motion.deleteKeyframe")}
1249
1846
  >
1250
1847
  <IconTrash className="size-3" />
1251
1848
  </button>
1252
1849
  </TooltipContent>
1253
1850
  </Tooltip>
1851
+ </div>
1852
+ );
1853
+ }
1854
+
1855
+ // ─── Segment easing (click the connector between two keyframes) ──────────────
1856
+
1857
+ interface EasingSegmentProps {
1858
+ /** Left edge as a fraction of the timeline. */
1859
+ left: number;
1860
+ /** Width as a fraction of the timeline. */
1861
+ width: number;
1862
+ /** The LEAVING keyframe's ease (undefined = timeline default). */
1863
+ ease: MotionEase | undefined;
1864
+ defaultEase: MotionEase;
1865
+ onEaseChange: (ease: MotionEase) => void;
1866
+ }
1867
+
1868
+ /**
1869
+ * The clickable bar between two keyframe diamonds. Opens the easing panel
1870
+ * (Curve / Spring tabs) for the transition into the next keyframe — matching
1871
+ * Figma Motion's "click the connecting line" flow.
1872
+ */
1873
+ function EasingSegment({
1874
+ left,
1875
+ width,
1876
+ ease,
1877
+ defaultEase,
1878
+ onEaseChange,
1879
+ }: EasingSegmentProps) {
1880
+ const t = useT();
1881
+ const effective = ease ?? defaultEase;
1882
+ const effectiveLabel = easeLabel(effective, t);
1883
+ return (
1884
+ <Popover>
1885
+ <PopoverTrigger asChild>
1886
+ <button
1887
+ type="button"
1888
+ className="absolute top-1/2 h-1.5 -translate-y-1/2 cursor-pointer rounded-full bg-primary/25 hover:bg-primary/50 data-[state=open]:bg-primary/60"
1889
+ style={{
1890
+ left: `${left * 100}%`,
1891
+ width: `${width * 100}%`,
1892
+ minWidth: 8,
1893
+ }}
1894
+ onPointerDown={(e) => e.stopPropagation()}
1895
+ aria-label={t("designEditor.motion.segmentEasing", {
1896
+ ease: effectiveLabel,
1897
+ })}
1898
+ title={effectiveLabel}
1899
+ />
1900
+ </PopoverTrigger>
1901
+ <PopoverContent
1902
+ side="top"
1903
+ align="center"
1904
+ className="w-60 p-2"
1905
+ onPointerDown={(e) => e.stopPropagation()}
1906
+ >
1907
+ <EasingPanel ease={effective} onChange={onEaseChange} />
1908
+ </PopoverContent>
1909
+ </Popover>
1910
+ );
1911
+ }
1912
+
1913
+ // ─── Easing panel (Curve / Spring tabs) ───────────────────────────────────────
1914
+
1915
+ interface EasingPanelProps {
1916
+ ease: MotionEase;
1917
+ onChange: (ease: MotionEase) => void;
1918
+ }
1919
+
1920
+ /**
1921
+ * Figma-Motion-parity easing editor:
1922
+ * - Curve tab: Hold, Linear, Ease in/out variants, back curves, and Custom
1923
+ * bezier with editable x1,y1,x2,y2 + a draggable curve editor.
1924
+ * - Spring tab: Gentle / Quick / Bouncy / Slow presets and Custom spring
1925
+ * with a single Bounce control (0–1, default 0.25).
1926
+ */
1927
+ function EasingPanel({ ease, onChange }: EasingPanelProps) {
1928
+ const t = useT();
1929
+ const easeStr = String(ease);
1930
+ const spring = parseSpringToken(easeStr);
1931
+ const [tab, setTab] = useState<"curve" | "spring">(
1932
+ spring ? "spring" : "curve",
1933
+ );
1934
+
1935
+ const isCurvePreset = MOTION_CURVE_PRESETS.some(
1936
+ (preset) => preset.value === easeStr,
1937
+ );
1938
+ const bezierMatch = /^cubic-bezier\(([^)]+)\)$/.exec(easeStr);
1939
+ const bezierPoints: [number, number, number, number] = (() => {
1940
+ if (bezierMatch) {
1941
+ const parts = bezierMatch[1].split(",").map((part) => parseFloat(part));
1942
+ if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
1943
+ return [parts[0], parts[1], parts[2], parts[3]];
1944
+ }
1945
+ }
1946
+ return [0.42, 0, 0.58, 1];
1947
+ })();
1948
+ const isCustomBezier = bezierMatch !== null && !isCurvePreset;
1949
+
1950
+ const springPreset = MOTION_SPRING_PRESETS.find(
1951
+ (preset) => preset.value === easeStr,
1952
+ );
1953
+ const isCustomSpring = spring !== null && !springPreset;
1954
+ const bounce = spring?.bounce ?? MOTION_SPRING_DEFAULT_BOUNCE;
1955
+ const settle = spring?.settle ?? 1;
1956
+
1957
+ const itemClass = (active: boolean) =>
1958
+ cn(
1959
+ "flex h-6 w-full cursor-pointer items-center gap-1 rounded px-2 text-left text-[11px] leading-none hover:bg-accent",
1960
+ active && "font-medium",
1961
+ );
1962
+ const checkClass = (active: boolean) =>
1963
+ cn("size-3 shrink-0", active ? "opacity-100" : "opacity-0");
1254
1964
 
1255
- {/* Easing picker — appears under the diamond on hover. */}
1256
- <DropdownMenu>
1257
- <DropdownMenuTrigger asChild>
1965
+ return (
1966
+ <div className="flex flex-col gap-2">
1967
+ {/* Pill tab switch: Curve | Spring */}
1968
+ <div className="flex gap-0.5 rounded-md bg-muted p-0.5">
1969
+ {(
1970
+ [
1971
+ ["curve", t("designEditor.motion.curveTab")],
1972
+ ["spring", t("designEditor.motion.springTab")],
1973
+ ] as const
1974
+ ).map(([key, label]) => (
1258
1975
  <button
1976
+ key={key}
1259
1977
  type="button"
1260
- className="absolute left-1/2 top-full flex size-3 -translate-x-1/2 cursor-pointer items-center justify-center rounded-sm text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100 data-[state=open]:opacity-100"
1261
- aria-label={`Change easing (currently ${currentPreset?.label ?? currentEase})`}
1978
+ className={cn(
1979
+ "flex-1 cursor-pointer rounded px-2 py-1 text-[11px] leading-none",
1980
+ tab === key
1981
+ ? "bg-background font-medium shadow-sm"
1982
+ : "text-muted-foreground hover:text-foreground",
1983
+ )}
1984
+ onClick={() => setTab(key)}
1985
+ aria-pressed={tab === key}
1262
1986
  >
1263
- <IconChevronDown className="size-2.5" />
1987
+ {label}
1264
1988
  </button>
1265
- </DropdownMenuTrigger>
1266
- <DropdownMenuContent align="start" className="w-40 p-1">
1267
- <DropdownMenuLabel className="px-2 py-1 text-[10px] font-medium leading-none text-muted-foreground">
1268
- Easing
1269
- </DropdownMenuLabel>
1270
- <DropdownMenuSeparator className="my-1" />
1271
- {EASE_PRESETS.map((preset) => (
1272
- <DropdownMenuItem
1273
- key={preset.value}
1274
- className="h-7 px-2 text-[12px] leading-none"
1275
- onSelect={() => onEaseChange(preset.value)}
1989
+ ))}
1990
+ </div>
1991
+
1992
+ {tab === "curve" ? (
1993
+ <div className="flex flex-col gap-2">
1994
+ <div className="flex flex-col">
1995
+ {MOTION_CURVE_PRESETS.map((preset) => (
1996
+ <button
1997
+ key={preset.label}
1998
+ type="button"
1999
+ className={itemClass(preset.value === easeStr)}
2000
+ onClick={() => onChange(preset.value)}
2001
+ >
2002
+ <IconCheck className={checkClass(preset.value === easeStr)} />
2003
+ {preset.label}
2004
+ </button>
2005
+ ))}
2006
+ <button
2007
+ type="button"
2008
+ className={itemClass(isCustomBezier)}
2009
+ onClick={() =>
2010
+ onChange(
2011
+ `cubic-bezier(${bezierPoints
2012
+ .map((n) => Math.round(n * 100) / 100)
2013
+ .join(", ")})`,
2014
+ )
2015
+ }
1276
2016
  >
1277
- <IconCheck
1278
- className={cn(
1279
- "mr-1 size-3",
1280
- preset.value === currentEase ? "opacity-100" : "opacity-0",
1281
- )}
2017
+ <IconCheck className={checkClass(isCustomBezier)} />
2018
+ {t("designEditor.motion.customBezier")}
2019
+ </button>
2020
+ </div>
2021
+ {isCustomBezier && (
2022
+ <CurveEditor
2023
+ value={bezierPoints}
2024
+ onChange={(points) =>
2025
+ onChange(
2026
+ `cubic-bezier(${points
2027
+ .map((n) => Math.round(n * 100) / 100)
2028
+ .join(", ")})`,
2029
+ )
2030
+ }
2031
+ />
2032
+ )}
2033
+ </div>
2034
+ ) : (
2035
+ <div className="flex flex-col gap-2">
2036
+ <div className="flex flex-col">
2037
+ {MOTION_SPRING_PRESETS.map((preset) => (
2038
+ <button
2039
+ key={preset.label}
2040
+ type="button"
2041
+ className={itemClass(preset.value === easeStr)}
2042
+ onClick={() => onChange(preset.value)}
2043
+ >
2044
+ <IconCheck className={checkClass(preset.value === easeStr)} />
2045
+ {preset.label}
2046
+ </button>
2047
+ ))}
2048
+ <button
2049
+ type="button"
2050
+ className={itemClass(isCustomSpring)}
2051
+ onClick={() =>
2052
+ onChange(
2053
+ springToken({
2054
+ bounce: MOTION_SPRING_DEFAULT_BOUNCE,
2055
+ settle: 1,
2056
+ }),
2057
+ )
2058
+ }
2059
+ >
2060
+ <IconCheck className={checkClass(isCustomSpring)} />
2061
+ {t("designEditor.motion.customSpring")}
2062
+ </button>
2063
+ </div>
2064
+ {isCustomSpring && (
2065
+ <div className="flex flex-col gap-1 px-1">
2066
+ <div className="flex items-center justify-between">
2067
+ <span className="text-[10px] text-muted-foreground">
2068
+ {t("designEditor.motion.bounce")}
2069
+ </span>
2070
+ <span className="text-[10px] tabular-nums">
2071
+ {bounce.toFixed(2)}
2072
+ </span>
2073
+ </div>
2074
+ <Slider
2075
+ value={[bounce]}
2076
+ min={0}
2077
+ max={1}
2078
+ step={0.01}
2079
+ onValueChange={(values) =>
2080
+ onChange(springToken({ bounce: values[0] ?? bounce, settle }))
2081
+ }
2082
+ aria-label={t("designEditor.motion.bounce")}
1282
2083
  />
1283
- {preset.label}
1284
- </DropdownMenuItem>
1285
- ))}
1286
- </DropdownMenuContent>
1287
- </DropdownMenu>
2084
+ </div>
2085
+ )}
2086
+ {spring !== null && (
2087
+ <SpringCurvePreview bounce={bounce} settle={settle} />
2088
+ )}
2089
+ </div>
2090
+ )}
1288
2091
  </div>
1289
2092
  );
1290
2093
  }
2094
+
2095
+ // ─── Custom bezier curve editor (draggable control points) ────────────────────
2096
+
2097
+ const CURVE_W = 216;
2098
+ const CURVE_H = 132;
2099
+ const CURVE_PAD = 10;
2100
+ // Vertical view range: y ∈ [-0.25, 1.25] so overshoot handles stay visible.
2101
+ const CURVE_Y_MIN = -0.25;
2102
+ const CURVE_Y_MAX = 1.25;
2103
+
2104
+ interface CurveEditorProps {
2105
+ value: [number, number, number, number];
2106
+ onChange: (value: [number, number, number, number]) => void;
2107
+ }
2108
+
2109
+ function CurveEditor({ value, onChange }: CurveEditorProps) {
2110
+ const t = useT();
2111
+ const [x1, y1, x2, y2] = value;
2112
+ const svgRef = useRef<SVGSVGElement>(null);
2113
+ const draggingHandle = useRef<1 | 2 | null>(null);
2114
+
2115
+ const xPx = (x: number) => CURVE_PAD + x * (CURVE_W - 2 * CURVE_PAD);
2116
+ const yPx = (y: number) =>
2117
+ CURVE_PAD +
2118
+ ((CURVE_Y_MAX - y) / (CURVE_Y_MAX - CURVE_Y_MIN)) *
2119
+ (CURVE_H - 2 * CURVE_PAD);
2120
+
2121
+ const fromEvent = (e: ReactPointerEvent<SVGSVGElement>) => {
2122
+ const rect = svgRef.current?.getBoundingClientRect();
2123
+ if (!rect) return null;
2124
+ const px = ((e.clientX - rect.left) / rect.width) * CURVE_W;
2125
+ const py = ((e.clientY - rect.top) / rect.height) * CURVE_H;
2126
+ // CSS requires x control points in [0, 1]; y may overshoot.
2127
+ const x = Math.max(
2128
+ 0,
2129
+ Math.min(1, (px - CURVE_PAD) / (CURVE_W - 2 * CURVE_PAD)),
2130
+ );
2131
+ const y =
2132
+ CURVE_Y_MAX -
2133
+ ((py - CURVE_PAD) / (CURVE_H - 2 * CURVE_PAD)) *
2134
+ (CURVE_Y_MAX - CURVE_Y_MIN);
2135
+ return [x, Math.max(-1, Math.min(2, y))] as const;
2136
+ };
2137
+
2138
+ const handleMove = (e: ReactPointerEvent<SVGSVGElement>) => {
2139
+ if (!draggingHandle.current) return;
2140
+ const point = fromEvent(e);
2141
+ if (!point) return;
2142
+ onChange(
2143
+ draggingHandle.current === 1
2144
+ ? [point[0], point[1], x2, y2]
2145
+ : [x1, y1, point[0], point[1]],
2146
+ );
2147
+ };
2148
+
2149
+ const numberField = (
2150
+ label: string,
2151
+ fieldValue: number,
2152
+ apply: (n: number) => [number, number, number, number],
2153
+ ) => (
2154
+ <label className="flex flex-col items-stretch gap-0.5">
2155
+ <span className="text-center text-[9px] text-muted-foreground">
2156
+ {label}
2157
+ </span>
2158
+ <Input
2159
+ type="number"
2160
+ step={0.01}
2161
+ value={String(Math.round(fieldValue * 100) / 100)}
2162
+ onChange={(e) => {
2163
+ const n = parseFloat(e.target.value);
2164
+ if (Number.isFinite(n)) onChange(apply(n));
2165
+ }}
2166
+ className="h-5 px-1 !text-[10px] md:!text-[10px]"
2167
+ aria-label={label}
2168
+ />
2169
+ </label>
2170
+ );
2171
+
2172
+ return (
2173
+ <div className="flex flex-col gap-1">
2174
+ <svg
2175
+ ref={svgRef}
2176
+ width="100%"
2177
+ viewBox={`0 0 ${CURVE_W} ${CURVE_H}`}
2178
+ className="rounded border border-border bg-muted/40 touch-none"
2179
+ onPointerMove={handleMove}
2180
+ onPointerUp={() => (draggingHandle.current = null)}
2181
+ onPointerCancel={() => (draggingHandle.current = null)}
2182
+ role="application"
2183
+ aria-label={t("designEditor.motion.bezierCurveEditor")}
2184
+ >
2185
+ {/* Unit box (0..1) */}
2186
+ <rect
2187
+ x={xPx(0)}
2188
+ y={yPx(1)}
2189
+ width={xPx(1) - xPx(0)}
2190
+ height={yPx(0) - yPx(1)}
2191
+ className="fill-transparent stroke-border"
2192
+ strokeDasharray="3 3"
2193
+ />
2194
+ {/* Handle arms */}
2195
+ <line
2196
+ x1={xPx(0)}
2197
+ y1={yPx(0)}
2198
+ x2={xPx(Math.max(0, Math.min(1, x1)))}
2199
+ y2={yPx(y1)}
2200
+ className="stroke-primary/50"
2201
+ />
2202
+ <line
2203
+ x1={xPx(1)}
2204
+ y1={yPx(1)}
2205
+ x2={xPx(Math.max(0, Math.min(1, x2)))}
2206
+ y2={yPx(y2)}
2207
+ className="stroke-primary/50"
2208
+ />
2209
+ {/* The curve itself */}
2210
+ <path
2211
+ d={`M ${xPx(0)} ${yPx(0)} C ${xPx(Math.max(0, Math.min(1, x1)))} ${yPx(y1)}, ${xPx(Math.max(0, Math.min(1, x2)))} ${yPx(y2)}, ${xPx(1)} ${yPx(1)}`}
2212
+ className="fill-none stroke-primary"
2213
+ strokeWidth={1.5}
2214
+ />
2215
+ {/* Draggable control points */}
2216
+ <circle
2217
+ cx={xPx(Math.max(0, Math.min(1, x1)))}
2218
+ cy={yPx(y1)}
2219
+ r={5}
2220
+ className="cursor-grab fill-primary stroke-background active:cursor-grabbing"
2221
+ onPointerDown={(e) => {
2222
+ draggingHandle.current = 1;
2223
+ (e.target as Element).setPointerCapture(e.pointerId);
2224
+ }}
2225
+ />
2226
+ <circle
2227
+ cx={xPx(Math.max(0, Math.min(1, x2)))}
2228
+ cy={yPx(y2)}
2229
+ r={5}
2230
+ className="cursor-grab fill-primary stroke-background active:cursor-grabbing"
2231
+ onPointerDown={(e) => {
2232
+ draggingHandle.current = 2;
2233
+ (e.target as Element).setPointerCapture(e.pointerId);
2234
+ }}
2235
+ />
2236
+ </svg>
2237
+ <div className="grid grid-cols-4 gap-1">
2238
+ {numberField("x1", x1, (n) => [
2239
+ Math.max(0, Math.min(1, n)),
2240
+ y1,
2241
+ x2,
2242
+ y2,
2243
+ ])}
2244
+ {numberField("y1", y1, (n) => [x1, n, x2, y2])}
2245
+ {numberField("x2", x2, (n) => [
2246
+ x1,
2247
+ y1,
2248
+ Math.max(0, Math.min(1, n)),
2249
+ y2,
2250
+ ])}
2251
+ {numberField("y2", y2, (n) => [x1, y1, x2, n])}
2252
+ </div>
2253
+ </div>
2254
+ );
2255
+ }
2256
+
2257
+ // ─── Spring curve preview ─────────────────────────────────────────────────────
2258
+
2259
+ function SpringCurvePreview({
2260
+ bounce,
2261
+ settle,
2262
+ }: {
2263
+ bounce: number;
2264
+ settle: number;
2265
+ }) {
2266
+ const t = useT();
2267
+ const points: string[] = [];
2268
+ const samples = 72;
2269
+ for (let i = 0; i <= samples; i++) {
2270
+ const x = i / samples;
2271
+ const y = sampleSpring({ bounce, settle }, x);
2272
+ points.push(
2273
+ `${(CURVE_PAD + x * (CURVE_W - 2 * CURVE_PAD)).toFixed(1)},${(
2274
+ CURVE_PAD +
2275
+ ((CURVE_Y_MAX - y) / (CURVE_Y_MAX - CURVE_Y_MIN)) *
2276
+ (CURVE_H - 2 * CURVE_PAD)
2277
+ ).toFixed(1)}`,
2278
+ );
2279
+ }
2280
+ return (
2281
+ <svg
2282
+ width="100%"
2283
+ viewBox={`0 0 ${CURVE_W} ${CURVE_H}`}
2284
+ className="rounded border border-border bg-muted/40"
2285
+ aria-label={t("designEditor.motion.springCurvePreview")}
2286
+ role="img"
2287
+ >
2288
+ <line
2289
+ x1={CURVE_PAD}
2290
+ y1={
2291
+ CURVE_PAD +
2292
+ ((CURVE_Y_MAX - 1) / (CURVE_Y_MAX - CURVE_Y_MIN)) *
2293
+ (CURVE_H - 2 * CURVE_PAD)
2294
+ }
2295
+ x2={CURVE_W - CURVE_PAD}
2296
+ y2={
2297
+ CURVE_PAD +
2298
+ ((CURVE_Y_MAX - 1) / (CURVE_Y_MAX - CURVE_Y_MIN)) *
2299
+ (CURVE_H - 2 * CURVE_PAD)
2300
+ }
2301
+ className="stroke-border"
2302
+ strokeDasharray="3 3"
2303
+ />
2304
+ <polyline
2305
+ points={points.join(" ")}
2306
+ className="fill-none stroke-primary"
2307
+ strokeWidth={1.5}
2308
+ />
2309
+ </svg>
2310
+ );
2311
+ }