@agent-native/core 0.85.7 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1019) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +124 -1
  3. package/corpus/core/README.md +0 -11
  4. package/corpus/core/docs/content/cloneable-saas.mdx +0 -1
  5. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +0 -1
  6. package/corpus/core/docs/content/locales/ar-SA/plan-plugin.mdx +10 -9
  7. package/corpus/core/docs/content/locales/ar-SA/sharing.mdx +1 -2
  8. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +2 -3
  9. package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +23 -7
  10. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ar-SA/workspace-connections.mdx +11 -11
  12. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +0 -1
  13. package/corpus/core/docs/content/locales/de-DE/plan-plugin.mdx +10 -9
  14. package/corpus/core/docs/content/locales/de-DE/sharing.mdx +1 -2
  15. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +2 -3
  16. package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +23 -7
  17. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +1 -1
  18. package/corpus/core/docs/content/locales/de-DE/workspace-connections.mdx +1 -1
  19. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +0 -1
  20. package/corpus/core/docs/content/locales/es-ES/plan-plugin.mdx +10 -9
  21. package/corpus/core/docs/content/locales/es-ES/sharing.mdx +1 -2
  22. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +2 -3
  23. package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +23 -7
  24. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +1 -1
  25. package/corpus/core/docs/content/locales/es-ES/workspace-connections.mdx +1 -1
  26. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +0 -1
  27. package/corpus/core/docs/content/locales/fr-FR/plan-plugin.mdx +10 -9
  28. package/corpus/core/docs/content/locales/fr-FR/sharing.mdx +1 -2
  29. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +2 -3
  30. package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +23 -7
  31. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +1 -1
  32. package/corpus/core/docs/content/locales/fr-FR/workspace-connections.mdx +1 -1
  33. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +0 -1
  34. package/corpus/core/docs/content/locales/hi-IN/plan-plugin.mdx +10 -9
  35. package/corpus/core/docs/content/locales/hi-IN/sharing.mdx +1 -2
  36. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +2 -3
  37. package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +23 -7
  38. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +1 -1
  39. package/corpus/core/docs/content/locales/hi-IN/workspace-connections.mdx +1 -1
  40. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +0 -1
  41. package/corpus/core/docs/content/locales/ja-JP/plan-plugin.mdx +10 -9
  42. package/corpus/core/docs/content/locales/ja-JP/sharing.mdx +1 -2
  43. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +2 -3
  44. package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +23 -7
  45. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +1 -1
  46. package/corpus/core/docs/content/locales/ja-JP/workspace-connections.mdx +1 -1
  47. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +0 -1
  48. package/corpus/core/docs/content/locales/ko-KR/plan-plugin.mdx +10 -9
  49. package/corpus/core/docs/content/locales/ko-KR/sharing.mdx +1 -2
  50. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +2 -3
  51. package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +23 -7
  52. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +1 -1
  53. package/corpus/core/docs/content/locales/ko-KR/workspace-connections.mdx +1 -1
  54. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +0 -1
  55. package/corpus/core/docs/content/locales/pt-BR/plan-plugin.mdx +10 -9
  56. package/corpus/core/docs/content/locales/pt-BR/sharing.mdx +1 -2
  57. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +2 -3
  58. package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +23 -7
  59. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +1 -1
  60. package/corpus/core/docs/content/locales/pt-BR/workspace-connections.mdx +1 -1
  61. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +0 -1
  62. package/corpus/core/docs/content/locales/zh-CN/plan-plugin.mdx +10 -9
  63. package/corpus/core/docs/content/locales/zh-CN/sharing.mdx +1 -2
  64. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +2 -3
  65. package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +23 -7
  66. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +1 -1
  67. package/corpus/core/docs/content/locales/zh-CN/workspace-connections.mdx +1 -1
  68. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +0 -1
  69. package/corpus/core/docs/content/locales/zh-TW/plan-plugin.mdx +10 -9
  70. package/corpus/core/docs/content/locales/zh-TW/sharing.mdx +1 -2
  71. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +2 -3
  72. package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +23 -7
  73. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +1 -1
  74. package/corpus/core/docs/content/locales/zh-TW/workspace-connections.mdx +1 -1
  75. package/corpus/core/docs/content/plan-plugin.mdx +12 -11
  76. package/corpus/core/docs/content/sharing.mdx +1 -2
  77. package/corpus/core/docs/content/skills-guide.mdx +4 -3
  78. package/corpus/core/docs/content/template-plan.mdx +37 -6
  79. package/corpus/core/docs/content/tracking.mdx +1 -1
  80. package/corpus/core/docs/content/workspace-connections.mdx +1 -1
  81. package/corpus/core/package.json +6 -6
  82. package/corpus/core/src/agent/run-store.ts +1 -1
  83. package/corpus/core/src/chat-threads/store.ts +73 -11
  84. package/corpus/core/src/checkpoints/store.ts +13 -2
  85. package/corpus/core/src/cli/create.ts +3 -4
  86. package/corpus/core/src/cli/design-connect.ts +469 -9
  87. package/corpus/core/src/cli/index.ts +13 -0
  88. package/corpus/core/src/cli/plan-local.ts +281 -24
  89. package/corpus/core/src/cli/skills.ts +177 -21
  90. package/corpus/core/src/cli/templates-meta.ts +0 -13
  91. package/corpus/core/src/cli/visualize-repo.ts +715 -0
  92. package/corpus/core/src/client/AssistantChat.tsx +126 -17
  93. package/corpus/core/src/client/agent-chat-adapter.ts +6 -5
  94. package/corpus/core/src/client/app-providers.tsx +2 -2
  95. package/corpus/core/src/client/collab/index.ts +31 -0
  96. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +16 -1
  97. package/corpus/core/src/client/components/PresenceBar.tsx +14 -1
  98. package/corpus/core/src/client/components/RecentEditHighlights.tsx +219 -0
  99. package/corpus/core/src/client/components/RemoteSelectionRings.tsx +40 -6
  100. package/corpus/core/src/client/index.ts +48 -0
  101. package/corpus/core/src/client/org/OrgSwitcher.tsx +0 -4
  102. package/corpus/core/src/client/rich-markdown-editor/SharedRichEditor.tsx +49 -32
  103. package/corpus/core/src/client/rich-markdown-editor/index.ts +6 -0
  104. package/corpus/core/src/client/rich-markdown-editor/surgical-apply.ts +192 -0
  105. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +59 -8
  106. package/corpus/core/src/client/session-replay.ts +181 -15
  107. package/corpus/core/src/client/sse-event-processor.ts +90 -1
  108. package/corpus/core/src/client/use-action.ts +123 -25
  109. package/corpus/core/src/client/use-agent-engine-configured.ts +7 -1
  110. package/corpus/core/src/client/use-chat-threads.ts +18 -7
  111. package/corpus/core/src/client/use-db-sync.ts +104 -19
  112. package/corpus/core/src/client/visual-style-controls.tsx +475 -0
  113. package/corpus/core/src/collab/agent-presence.ts +191 -62
  114. package/corpus/core/src/collab/awareness.ts +83 -6
  115. package/corpus/core/src/collab/client.ts +89 -146
  116. package/corpus/core/src/collab/index.ts +34 -0
  117. package/corpus/core/src/collab/recent-edits.ts +171 -0
  118. package/corpus/core/src/collab/undo.ts +432 -0
  119. package/corpus/core/src/collab/ydoc-manager.ts +68 -0
  120. package/corpus/core/src/connections/catalog.ts +2 -3
  121. package/corpus/core/src/db/create-get-db.ts +86 -1
  122. package/corpus/core/src/db/ensure-additive-columns.ts +430 -0
  123. package/corpus/core/src/db/index.ts +6 -0
  124. package/corpus/core/src/db/migrations.ts +236 -35
  125. package/corpus/core/src/jobs/scheduler.ts +17 -14
  126. package/corpus/core/src/observability/traces.ts +182 -1
  127. package/corpus/core/src/org/context.ts +79 -2
  128. package/corpus/core/src/server/agent-chat-plugin.ts +10 -6
  129. package/corpus/core/src/server/auth-marketing.ts +0 -11
  130. package/corpus/core/src/server/auth.ts +11 -4
  131. package/corpus/core/src/server/collab-plugin.ts +56 -13
  132. package/corpus/core/src/server/design-token-utils.ts +1 -1
  133. package/corpus/core/src/server/fusion-app.ts +447 -0
  134. package/corpus/core/src/server/index.ts +13 -0
  135. package/corpus/core/src/server/onboarding-html.ts +3 -4
  136. package/corpus/core/src/server/poll-events.ts +14 -2
  137. package/corpus/core/src/server/poll.ts +219 -12
  138. package/corpus/core/src/server/schema-prompt.ts +28 -9
  139. package/corpus/core/src/settings/store.ts +33 -2
  140. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +2 -0
  141. package/corpus/core/src/sharing/actions/share-resource.ts +3 -0
  142. package/corpus/core/src/sharing/actions/unshare-resource.ts +2 -0
  143. package/corpus/core/src/styles/agent-native.css +0 -5
  144. package/corpus/core/src/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  145. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  146. package/corpus/core/src/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  147. package/corpus/core/src/templates/default/package.json +3 -3
  148. package/corpus/core/src/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  149. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  150. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  151. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  152. package/corpus/core/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  153. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  154. package/corpus/core/src/tracking/providers.ts +22 -1
  155. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +15 -0
  156. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +20 -5
  157. package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +8 -0
  158. package/corpus/templates/analytics/AGENTS.md +33 -6
  159. package/corpus/templates/analytics/actions/navigate.ts +14 -4
  160. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  161. package/corpus/templates/analytics/actions/view-screen.ts +57 -0
  162. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +23 -1
  163. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -0
  164. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -0
  165. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -0
  166. package/corpus/templates/analytics/app/i18n/zh-TW.ts +15 -1
  167. package/corpus/templates/analytics/app/i18n-data.ts +151 -10
  168. package/corpus/templates/analytics/app/pages/Agents.tsx +118 -0
  169. package/corpus/templates/analytics/app/pages/DataSources.tsx +11 -1
  170. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +76 -40
  171. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +5 -0
  172. package/corpus/templates/analytics/app/routes/agents.tsx +10 -0
  173. package/corpus/templates/analytics/changelog/2026-07-03-agent-admin-tools-page.md +6 -0
  174. package/corpus/templates/analytics/changelog/2026-07-03-agent-llm-observability-dashboard.md +6 -0
  175. package/corpus/templates/analytics/changelog/2026-07-03-session-recordings-now-include-response-bodies-for-5xx-error.md +6 -0
  176. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +34 -0
  177. package/corpus/templates/analytics/docs/schemas/tracked-events.md +6 -0
  178. package/corpus/templates/analytics/package.json +3 -3
  179. package/corpus/templates/analytics/seeds/dashboards/agent-observability-llm.json +173 -0
  180. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +23 -5
  181. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +58 -13
  182. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +23 -0
  183. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +5 -0
  184. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +120 -8
  185. package/corpus/templates/analytics/server/lib/session-replay.ts +3 -2
  186. package/corpus/templates/analytics/server/plugins/db.ts +93 -2
  187. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +45 -1
  188. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +10 -0
  189. package/corpus/templates/assets/.agents/skills/real-time-collab/SKILL.md +82 -19
  190. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +15 -0
  191. package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +8 -0
  192. package/corpus/templates/assets/package.json +3 -3
  193. package/corpus/templates/assets/server/plugins/db.ts +63 -2
  194. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +15 -0
  195. package/corpus/templates/brain/package.json +3 -3
  196. package/corpus/templates/brain/server/plugins/db.ts +75 -2
  197. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +15 -0
  198. package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +8 -0
  199. package/corpus/templates/calendar/package.json +3 -3
  200. package/corpus/templates/calendar/server/plugins/db.ts +64 -2
  201. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +15 -0
  202. package/corpus/templates/chat/package.json +3 -3
  203. package/corpus/templates/clips/actions/finalize-recording.ts +50 -40
  204. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
  205. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -3
  206. package/corpus/templates/clips/changelog/2026-07-03-mobile-recording-pages-keep-videos-in-a-16-9-player-with-act.md +6 -0
  207. package/corpus/templates/clips/package.json +3 -3
  208. package/corpus/templates/clips/server/lib/recording-upload-state.ts +56 -0
  209. package/corpus/templates/clips/server/plugins/db.ts +68 -1
  210. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +27 -3
  211. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +15 -0
  212. package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +8 -0
  213. package/corpus/templates/content/actions/edit-document.ts +48 -1
  214. package/corpus/templates/content/actions/list-documents.ts +101 -94
  215. package/corpus/templates/content/actions/search-documents.ts +19 -3
  216. package/corpus/templates/content/actions/update-document.ts +54 -1
  217. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -7
  218. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +97 -2
  219. package/corpus/templates/content/changelog/2026-07-03-agent-document-edits-now-appear-as-a-live-collaborator-prese.md +6 -0
  220. package/corpus/templates/content/changelog/2026-07-03-document-search-and-list-views-load-faster-on-large-workspac.md +6 -0
  221. package/corpus/templates/content/package.json +3 -3
  222. package/corpus/templates/content/server/plugins/db.ts +57 -1
  223. package/corpus/templates/design/.agents/skills/full-app-build/SKILL.md +140 -0
  224. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +20 -0
  225. package/corpus/templates/design/AGENTS.md +50 -12
  226. package/corpus/templates/design/actions/add-fusion-screens.ts +91 -0
  227. package/corpus/templates/design/actions/apply-a11y-fix.ts +5 -1
  228. package/corpus/templates/design/actions/apply-component-prop-edit.ts +10 -2
  229. package/corpus/templates/design/actions/apply-fusion-edits.ts +161 -0
  230. package/corpus/templates/design/actions/apply-visual-edit.ts +34 -1
  231. package/corpus/templates/design/actions/create-component.ts +5 -1
  232. package/corpus/templates/design/actions/create-fusion-app.ts +202 -0
  233. package/corpus/templates/design/actions/deploy-fusion-app.ts +123 -0
  234. package/corpus/templates/design/actions/edit-design.ts +13 -16
  235. package/corpus/templates/design/actions/get-fusion-deploy-status.ts +86 -0
  236. package/corpus/templates/design/actions/list-fusion-edits.ts +96 -0
  237. package/corpus/templates/design/actions/list-local-files.ts +133 -0
  238. package/corpus/templates/design/actions/push-fusion-app.ts +62 -0
  239. package/corpus/templates/design/actions/queue-fusion-edit.ts +112 -0
  240. package/corpus/templates/design/actions/read-local-file.ts +139 -0
  241. package/corpus/templates/design/actions/send-fusion-message.ts +69 -0
  242. package/corpus/templates/design/actions/sync-fusion-app.ts +196 -0
  243. package/corpus/templates/design/actions/write-local-file.ts +115 -14
  244. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +12 -0
  245. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +379 -0
  246. package/corpus/templates/design/app/components/design/EditPanel.tsx +149 -73
  247. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +328 -0
  248. package/corpus/templates/design/app/components/design/LayersPanel.tsx +148 -30
  249. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +1 -1
  250. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +567 -6
  251. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -98
  252. package/corpus/templates/design/app/components/design/code-workbench/ActivityBar.tsx +85 -0
  253. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +466 -0
  254. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +53 -0
  255. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +339 -0
  256. package/corpus/templates/design/app/components/design/code-workbench/editor/Breadcrumbs.tsx +42 -0
  257. package/corpus/templates/design/app/components/design/code-workbench/editor/BufferSync.tsx +128 -0
  258. package/corpus/templates/design/app/components/design/code-workbench/editor/EditorTabs.tsx +363 -0
  259. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +268 -0
  260. package/corpus/templates/design/app/components/design/code-workbench/editor/StatusBar.tsx +208 -0
  261. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-setup.ts +61 -0
  262. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-theme-palette.ts +191 -0
  263. package/corpus/templates/design/app/components/design/code-workbench/editor/monaco-theme.ts +29 -0
  264. package/corpus/templates/design/app/components/design/code-workbench/editor/status-bar-lang.ts +21 -0
  265. package/corpus/templates/design/app/components/design/code-workbench/editor/tab-drag.ts +27 -0
  266. package/corpus/templates/design/app/components/design/code-workbench/explorer/ExplorerView.tsx +155 -0
  267. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +551 -0
  268. package/corpus/templates/design/app/components/design/code-workbench/explorer/file-icons.tsx +115 -0
  269. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +65 -0
  270. package/corpus/templates/design/app/components/design/code-workbench/format/prettier-format.ts +182 -0
  271. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +133 -0
  272. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +563 -0
  273. package/corpus/templates/design/app/components/design/code-workbench/quickinput/fuzzy.ts +170 -0
  274. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +508 -0
  275. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +280 -0
  276. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +734 -0
  277. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +39 -0
  278. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +196 -0
  279. package/corpus/templates/design/app/components/design/code-workbench/workspace/localhost-provider.ts +177 -0
  280. package/corpus/templates/design/app/components/design/code-workbench/workspace/tree.ts +110 -0
  281. package/corpus/templates/design/app/components/design/code-workbench/workspace/types.ts +103 -0
  282. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +68 -0
  283. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +127 -10
  284. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +52 -29
  285. package/corpus/templates/design/app/components/design/inspector/index.ts +9 -0
  286. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +56 -0
  287. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +80 -1
  288. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +6 -16
  289. package/corpus/templates/design/app/i18n/zh-TW.ts +2 -1
  290. package/corpus/templates/design/app/i18n-data.ts +31 -18
  291. package/corpus/templates/design/app/pages/DesignEditor.tsx +1474 -175
  292. package/corpus/templates/design/app/pages/Index.tsx +90 -17
  293. package/corpus/templates/design/changelog/2026-07-03-the-ai-now-edits-like-a-collaborator-on-canvas-its-cursor-gl.md +6 -0
  294. package/corpus/templates/design/changelog/2026-07-03-the-code-panel-is-now-a-full-vs-code-style-editor-with-a-fil.md +6 -0
  295. package/corpus/templates/design/package.json +4 -3
  296. package/corpus/templates/design/server/db/schema.ts +31 -0
  297. package/corpus/templates/design/server/lib/fusion-screens.ts +246 -0
  298. package/corpus/templates/design/server/plugins/db.ts +89 -2
  299. package/corpus/templates/design/shared/collab-selection.ts +52 -0
  300. package/corpus/templates/design/shared/full-app.ts +126 -0
  301. package/corpus/templates/design/shared/pen-path.ts +296 -0
  302. package/corpus/templates/dispatch/package.json +3 -3
  303. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +15 -0
  304. package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +8 -0
  305. package/corpus/templates/forms/package.json +3 -3
  306. package/corpus/templates/forms/server/plugins/db.ts +62 -2
  307. package/corpus/templates/macros/package.json +3 -3
  308. package/corpus/templates/macros/server/plugins/db.ts +63 -2
  309. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +15 -0
  310. package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +8 -0
  311. package/corpus/templates/mail/package.json +3 -3
  312. package/corpus/templates/mail/server/lib/contact-frequency.ts +42 -15
  313. package/corpus/templates/mail/server/plugins/db.ts +63 -2
  314. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +15 -0
  315. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +9 -3
  316. package/corpus/templates/plan/.agents/skills/visualize-repo/SKILL.md +92 -0
  317. package/corpus/templates/plan/actions/export-visual-plan.ts +5 -0
  318. package/corpus/templates/plan/actions/get-local-plan-folder.ts +4 -22
  319. package/corpus/templates/plan/actions/get-plan-version.ts +6 -16
  320. package/corpus/templates/plan/actions/list-plan-versions.ts +48 -20
  321. package/corpus/templates/plan/actions/patch-visual-plan-source.ts +41 -1
  322. package/corpus/templates/plan/actions/promote-local-plan-folder.ts +4 -22
  323. package/corpus/templates/plan/actions/update-local-plan-comments.ts +4 -2
  324. package/corpus/templates/plan/actions/update-local-plan-folder.ts +4 -22
  325. package/corpus/templates/plan/actions/update-visual-plan.ts +60 -1
  326. package/corpus/templates/plan/agent-native.app-skill.json +10 -0
  327. package/corpus/templates/plan/app/components/editor/PlanDocumentEditor.tsx +138 -39
  328. package/corpus/templates/plan/app/components/editor/usePlanUndoStack.ts +165 -23
  329. package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +80 -4
  330. package/corpus/templates/plan/app/global.css +11 -9
  331. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +10 -1
  332. package/corpus/templates/plan/app/hooks/use-plan-presence.ts +66 -0
  333. package/corpus/templates/plan/app/pages/PlansPage.tsx +215 -63
  334. package/corpus/templates/plan/changelog/2026-07-03-local-visual-plans-and-recaps-can-save-comments-without-sign.md +6 -0
  335. package/corpus/templates/plan/changelog/2026-07-03-plan-tabs-render-nested-text-instead-of-raw-json.md +6 -0
  336. package/corpus/templates/plan/changelog/2026-07-03-plans-are-now-fully-real-time-collaborative-multiple-people-.md +6 -0
  337. package/corpus/templates/plan/changelog/2026-07-03-plans-open-and-refresh-faster-especially-long-lived-plans-wi.md +6 -0
  338. package/corpus/templates/plan/changelog/2026-07-03-undo-now-survives-agent-edits-cmd-z-keeps-working-on-your-ow.md +6 -0
  339. package/corpus/templates/plan/package.json +5 -3
  340. package/corpus/templates/plan/server/db/schema.ts +12 -0
  341. package/corpus/templates/plan/server/lib/local-plan-kind.ts +25 -0
  342. package/corpus/templates/plan/server/lib/plan-versions.ts +83 -1
  343. package/corpus/templates/plan/server/lib/public-action-paths.ts +0 -2
  344. package/corpus/templates/plan/server/plans.ts +26 -3
  345. package/corpus/templates/plan/server/plugins/db.ts +105 -2
  346. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +8 -4
  347. package/corpus/templates/slides/AGENTS.md +4 -0
  348. package/corpus/templates/slides/actions/_agent-presence.ts +48 -0
  349. package/corpus/templates/slides/actions/add-slide.ts +16 -18
  350. package/corpus/templates/slides/actions/patch-deck.ts +4 -2
  351. package/corpus/templates/slides/actions/update-slide.ts +114 -77
  352. package/corpus/templates/slides/actions/view-screen.ts +39 -0
  353. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +47 -3
  354. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +32 -35
  355. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +625 -199
  356. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +342 -0
  357. package/corpus/templates/slides/app/components/editor/TweaksPanel.tsx +46 -96
  358. package/corpus/templates/slides/app/components/visual-editor/SaveStatusIndicator.tsx +8 -1
  359. package/corpus/templates/slides/app/context/DeckContext.tsx +589 -176
  360. package/corpus/templates/slides/app/hooks/use-deck-presence.ts +84 -42
  361. package/corpus/templates/slides/app/i18n/ar-SA.ts +46 -0
  362. package/corpus/templates/slides/app/i18n/de-DE.ts +46 -0
  363. package/corpus/templates/slides/app/i18n/en-US.ts +46 -0
  364. package/corpus/templates/slides/app/i18n/es-ES.ts +46 -0
  365. package/corpus/templates/slides/app/i18n/fr-FR.ts +46 -0
  366. package/corpus/templates/slides/app/i18n/hi-IN.ts +46 -0
  367. package/corpus/templates/slides/app/i18n/ja-JP.ts +46 -0
  368. package/corpus/templates/slides/app/i18n/ko-KR.ts +46 -0
  369. package/corpus/templates/slides/app/i18n/pt-BR.ts +46 -0
  370. package/corpus/templates/slides/app/i18n/zh-CN.ts +46 -0
  371. package/corpus/templates/slides/app/i18n/zh-TW.ts +46 -0
  372. package/corpus/templates/slides/app/pages/DeckEditor.tsx +25 -13
  373. package/corpus/templates/slides/changelog/2026-07-03-figma-style-slide-editing.md +6 -0
  374. package/corpus/templates/slides/changelog/2026-07-03-see-the-ai-working-with-you-when-the-agent-edits-a-slide-it-.md +6 -0
  375. package/corpus/templates/slides/changelog/2026-07-03-undo-redo-is-now-precise-and-safe-with-collaborators-it-only.md +6 -0
  376. package/corpus/templates/slides/package.json +3 -3
  377. package/corpus/templates/slides/server/handlers/decks.ts +31 -3
  378. package/corpus/templates/slides/server/plugins/db.ts +65 -2
  379. package/corpus/templates/videos/netlify.toml +1 -12
  380. package/dist/agent/run-store.js +1 -1
  381. package/dist/agent/run-store.js.map +1 -1
  382. package/dist/chat-threads/store.d.ts +9 -1
  383. package/dist/chat-threads/store.d.ts.map +1 -1
  384. package/dist/chat-threads/store.js +63 -11
  385. package/dist/chat-threads/store.js.map +1 -1
  386. package/dist/checkpoints/store.d.ts.map +1 -1
  387. package/dist/checkpoints/store.js +10 -2
  388. package/dist/checkpoints/store.js.map +1 -1
  389. package/dist/cli/create.d.ts.map +1 -1
  390. package/dist/cli/create.js +3 -5
  391. package/dist/cli/create.js.map +1 -1
  392. package/dist/cli/design-connect.d.ts +49 -1
  393. package/dist/cli/design-connect.d.ts.map +1 -1
  394. package/dist/cli/design-connect.js +414 -9
  395. package/dist/cli/design-connect.js.map +1 -1
  396. package/dist/cli/index.js +12 -0
  397. package/dist/cli/index.js.map +1 -1
  398. package/dist/cli/plan-local.d.ts +24 -0
  399. package/dist/cli/plan-local.d.ts.map +1 -1
  400. package/dist/cli/plan-local.js +188 -20
  401. package/dist/cli/plan-local.js.map +1 -1
  402. package/dist/cli/skills.d.ts +3 -1
  403. package/dist/cli/skills.d.ts.map +1 -1
  404. package/dist/cli/skills.js +173 -21
  405. package/dist/cli/skills.js.map +1 -1
  406. package/dist/cli/templates-meta.d.ts.map +1 -1
  407. package/dist/cli/templates-meta.js +0 -14
  408. package/dist/cli/templates-meta.js.map +1 -1
  409. package/dist/cli/visualize-repo.d.ts +49 -0
  410. package/dist/cli/visualize-repo.d.ts.map +1 -0
  411. package/dist/cli/visualize-repo.js +561 -0
  412. package/dist/cli/visualize-repo.js.map +1 -0
  413. package/dist/client/AssistantChat.d.ts.map +1 -1
  414. package/dist/client/AssistantChat.js +120 -15
  415. package/dist/client/AssistantChat.js.map +1 -1
  416. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  417. package/dist/client/agent-chat-adapter.js +6 -5
  418. package/dist/client/agent-chat-adapter.js.map +1 -1
  419. package/dist/client/app-providers.d.ts +2 -2
  420. package/dist/client/app-providers.js.map +1 -1
  421. package/dist/client/collab/index.d.ts +4 -1
  422. package/dist/client/collab/index.d.ts.map +1 -1
  423. package/dist/client/collab/index.js +3 -0
  424. package/dist/client/collab/index.js.map +1 -1
  425. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  426. package/dist/client/components/LiveCursorOverlay.js +11 -3
  427. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  428. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  429. package/dist/client/components/PresenceBar.js +6 -1
  430. package/dist/client/components/PresenceBar.js.map +1 -1
  431. package/dist/client/components/RecentEditHighlights.d.ts +43 -0
  432. package/dist/client/components/RecentEditHighlights.d.ts.map +1 -0
  433. package/dist/client/components/RecentEditHighlights.js +146 -0
  434. package/dist/client/components/RecentEditHighlights.js.map +1 -0
  435. package/dist/client/components/RemoteSelectionRings.d.ts +9 -0
  436. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -1
  437. package/dist/client/components/RemoteSelectionRings.js +22 -8
  438. package/dist/client/components/RemoteSelectionRings.js.map +1 -1
  439. package/dist/client/index.d.ts +6 -1
  440. package/dist/client/index.d.ts.map +1 -1
  441. package/dist/client/index.js +7 -0
  442. package/dist/client/index.js.map +1 -1
  443. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  444. package/dist/client/org/OrgSwitcher.js +1 -3
  445. package/dist/client/org/OrgSwitcher.js.map +1 -1
  446. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +8 -1
  447. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  448. package/dist/client/rich-markdown-editor/SharedRichEditor.js +8 -2
  449. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  450. package/dist/client/rich-markdown-editor/index.d.ts +1 -0
  451. package/dist/client/rich-markdown-editor/index.d.ts.map +1 -1
  452. package/dist/client/rich-markdown-editor/index.js +1 -0
  453. package/dist/client/rich-markdown-editor/index.js.map +1 -1
  454. package/dist/client/rich-markdown-editor/surgical-apply.d.ts +66 -0
  455. package/dist/client/rich-markdown-editor/surgical-apply.d.ts.map +1 -0
  456. package/dist/client/rich-markdown-editor/surgical-apply.js +147 -0
  457. package/dist/client/rich-markdown-editor/surgical-apply.js.map +1 -0
  458. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts +18 -2
  459. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  460. package/dist/client/rich-markdown-editor/useCollabReconcile.js +39 -8
  461. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  462. package/dist/client/session-replay.d.ts +10 -1
  463. package/dist/client/session-replay.d.ts.map +1 -1
  464. package/dist/client/session-replay.js +134 -7
  465. package/dist/client/session-replay.js.map +1 -1
  466. package/dist/client/sse-event-processor.d.ts.map +1 -1
  467. package/dist/client/sse-event-processor.js +81 -1
  468. package/dist/client/sse-event-processor.js.map +1 -1
  469. package/dist/client/use-action.d.ts +24 -0
  470. package/dist/client/use-action.d.ts.map +1 -1
  471. package/dist/client/use-action.js +109 -27
  472. package/dist/client/use-action.js.map +1 -1
  473. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  474. package/dist/client/use-agent-engine-configured.js +7 -1
  475. package/dist/client/use-agent-engine-configured.js.map +1 -1
  476. package/dist/client/use-chat-threads.d.ts.map +1 -1
  477. package/dist/client/use-chat-threads.js +11 -3
  478. package/dist/client/use-chat-threads.js.map +1 -1
  479. package/dist/client/use-db-sync.d.ts +36 -0
  480. package/dist/client/use-db-sync.d.ts.map +1 -1
  481. package/dist/client/use-db-sync.js +68 -12
  482. package/dist/client/use-db-sync.js.map +1 -1
  483. package/dist/client/visual-style-controls.d.ts +79 -0
  484. package/dist/client/visual-style-controls.d.ts.map +1 -0
  485. package/dist/client/visual-style-controls.js +150 -0
  486. package/dist/client/visual-style-controls.js.map +1 -0
  487. package/dist/collab/agent-presence.d.ts +51 -5
  488. package/dist/collab/agent-presence.d.ts.map +1 -1
  489. package/dist/collab/agent-presence.js +144 -58
  490. package/dist/collab/agent-presence.js.map +1 -1
  491. package/dist/collab/awareness-store.d.ts +32 -0
  492. package/dist/collab/awareness-store.d.ts.map +1 -0
  493. package/dist/collab/awareness-store.js +0 -0
  494. package/dist/collab/awareness-store.js.map +1 -0
  495. package/dist/collab/awareness.d.ts +12 -1
  496. package/dist/collab/awareness.d.ts.map +1 -1
  497. package/dist/collab/awareness.js +53 -5
  498. package/dist/collab/awareness.js.map +1 -1
  499. package/dist/collab/client.d.ts +2 -0
  500. package/dist/collab/client.d.ts.map +1 -1
  501. package/dist/collab/client.js +79 -119
  502. package/dist/collab/client.js.map +1 -1
  503. package/dist/collab/index.d.ts +3 -1
  504. package/dist/collab/index.d.ts.map +1 -1
  505. package/dist/collab/index.js +5 -1
  506. package/dist/collab/index.js.map +1 -1
  507. package/dist/collab/recent-edits.d.ts +87 -0
  508. package/dist/collab/recent-edits.d.ts.map +1 -0
  509. package/dist/collab/recent-edits.js +116 -0
  510. package/dist/collab/recent-edits.js.map +1 -0
  511. package/dist/collab/routes.d.ts +2 -2
  512. package/dist/collab/struct-routes.d.ts +1 -1
  513. package/dist/collab/undo.d.ts +121 -0
  514. package/dist/collab/undo.d.ts.map +1 -0
  515. package/dist/collab/undo.js +244 -0
  516. package/dist/collab/undo.js.map +1 -0
  517. package/dist/collab/ydoc-manager.d.ts +10 -0
  518. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  519. package/dist/collab/ydoc-manager.js +57 -0
  520. package/dist/collab/ydoc-manager.js.map +1 -1
  521. package/dist/connections/catalog.d.ts +2 -2
  522. package/dist/connections/catalog.d.ts.map +1 -1
  523. package/dist/connections/catalog.js +1 -1
  524. package/dist/connections/catalog.js.map +1 -1
  525. package/dist/db/create-get-db.d.ts +22 -0
  526. package/dist/db/create-get-db.d.ts.map +1 -1
  527. package/dist/db/create-get-db.js +73 -1
  528. package/dist/db/create-get-db.js.map +1 -1
  529. package/dist/db/ensure-additive-columns.d.ts +93 -0
  530. package/dist/db/ensure-additive-columns.d.ts.map +1 -0
  531. package/dist/db/ensure-additive-columns.js +355 -0
  532. package/dist/db/ensure-additive-columns.js.map +1 -0
  533. package/dist/db/index.d.ts +1 -0
  534. package/dist/db/index.d.ts.map +1 -1
  535. package/dist/db/index.js +1 -0
  536. package/dist/db/index.js.map +1 -1
  537. package/dist/db/migrations.d.ts +70 -0
  538. package/dist/db/migrations.d.ts.map +1 -1
  539. package/dist/db/migrations.js +196 -33
  540. package/dist/db/migrations.js.map +1 -1
  541. package/dist/jobs/scheduler.js +17 -10
  542. package/dist/jobs/scheduler.js.map +1 -1
  543. package/dist/notifications/routes.d.ts +3 -3
  544. package/dist/observability/routes.d.ts +2 -2
  545. package/dist/observability/traces.d.ts.map +1 -1
  546. package/dist/observability/traces.js +155 -1
  547. package/dist/observability/traces.js.map +1 -1
  548. package/dist/org/context.d.ts +7 -0
  549. package/dist/org/context.d.ts.map +1 -1
  550. package/dist/org/context.js +54 -2
  551. package/dist/org/context.js.map +1 -1
  552. package/dist/resources/handlers.d.ts +1 -1
  553. package/dist/secrets/routes.d.ts +9 -9
  554. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  555. package/dist/server/agent-chat-plugin.js +10 -6
  556. package/dist/server/agent-chat-plugin.js.map +1 -1
  557. package/dist/server/auth-marketing.d.ts.map +1 -1
  558. package/dist/server/auth-marketing.js +0 -10
  559. package/dist/server/auth-marketing.js.map +1 -1
  560. package/dist/server/auth.d.ts.map +1 -1
  561. package/dist/server/auth.js +6 -4
  562. package/dist/server/auth.js.map +1 -1
  563. package/dist/server/collab-plugin.d.ts.map +1 -1
  564. package/dist/server/collab-plugin.js +45 -13
  565. package/dist/server/collab-plugin.js.map +1 -1
  566. package/dist/server/design-token-utils.d.ts +1 -1
  567. package/dist/server/design-token-utils.js +1 -1
  568. package/dist/server/design-token-utils.js.map +1 -1
  569. package/dist/server/fusion-app.d.ts +107 -0
  570. package/dist/server/fusion-app.d.ts.map +1 -0
  571. package/dist/server/fusion-app.js +371 -0
  572. package/dist/server/fusion-app.js.map +1 -0
  573. package/dist/server/index.d.ts +1 -0
  574. package/dist/server/index.d.ts.map +1 -1
  575. package/dist/server/index.js +1 -0
  576. package/dist/server/index.js.map +1 -1
  577. package/dist/server/onboarding-html.d.ts.map +1 -1
  578. package/dist/server/onboarding-html.js +3 -4
  579. package/dist/server/onboarding-html.js.map +1 -1
  580. package/dist/server/poll-events.d.ts +2 -0
  581. package/dist/server/poll-events.d.ts.map +1 -1
  582. package/dist/server/poll-events.js +7 -2
  583. package/dist/server/poll-events.js.map +1 -1
  584. package/dist/server/poll.d.ts +47 -3
  585. package/dist/server/poll.d.ts.map +1 -1
  586. package/dist/server/poll.js +176 -10
  587. package/dist/server/poll.js.map +1 -1
  588. package/dist/server/schema-prompt.d.ts.map +1 -1
  589. package/dist/server/schema-prompt.js +24 -7
  590. package/dist/server/schema-prompt.js.map +1 -1
  591. package/dist/server/transcribe-voice.d.ts +1 -1
  592. package/dist/settings/store.d.ts.map +1 -1
  593. package/dist/settings/store.js +32 -3
  594. package/dist/settings/store.js.map +1 -1
  595. package/dist/sharing/actions/set-resource-visibility.js +2 -0
  596. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  597. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  598. package/dist/sharing/actions/share-resource.js +3 -0
  599. package/dist/sharing/actions/share-resource.js.map +1 -1
  600. package/dist/sharing/actions/unshare-resource.js +2 -0
  601. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  602. package/dist/styles/agent-native.css +0 -5
  603. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  604. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  605. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  606. package/dist/templates/default/package.json +3 -3
  607. package/dist/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  608. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  609. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  610. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  611. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  612. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  613. package/dist/tracking/providers.d.ts.map +1 -1
  614. package/dist/tracking/providers.js +17 -1
  615. package/dist/tracking/providers.js.map +1 -1
  616. package/docs/content/cloneable-saas.mdx +0 -1
  617. package/docs/content/locales/ar-SA/cloneable-saas.mdx +0 -1
  618. package/docs/content/locales/ar-SA/plan-plugin.mdx +10 -9
  619. package/docs/content/locales/ar-SA/sharing.mdx +1 -2
  620. package/docs/content/locales/ar-SA/skills-guide.mdx +2 -3
  621. package/docs/content/locales/ar-SA/template-plan.mdx +23 -7
  622. package/docs/content/locales/ar-SA/tracking.mdx +1 -1
  623. package/docs/content/locales/ar-SA/workspace-connections.mdx +11 -11
  624. package/docs/content/locales/de-DE/cloneable-saas.mdx +0 -1
  625. package/docs/content/locales/de-DE/plan-plugin.mdx +10 -9
  626. package/docs/content/locales/de-DE/sharing.mdx +1 -2
  627. package/docs/content/locales/de-DE/skills-guide.mdx +2 -3
  628. package/docs/content/locales/de-DE/template-plan.mdx +23 -7
  629. package/docs/content/locales/de-DE/tracking.mdx +1 -1
  630. package/docs/content/locales/de-DE/workspace-connections.mdx +1 -1
  631. package/docs/content/locales/es-ES/cloneable-saas.mdx +0 -1
  632. package/docs/content/locales/es-ES/plan-plugin.mdx +10 -9
  633. package/docs/content/locales/es-ES/sharing.mdx +1 -2
  634. package/docs/content/locales/es-ES/skills-guide.mdx +2 -3
  635. package/docs/content/locales/es-ES/template-plan.mdx +23 -7
  636. package/docs/content/locales/es-ES/tracking.mdx +1 -1
  637. package/docs/content/locales/es-ES/workspace-connections.mdx +1 -1
  638. package/docs/content/locales/fr-FR/cloneable-saas.mdx +0 -1
  639. package/docs/content/locales/fr-FR/plan-plugin.mdx +10 -9
  640. package/docs/content/locales/fr-FR/sharing.mdx +1 -2
  641. package/docs/content/locales/fr-FR/skills-guide.mdx +2 -3
  642. package/docs/content/locales/fr-FR/template-plan.mdx +23 -7
  643. package/docs/content/locales/fr-FR/tracking.mdx +1 -1
  644. package/docs/content/locales/fr-FR/workspace-connections.mdx +1 -1
  645. package/docs/content/locales/hi-IN/cloneable-saas.mdx +0 -1
  646. package/docs/content/locales/hi-IN/plan-plugin.mdx +10 -9
  647. package/docs/content/locales/hi-IN/sharing.mdx +1 -2
  648. package/docs/content/locales/hi-IN/skills-guide.mdx +2 -3
  649. package/docs/content/locales/hi-IN/template-plan.mdx +23 -7
  650. package/docs/content/locales/hi-IN/tracking.mdx +1 -1
  651. package/docs/content/locales/hi-IN/workspace-connections.mdx +1 -1
  652. package/docs/content/locales/ja-JP/cloneable-saas.mdx +0 -1
  653. package/docs/content/locales/ja-JP/plan-plugin.mdx +10 -9
  654. package/docs/content/locales/ja-JP/sharing.mdx +1 -2
  655. package/docs/content/locales/ja-JP/skills-guide.mdx +2 -3
  656. package/docs/content/locales/ja-JP/template-plan.mdx +23 -7
  657. package/docs/content/locales/ja-JP/tracking.mdx +1 -1
  658. package/docs/content/locales/ja-JP/workspace-connections.mdx +1 -1
  659. package/docs/content/locales/ko-KR/cloneable-saas.mdx +0 -1
  660. package/docs/content/locales/ko-KR/plan-plugin.mdx +10 -9
  661. package/docs/content/locales/ko-KR/sharing.mdx +1 -2
  662. package/docs/content/locales/ko-KR/skills-guide.mdx +2 -3
  663. package/docs/content/locales/ko-KR/template-plan.mdx +23 -7
  664. package/docs/content/locales/ko-KR/tracking.mdx +1 -1
  665. package/docs/content/locales/ko-KR/workspace-connections.mdx +1 -1
  666. package/docs/content/locales/pt-BR/cloneable-saas.mdx +0 -1
  667. package/docs/content/locales/pt-BR/plan-plugin.mdx +10 -9
  668. package/docs/content/locales/pt-BR/sharing.mdx +1 -2
  669. package/docs/content/locales/pt-BR/skills-guide.mdx +2 -3
  670. package/docs/content/locales/pt-BR/template-plan.mdx +23 -7
  671. package/docs/content/locales/pt-BR/tracking.mdx +1 -1
  672. package/docs/content/locales/pt-BR/workspace-connections.mdx +1 -1
  673. package/docs/content/locales/zh-CN/cloneable-saas.mdx +0 -1
  674. package/docs/content/locales/zh-CN/plan-plugin.mdx +10 -9
  675. package/docs/content/locales/zh-CN/sharing.mdx +1 -2
  676. package/docs/content/locales/zh-CN/skills-guide.mdx +2 -3
  677. package/docs/content/locales/zh-CN/template-plan.mdx +23 -7
  678. package/docs/content/locales/zh-CN/tracking.mdx +1 -1
  679. package/docs/content/locales/zh-CN/workspace-connections.mdx +1 -1
  680. package/docs/content/locales/zh-TW/cloneable-saas.mdx +0 -1
  681. package/docs/content/locales/zh-TW/plan-plugin.mdx +10 -9
  682. package/docs/content/locales/zh-TW/sharing.mdx +1 -2
  683. package/docs/content/locales/zh-TW/skills-guide.mdx +2 -3
  684. package/docs/content/locales/zh-TW/template-plan.mdx +23 -7
  685. package/docs/content/locales/zh-TW/tracking.mdx +1 -1
  686. package/docs/content/locales/zh-TW/workspace-connections.mdx +1 -1
  687. package/docs/content/plan-plugin.mdx +12 -11
  688. package/docs/content/sharing.mdx +1 -2
  689. package/docs/content/skills-guide.mdx +4 -3
  690. package/docs/content/template-plan.mdx +37 -6
  691. package/docs/content/tracking.mdx +1 -1
  692. package/docs/content/workspace-connections.mdx +1 -1
  693. package/package.json +6 -6
  694. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +82 -19
  695. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +15 -0
  696. package/src/templates/default/.agents/skills/storing-data/SKILL.md +8 -0
  697. package/src/templates/default/package.json +3 -3
  698. package/src/templates/workspace-core/.agents/skills/client-methods/SKILL.md +8 -0
  699. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +22 -0
  700. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +82 -19
  701. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +15 -0
  702. package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +8 -0
  703. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -0
  704. package/corpus/core/docs/content/locales/ar-SA/template-videos.mdx +0 -336
  705. package/corpus/core/docs/content/locales/de-DE/template-videos.mdx +0 -336
  706. package/corpus/core/docs/content/locales/es-ES/template-videos.mdx +0 -336
  707. package/corpus/core/docs/content/locales/fr-FR/template-videos.mdx +0 -336
  708. package/corpus/core/docs/content/locales/hi-IN/template-videos.mdx +0 -336
  709. package/corpus/core/docs/content/locales/ja-JP/template-videos.mdx +0 -336
  710. package/corpus/core/docs/content/locales/ko-KR/template-videos.mdx +0 -336
  711. package/corpus/core/docs/content/locales/pt-BR/template-videos.mdx +0 -336
  712. package/corpus/core/docs/content/locales/zh-CN/template-videos.mdx +0 -334
  713. package/corpus/core/docs/content/locales/zh-TW/template-videos.mdx +0 -334
  714. package/corpus/core/docs/content/template-videos.mdx +0 -332
  715. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +0 -697
  716. package/corpus/templates/slides/app/components/editor/CodeEditor.tsx +0 -49
  717. package/corpus/templates/videos/.agents/skills/actions/SKILL.md +0 -432
  718. package/corpus/templates/videos/.agents/skills/animation-tracks/SKILL.md +0 -99
  719. package/corpus/templates/videos/.agents/skills/capture-learnings/SKILL.md +0 -76
  720. package/corpus/templates/videos/.agents/skills/composition-management/SKILL.md +0 -106
  721. package/corpus/templates/videos/.agents/skills/create-skill/SKILL.md +0 -221
  722. package/corpus/templates/videos/.agents/skills/delegate-to-agent/SKILL.md +0 -263
  723. package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +0 -112
  724. package/corpus/templates/videos/.agents/skills/security/SKILL.md +0 -263
  725. package/corpus/templates/videos/.agents/skills/self-modifying-code/SKILL.md +0 -88
  726. package/corpus/templates/videos/.agents/skills/shadcn-ui/SKILL.md +0 -95
  727. package/corpus/templates/videos/.builder/ANIMATED_PROPERTIES.md +0 -335
  728. package/corpus/templates/videos/.builder/KEYFRAME_SYNC.md +0 -166
  729. package/corpus/templates/videos/.builder/SEAMLESS_SYNC.md +0 -211
  730. package/corpus/templates/videos/.builder/rules/deploy-app.mdc +0 -22
  731. package/corpus/templates/videos/.builder/rules/organize-ui.mdc +0 -9
  732. package/corpus/templates/videos/.builder/skills/remotion/SKILL.md +0 -29
  733. package/corpus/templates/videos/.builder/skills/remotion/rules/animations.md +0 -27
  734. package/corpus/templates/videos/.builder/skills/remotion/rules/assets.md +0 -43
  735. package/corpus/templates/videos/.builder/skills/remotion/rules/audio.md +0 -58
  736. package/corpus/templates/videos/.builder/skills/remotion/rules/compositions.md +0 -129
  737. package/corpus/templates/videos/.builder/skills/remotion/rules/fonts.md +0 -47
  738. package/corpus/templates/videos/.builder/skills/remotion/rules/images.md +0 -41
  739. package/corpus/templates/videos/.builder/skills/remotion/rules/parameters.md +0 -59
  740. package/corpus/templates/videos/.builder/skills/remotion/rules/sequencing.md +0 -54
  741. package/corpus/templates/videos/.builder/skills/remotion/rules/tailwind.md +0 -11
  742. package/corpus/templates/videos/.builder/skills/remotion/rules/text-animations.md +0 -18
  743. package/corpus/templates/videos/.builder/skills/remotion/rules/timing.md +0 -103
  744. package/corpus/templates/videos/.builder/skills/remotion/rules/transparent-videos.md +0 -35
  745. package/corpus/templates/videos/.builder/skills/remotion/rules/trimming.md +0 -38
  746. package/corpus/templates/videos/.builder/skills/remotion/rules/videos.md +0 -54
  747. package/corpus/templates/videos/.dockerignore +0 -62
  748. package/corpus/templates/videos/.env.example +0 -4
  749. package/corpus/templates/videos/.oxfmtrc.json +0 -8
  750. package/corpus/templates/videos/AGENTS.md +0 -46
  751. package/corpus/templates/videos/CHANGELOG.md +0 -10
  752. package/corpus/templates/videos/DEVELOPING.md +0 -143
  753. package/corpus/templates/videos/README.md +0 -33
  754. package/corpus/templates/videos/actions/analyze-brand-assets.ts +0 -151
  755. package/corpus/templates/videos/actions/apply-design-system.ts +0 -35
  756. package/corpus/templates/videos/actions/create-design-system.ts +0 -84
  757. package/corpus/templates/videos/actions/create-folder.ts +0 -48
  758. package/corpus/templates/videos/actions/db-connect.ts +0 -45
  759. package/corpus/templates/videos/actions/db-health.ts +0 -24
  760. package/corpus/templates/videos/actions/db-status.ts +0 -28
  761. package/corpus/templates/videos/actions/delete-composition.ts +0 -272
  762. package/corpus/templates/videos/actions/delete-folder.ts +0 -27
  763. package/corpus/templates/videos/actions/generate-animated-component.ts +0 -470
  764. package/corpus/templates/videos/actions/get-composition.ts +0 -36
  765. package/corpus/templates/videos/actions/get-design-system.ts +0 -35
  766. package/corpus/templates/videos/actions/import-code.ts +0 -67
  767. package/corpus/templates/videos/actions/import-design-project.ts +0 -42
  768. package/corpus/templates/videos/actions/import-document.ts +0 -125
  769. package/corpus/templates/videos/actions/import-from-url.ts +0 -18
  770. package/corpus/templates/videos/actions/import-github.ts +0 -182
  771. package/corpus/templates/videos/actions/list-compositions.ts +0 -32
  772. package/corpus/templates/videos/actions/list-design-systems.ts +0 -66
  773. package/corpus/templates/videos/actions/list-folders.ts +0 -61
  774. package/corpus/templates/videos/actions/move-composition-to-folder.ts +0 -59
  775. package/corpus/templates/videos/actions/navigate.ts +0 -40
  776. package/corpus/templates/videos/actions/rename-folder.ts +0 -41
  777. package/corpus/templates/videos/actions/run.ts +0 -2
  778. package/corpus/templates/videos/actions/save-composition.ts +0 -114
  779. package/corpus/templates/videos/actions/set-default-design-system.ts +0 -40
  780. package/corpus/templates/videos/actions/update-composition.ts +0 -81
  781. package/corpus/templates/videos/actions/update-design-system.ts +0 -68
  782. package/corpus/templates/videos/actions/validate-compositions.ts +0 -502
  783. package/corpus/templates/videos/actions/view-screen.ts +0 -48
  784. package/corpus/templates/videos/app/components/CameraControls.tsx +0 -442
  785. package/corpus/templates/videos/app/components/CameraToolbar.tsx +0 -437
  786. package/corpus/templates/videos/app/components/CloudUpgrade.tsx +0 -287
  787. package/corpus/templates/videos/app/components/CollabPresenceBar.tsx +0 -81
  788. package/corpus/templates/videos/app/components/CompSettingsEditor.tsx +0 -107
  789. package/corpus/templates/videos/app/components/ComponentLibrarySidebar.tsx +0 -457
  790. package/corpus/templates/videos/app/components/ComponentPropsPanel.tsx +0 -227
  791. package/corpus/templates/videos/app/components/ComponentsSidebar.tsx +0 -70
  792. package/corpus/templates/videos/app/components/CompositionCard.tsx +0 -147
  793. package/corpus/templates/videos/app/components/CurrentElementPanel.tsx +0 -835
  794. package/corpus/templates/videos/app/components/CursorControls.tsx +0 -664
  795. package/corpus/templates/videos/app/components/CursorPositioningOverlay.tsx +0 -238
  796. package/corpus/templates/videos/app/components/LibraryFolderRow.tsx +0 -235
  797. package/corpus/templates/videos/app/components/MotionCurveSelect.tsx +0 -61
  798. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +0 -192
  799. package/corpus/templates/videos/app/components/PropsEditor.tsx +0 -210
  800. package/corpus/templates/videos/app/components/QuestionFlow.tsx +0 -40
  801. package/corpus/templates/videos/app/components/Sidebar.tsx +0 -609
  802. package/corpus/templates/videos/app/components/StudioHeader.tsx +0 -109
  803. package/corpus/templates/videos/app/components/Timeline.tsx +0 -2067
  804. package/corpus/templates/videos/app/components/TrackPropertiesPanel.tsx +0 -922
  805. package/corpus/templates/videos/app/components/TweaksPanel.tsx +0 -277
  806. package/corpus/templates/videos/app/components/VideoPlayer.tsx +0 -487
  807. package/corpus/templates/videos/app/components/design-system/DesignSystemCard.tsx +0 -134
  808. package/corpus/templates/videos/app/components/design-system/DesignSystemPreview.tsx +0 -176
  809. package/corpus/templates/videos/app/components/design-system/DesignSystemSetup.tsx +0 -707
  810. package/corpus/templates/videos/app/components/keyframes/KeyframeActionButtons.tsx +0 -78
  811. package/corpus/templates/videos/app/components/keyframes/KeyframeNavigation.tsx +0 -59
  812. package/corpus/templates/videos/app/components/keyframes/PropertyNumberInput.tsx +0 -56
  813. package/corpus/templates/videos/app/components/layout/Header.tsx +0 -64
  814. package/corpus/templates/videos/app/components/layout/HeaderActions.tsx +0 -84
  815. package/corpus/templates/videos/app/components/layout/Layout.tsx +0 -132
  816. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -180
  817. package/corpus/templates/videos/app/components/ui/accordion.tsx +0 -56
  818. package/corpus/templates/videos/app/components/ui/alert-dialog.tsx +0 -139
  819. package/corpus/templates/videos/app/components/ui/alert.tsx +0 -59
  820. package/corpus/templates/videos/app/components/ui/aspect-ratio.tsx +0 -5
  821. package/corpus/templates/videos/app/components/ui/avatar.tsx +0 -48
  822. package/corpus/templates/videos/app/components/ui/badge.tsx +0 -37
  823. package/corpus/templates/videos/app/components/ui/breadcrumb.tsx +0 -115
  824. package/corpus/templates/videos/app/components/ui/button.tsx +0 -57
  825. package/corpus/templates/videos/app/components/ui/calendar.tsx +0 -68
  826. package/corpus/templates/videos/app/components/ui/card.tsx +0 -86
  827. package/corpus/templates/videos/app/components/ui/carousel.tsx +0 -260
  828. package/corpus/templates/videos/app/components/ui/chart.tsx +0 -375
  829. package/corpus/templates/videos/app/components/ui/checkbox.tsx +0 -28
  830. package/corpus/templates/videos/app/components/ui/collapsible.tsx +0 -9
  831. package/corpus/templates/videos/app/components/ui/command.tsx +0 -154
  832. package/corpus/templates/videos/app/components/ui/context-menu.tsx +0 -202
  833. package/corpus/templates/videos/app/components/ui/dialog.tsx +0 -130
  834. package/corpus/templates/videos/app/components/ui/drawer.tsx +0 -116
  835. package/corpus/templates/videos/app/components/ui/dropdown-menu.tsx +0 -212
  836. package/corpus/templates/videos/app/components/ui/form.tsx +0 -177
  837. package/corpus/templates/videos/app/components/ui/hover-card.tsx +0 -27
  838. package/corpus/templates/videos/app/components/ui/input-otp.tsx +0 -69
  839. package/corpus/templates/videos/app/components/ui/input.tsx +0 -22
  840. package/corpus/templates/videos/app/components/ui/label.tsx +0 -24
  841. package/corpus/templates/videos/app/components/ui/menubar.tsx +0 -234
  842. package/corpus/templates/videos/app/components/ui/navigation-menu.tsx +0 -128
  843. package/corpus/templates/videos/app/components/ui/pagination.tsx +0 -121
  844. package/corpus/templates/videos/app/components/ui/popover.tsx +0 -44
  845. package/corpus/templates/videos/app/components/ui/progress.tsx +0 -26
  846. package/corpus/templates/videos/app/components/ui/radio-group.tsx +0 -42
  847. package/corpus/templates/videos/app/components/ui/resizable.tsx +0 -43
  848. package/corpus/templates/videos/app/components/ui/scroll-area.tsx +0 -46
  849. package/corpus/templates/videos/app/components/ui/select.tsx +0 -158
  850. package/corpus/templates/videos/app/components/ui/separator.tsx +0 -29
  851. package/corpus/templates/videos/app/components/ui/sheet.tsx +0 -160
  852. package/corpus/templates/videos/app/components/ui/sidebar.tsx +0 -782
  853. package/corpus/templates/videos/app/components/ui/skeleton.tsx +0 -15
  854. package/corpus/templates/videos/app/components/ui/slider.tsx +0 -26
  855. package/corpus/templates/videos/app/components/ui/sonner.tsx +0 -58
  856. package/corpus/templates/videos/app/components/ui/spinner.tsx +0 -17
  857. package/corpus/templates/videos/app/components/ui/switch.tsx +0 -27
  858. package/corpus/templates/videos/app/components/ui/table.tsx +0 -117
  859. package/corpus/templates/videos/app/components/ui/tabs.tsx +0 -53
  860. package/corpus/templates/videos/app/components/ui/textarea.tsx +0 -23
  861. package/corpus/templates/videos/app/components/ui/toast.tsx +0 -127
  862. package/corpus/templates/videos/app/components/ui/toaster.tsx +0 -33
  863. package/corpus/templates/videos/app/components/ui/toggle-group.tsx +0 -59
  864. package/corpus/templates/videos/app/components/ui/toggle.tsx +0 -43
  865. package/corpus/templates/videos/app/components/ui/tooltip.tsx +0 -30
  866. package/corpus/templates/videos/app/components/ui/use-toast.ts +0 -3
  867. package/corpus/templates/videos/app/config/constants.ts +0 -204
  868. package/corpus/templates/videos/app/contexts/CompositionContext.tsx +0 -437
  869. package/corpus/templates/videos/app/contexts/CurrentElementContext.tsx +0 -247
  870. package/corpus/templates/videos/app/contexts/PlaybackContext.tsx +0 -65
  871. package/corpus/templates/videos/app/contexts/TimelineContext.tsx +0 -433
  872. package/corpus/templates/videos/app/entry.client.tsx +0 -19
  873. package/corpus/templates/videos/app/entry.server.tsx +0 -10
  874. package/corpus/templates/videos/app/global.css +0 -171
  875. package/corpus/templates/videos/app/hooks/use-composition-collab.ts +0 -269
  876. package/corpus/templates/videos/app/hooks/use-composition-design-system.ts +0 -47
  877. package/corpus/templates/videos/app/hooks/use-database-compositions.ts +0 -144
  878. package/corpus/templates/videos/app/hooks/use-db-status.ts +0 -30
  879. package/corpus/templates/videos/app/hooks/use-design-systems.ts +0 -21
  880. package/corpus/templates/videos/app/hooks/use-folders.ts +0 -199
  881. package/corpus/templates/videos/app/hooks/use-mobile.tsx +0 -21
  882. package/corpus/templates/videos/app/hooks/use-navigation-state.ts +0 -52
  883. package/corpus/templates/videos/app/hooks/use-question-flow.ts +0 -14
  884. package/corpus/templates/videos/app/hooks/use-toast.ts +0 -188
  885. package/corpus/templates/videos/app/hooks/useUnsavedChanges.ts +0 -48
  886. package/corpus/templates/videos/app/i18n/ar-SA.ts +0 -446
  887. package/corpus/templates/videos/app/i18n/de-DE.ts +0 -470
  888. package/corpus/templates/videos/app/i18n/en-US.ts +0 -452
  889. package/corpus/templates/videos/app/i18n/es-ES.ts +0 -468
  890. package/corpus/templates/videos/app/i18n/fr-FR.ts +0 -469
  891. package/corpus/templates/videos/app/i18n/hi-IN.ts +0 -446
  892. package/corpus/templates/videos/app/i18n/index.ts +0 -34
  893. package/corpus/templates/videos/app/i18n/ja-JP.ts +0 -460
  894. package/corpus/templates/videos/app/i18n/ko-KR.ts +0 -453
  895. package/corpus/templates/videos/app/i18n/pt-BR.ts +0 -461
  896. package/corpus/templates/videos/app/i18n/zh-CN.ts +0 -437
  897. package/corpus/templates/videos/app/i18n/zh-TW.ts +0 -437
  898. package/corpus/templates/videos/app/lib/database-compositions.ts +0 -67
  899. package/corpus/templates/videos/app/lib/design-systems.ts +0 -234
  900. package/corpus/templates/videos/app/lib/tab-id.ts +0 -1
  901. package/corpus/templates/videos/app/lib/utils.ts +0 -1
  902. package/corpus/templates/videos/app/pages/ComponentLibrary.tsx +0 -134
  903. package/corpus/templates/videos/app/pages/ComponentLibraryView.tsx +0 -362
  904. package/corpus/templates/videos/app/pages/CompositionView.tsx +0 -715
  905. package/corpus/templates/videos/app/pages/DesignSystems.tsx +0 -180
  906. package/corpus/templates/videos/app/pages/Index.tsx +0 -258
  907. package/corpus/templates/videos/app/pages/NewComposition.tsx +0 -95
  908. package/corpus/templates/videos/app/pages/NotFound.tsx +0 -36
  909. package/corpus/templates/videos/app/remotion/CameraHost.tsx +0 -223
  910. package/corpus/templates/videos/app/remotion/animations/useElementAnimations.ts +0 -231
  911. package/corpus/templates/videos/app/remotion/componentRegistry.ts +0 -473
  912. package/corpus/templates/videos/app/remotion/components/AnimatedElement.tsx +0 -436
  913. package/corpus/templates/videos/app/remotion/components/InteractiveElement.tsx +0 -159
  914. package/corpus/templates/videos/app/remotion/components/InteractiveZoneEditor.tsx +0 -273
  915. package/corpus/templates/videos/app/remotion/compositions/BlankComposition.tsx +0 -204
  916. package/corpus/templates/videos/app/remotion/compositions/index.ts +0 -2
  917. package/corpus/templates/videos/app/remotion/createComponentCursorTrack.ts +0 -132
  918. package/corpus/templates/videos/app/remotion/docs/CURSOR_TRACKS.md +0 -285
  919. package/corpus/templates/videos/app/remotion/easingFunctions.ts +0 -255
  920. package/corpus/templates/videos/app/remotion/hooks/createInteractiveComposition.tsx +0 -283
  921. package/corpus/templates/videos/app/remotion/hooks/useCursorHistory.ts +0 -72
  922. package/corpus/templates/videos/app/remotion/hooks/useCursorTypeFromHover.ts +0 -35
  923. package/corpus/templates/videos/app/remotion/hooks/useEditableZones.tsx +0 -267
  924. package/corpus/templates/videos/app/remotion/hooks/useHoverAnimation.ts +0 -90
  925. package/corpus/templates/videos/app/remotion/hooks/useHoverAnimationSmooth.ts +0 -146
  926. package/corpus/templates/videos/app/remotion/hooks/useInteractiveComponent.ts +0 -711
  927. package/corpus/templates/videos/app/remotion/hooks/useRegisterInteractiveElement.ts +0 -56
  928. package/corpus/templates/videos/app/remotion/library-components/Button.tsx +0 -252
  929. package/corpus/templates/videos/app/remotion/library-components/Card.tsx +0 -241
  930. package/corpus/templates/videos/app/remotion/library-components/CodePanel.tsx +0 -1005
  931. package/corpus/templates/videos/app/remotion/library-components/FileItem.tsx +0 -196
  932. package/corpus/templates/videos/app/remotion/library-components/FolderItem.tsx +0 -199
  933. package/corpus/templates/videos/app/remotion/library-components/PrimaryButton.tsx +0 -177
  934. package/corpus/templates/videos/app/remotion/library-components/SecondaryButton.tsx +0 -167
  935. package/corpus/templates/videos/app/remotion/library-components/SectionHeader.tsx +0 -108
  936. package/corpus/templates/videos/app/remotion/library-components/TEMPLATE.tsx +0 -211
  937. package/corpus/templates/videos/app/remotion/library-components/index.ts +0 -10
  938. package/corpus/templates/videos/app/remotion/registry.ts +0 -91
  939. package/corpus/templates/videos/app/remotion/testing/animationTestUtils.ts +0 -366
  940. package/corpus/templates/videos/app/remotion/trackAnimation.ts +0 -179
  941. package/corpus/templates/videos/app/remotion/trackHelpers.ts +0 -110
  942. package/corpus/templates/videos/app/remotion/ui-components/Cursor.tsx +0 -182
  943. package/corpus/templates/videos/app/remotion/ui-components/InteractiveButton.tsx +0 -116
  944. package/corpus/templates/videos/app/remotion/ui-components/InteractiveCard.tsx +0 -126
  945. package/corpus/templates/videos/app/remotion/ui-components/index.ts +0 -16
  946. package/corpus/templates/videos/app/remotion/utils/animationHelpers.ts +0 -444
  947. package/corpus/templates/videos/app/remotion/utils/interactiveElements.ts +0 -235
  948. package/corpus/templates/videos/app/remotion/utils/springAnimation.ts +0 -76
  949. package/corpus/templates/videos/app/root.tsx +0 -165
  950. package/corpus/templates/videos/app/routes/$.tsx +0 -10
  951. package/corpus/templates/videos/app/routes/_index.tsx +0 -25
  952. package/corpus/templates/videos/app/routes/c.$compositionId.tsx +0 -10
  953. package/corpus/templates/videos/app/routes/components.tsx +0 -10
  954. package/corpus/templates/videos/app/routes/design-systems.tsx +0 -10
  955. package/corpus/templates/videos/app/routes/extensions.$id.$slug.tsx +0 -2
  956. package/corpus/templates/videos/app/routes/extensions.$id.tsx +0 -5
  957. package/corpus/templates/videos/app/routes/extensions._index.tsx +0 -5
  958. package/corpus/templates/videos/app/routes/extensions.tsx +0 -5
  959. package/corpus/templates/videos/app/routes/settings.tsx +0 -88
  960. package/corpus/templates/videos/app/routes/team.tsx +0 -11
  961. package/corpus/templates/videos/app/routes.ts +0 -4
  962. package/corpus/templates/videos/app/state.ts +0 -77
  963. package/corpus/templates/videos/app/types/elementAnimations.ts +0 -121
  964. package/corpus/templates/videos/app/types.ts +0 -200
  965. package/corpus/templates/videos/app/utils/compositionHelpers.ts +0 -445
  966. package/corpus/templates/videos/app/utils/debug.ts +0 -63
  967. package/corpus/templates/videos/app/utils/keyframeUtils.ts +0 -331
  968. package/corpus/templates/videos/app/utils/timelineCoordinates.ts +0 -79
  969. package/corpus/templates/videos/app/vite-env.d.ts +0 -6
  970. package/corpus/templates/videos/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +0 -6
  971. package/corpus/templates/videos/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +0 -6
  972. package/corpus/templates/videos/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +0 -6
  973. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +0 -6
  974. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +0 -6
  975. package/corpus/templates/videos/components.json +0 -20
  976. package/corpus/templates/videos/data/sync-config.json +0 -3
  977. package/corpus/templates/videos/docs/examples/create-composition.example.ts +0 -128
  978. package/corpus/templates/videos/drizzle.config.ts +0 -3
  979. package/corpus/templates/videos/learnings.defaults.md +0 -5
  980. package/corpus/templates/videos/package.json +0 -114
  981. package/corpus/templates/videos/public/agent-native-icon-dark.svg +0 -10
  982. package/corpus/templates/videos/public/agent-native-icon-light.svg +0 -10
  983. package/corpus/templates/videos/public/agent-native-logo-dark.svg +0 -21
  984. package/corpus/templates/videos/public/agent-native-logo-light.svg +0 -21
  985. package/corpus/templates/videos/public/favicon.svg +0 -1
  986. package/corpus/templates/videos/public/icon-180.svg +0 -1
  987. package/corpus/templates/videos/public/icon-192.svg +0 -1
  988. package/corpus/templates/videos/public/icon-512.svg +0 -1
  989. package/corpus/templates/videos/public/manifest.json +0 -21
  990. package/corpus/templates/videos/public/placeholder.svg +0 -1
  991. package/corpus/templates/videos/public/robots.txt +0 -14
  992. package/corpus/templates/videos/react-router.config.ts +0 -7
  993. package/corpus/templates/videos/server/db/index.ts +0 -37
  994. package/corpus/templates/videos/server/db/schema.ts +0 -55
  995. package/corpus/templates/videos/server/middleware/auth.ts +0 -15
  996. package/corpus/templates/videos/server/plugins/agent-chat.ts +0 -29
  997. package/corpus/templates/videos/server/plugins/auth.ts +0 -14
  998. package/corpus/templates/videos/server/plugins/collab.ts +0 -11
  999. package/corpus/templates/videos/server/plugins/core-routes.ts +0 -22
  1000. package/corpus/templates/videos/server/plugins/db.ts +0 -160
  1001. package/corpus/templates/videos/server/routes/[...page].get.ts +0 -5
  1002. package/corpus/templates/videos/server/routes/[...page].head.ts +0 -1
  1003. package/corpus/templates/videos/server/routes/api/save-composition-defaults.post.ts +0 -274
  1004. package/corpus/templates/videos/shared/api.ts +0 -75
  1005. package/corpus/templates/videos/ssr-entry.ts +0 -15
  1006. package/corpus/templates/videos/tsconfig.json +0 -21
  1007. package/corpus/templates/videos/vite.config.ts +0 -19
  1008. package/corpus/templates/videos/vitest.config.ts +0 -16
  1009. package/docs/content/locales/ar-SA/template-videos.mdx +0 -336
  1010. package/docs/content/locales/de-DE/template-videos.mdx +0 -336
  1011. package/docs/content/locales/es-ES/template-videos.mdx +0 -336
  1012. package/docs/content/locales/fr-FR/template-videos.mdx +0 -336
  1013. package/docs/content/locales/hi-IN/template-videos.mdx +0 -336
  1014. package/docs/content/locales/ja-JP/template-videos.mdx +0 -336
  1015. package/docs/content/locales/ko-KR/template-videos.mdx +0 -336
  1016. package/docs/content/locales/pt-BR/template-videos.mdx +0 -336
  1017. package/docs/content/locales/zh-CN/template-videos.mdx +0 -334
  1018. package/docs/content/locales/zh-TW/template-videos.mdx +0 -334
  1019. package/docs/content/template-videos.mdx +0 -332
@@ -1,2067 +0,0 @@
1
- import { useT } from "@agent-native/core/client";
2
- import {
3
- IconCamera,
4
- IconMouse,
5
- IconAlertCircle,
6
- IconTrash,
7
- IconRotate,
8
- } from "@tabler/icons-react";
9
- import { useRef, useCallback, useState, useEffect } from "react";
10
-
11
- import {
12
- AlertDialog,
13
- AlertDialogContent,
14
- AlertDialogHeader,
15
- AlertDialogTitle,
16
- AlertDialogDescription,
17
- AlertDialogFooter,
18
- AlertDialogAction,
19
- AlertDialogCancel,
20
- } from "@/components/ui/alert-dialog";
21
- import {
22
- Tooltip,
23
- TooltipContent,
24
- TooltipTrigger,
25
- } from "@/components/ui/tooltip";
26
- import { useIsMobile } from "@/hooks/use-mobile";
27
- import { cn } from "@/lib/utils";
28
- import type { AnimationTrack, EasingKey } from "@/types";
29
- import {
30
- frameToViewPct,
31
- clientXToFrame,
32
- pxDeltaToFrameDelta,
33
- clampFrame,
34
- } from "@/utils/timelineCoordinates";
35
-
36
- type DragMode = "move" | "resize-start" | "resize-end";
37
-
38
- interface DragState {
39
- trackId: string;
40
- mode: DragMode;
41
- startX: number;
42
- initialStart: number;
43
- initialEnd: number;
44
- barAreaWidth: number;
45
- }
46
-
47
- interface KeyframeDragState {
48
- originalFrame: number;
49
- currentFrame: number;
50
- startX: number;
51
- barAreaWidth: number;
52
- movingKeys?: string[]; // "trackId:frame" keys being moved
53
- originalFrames?: Map<number, number>; // original frame -> original frame
54
- }
55
-
56
- interface KeyframeConflict {
57
- trackId: string;
58
- newFrame: number;
59
- originalFrame: number;
60
- conflictingFrame: number;
61
- }
62
-
63
- export type TimelineProps = {
64
- currentFrame: number;
65
- durationInFrames: number;
66
- fps: number;
67
- onSeek: (frame: number) => void;
68
- tracks: AnimationTrack[];
69
- selectedTrackId: string | null;
70
- onSelectTrack: (id: string | null) => void;
71
- onUpdateTrack: (id: string, patch: Partial<AnimationTrack>) => void;
72
- onDeleteTrack?: (id: string) => void;
73
- // Controlled view window — lifted to CompositionView so VideoPlayer shares it
74
- viewStart: number;
75
- viewEnd: number;
76
- onViewChange: (start: number, end: number) => void;
77
- onCameraKeyframeClick?: (trackType: "camera" | "cursor") => void;
78
- isPlaying?: boolean;
79
- };
80
-
81
- const RULER_HEIGHT = 24;
82
- const TRACK_HEIGHT = 32;
83
- const LABEL_WIDTH_DESKTOP = 180;
84
- const LABEL_WIDTH_MOBILE = 100;
85
- const RANGE_BAR_HEIGHT = 12;
86
-
87
- // Expression-controlled accent (matches TrackPropertiesPanel)
88
- const EXPR_COLOR = "#60a5fa";
89
-
90
- /** True if any animatedProp on this track is driven by code */
91
- function hasExpressions(track: AnimationTrack): boolean {
92
- return !!track.animatedProps?.some((p) => p.programmatic || !!p.codeSnippet);
93
- }
94
-
95
- // Per-easing-type color tokens
96
- const EASING_COLORS: Record<
97
- EasingKey,
98
- { bg: string; border: string; text: string; activeBorder: string }
99
- > = {
100
- linear: {
101
- bg: "rgba(148,163,184,0.15)",
102
- border: "rgba(148,163,184,0.35)",
103
- activeBorder: "#94a3b8",
104
- text: "#94a3b8",
105
- },
106
- "ease-in": {
107
- bg: "rgba(59,130,246,0.15)",
108
- border: "rgba(59,130,246,0.35)",
109
- activeBorder: "#3b82f6",
110
- text: "#3b82f6",
111
- },
112
- "ease-out": {
113
- bg: "rgba(59,130,246,0.15)",
114
- border: "rgba(59,130,246,0.35)",
115
- activeBorder: "#3b82f6",
116
- text: "#3b82f6",
117
- },
118
- "ease-in-out": {
119
- bg: "rgba(37,99,235,0.15)",
120
- border: "rgba(37,99,235,0.35)",
121
- activeBorder: "#2563eb",
122
- text: "#2563eb",
123
- },
124
- spring: {
125
- bg: "rgba(251,191,36,0.15)",
126
- border: "rgba(251,191,36,0.35)",
127
- activeBorder: "#fbbf24",
128
- text: "#fbbf24",
129
- },
130
- // Power eases - blue tones
131
- "power1.in": {
132
- bg: "rgba(96,165,250,0.15)",
133
- border: "rgba(96,165,250,0.35)",
134
- activeBorder: "#60a5fa",
135
- text: "#60a5fa",
136
- },
137
- "power1.out": {
138
- bg: "rgba(59,130,246,0.15)",
139
- border: "rgba(59,130,246,0.35)",
140
- activeBorder: "#3b82f6",
141
- text: "#3b82f6",
142
- },
143
- "power1.inOut": {
144
- bg: "rgba(37,99,235,0.15)",
145
- border: "rgba(37,99,235,0.35)",
146
- activeBorder: "#2563eb",
147
- text: "#2563eb",
148
- },
149
- "power2.in": {
150
- bg: "rgba(96,165,250,0.15)",
151
- border: "rgba(96,165,250,0.35)",
152
- activeBorder: "#60a5fa",
153
- text: "#60a5fa",
154
- },
155
- "power2.out": {
156
- bg: "rgba(59,130,246,0.15)",
157
- border: "rgba(59,130,246,0.35)",
158
- activeBorder: "#3b82f6",
159
- text: "#3b82f6",
160
- },
161
- "power2.inOut": {
162
- bg: "rgba(37,99,235,0.15)",
163
- border: "rgba(37,99,235,0.35)",
164
- activeBorder: "#2563eb",
165
- text: "#2563eb",
166
- },
167
- "power3.in": {
168
- bg: "rgba(96,165,250,0.15)",
169
- border: "rgba(96,165,250,0.35)",
170
- activeBorder: "#60a5fa",
171
- text: "#60a5fa",
172
- },
173
- "power3.out": {
174
- bg: "rgba(59,130,246,0.15)",
175
- border: "rgba(59,130,246,0.35)",
176
- activeBorder: "#3b82f6",
177
- text: "#3b82f6",
178
- },
179
- "power3.inOut": {
180
- bg: "rgba(37,99,235,0.15)",
181
- border: "rgba(37,99,235,0.35)",
182
- activeBorder: "#2563eb",
183
- text: "#2563eb",
184
- },
185
- "power4.in": {
186
- bg: "rgba(96,165,250,0.15)",
187
- border: "rgba(96,165,250,0.35)",
188
- activeBorder: "#60a5fa",
189
- text: "#60a5fa",
190
- },
191
- "power4.out": {
192
- bg: "rgba(59,130,246,0.15)",
193
- border: "rgba(59,130,246,0.35)",
194
- activeBorder: "#3b82f6",
195
- text: "#3b82f6",
196
- },
197
- "power4.inOut": {
198
- bg: "rgba(37,99,235,0.15)",
199
- border: "rgba(37,99,235,0.35)",
200
- activeBorder: "#2563eb",
201
- text: "#2563eb",
202
- },
203
- // Back - blue tones
204
- "back.in": {
205
- bg: "rgba(125,211,252,0.15)",
206
- border: "rgba(125,211,252,0.35)",
207
- activeBorder: "#7DD3FC",
208
- text: "#7DD3FC",
209
- },
210
- "back.out": {
211
- bg: "rgba(0,181,255,0.15)",
212
- border: "rgba(0,181,255,0.35)",
213
- activeBorder: "#00B5FF",
214
- text: "#00B5FF",
215
- },
216
- "back.inOut": {
217
- bg: "rgba(2,132,199,0.15)",
218
- border: "rgba(2,132,199,0.35)",
219
- activeBorder: "#0284C7",
220
- text: "#0284C7",
221
- },
222
- // Bounce - green tones
223
- "bounce.in": {
224
- bg: "rgba(52,211,153,0.15)",
225
- border: "rgba(52,211,153,0.35)",
226
- activeBorder: "#34d399",
227
- text: "#34d399",
228
- },
229
- "bounce.out": {
230
- bg: "rgba(16,185,129,0.15)",
231
- border: "rgba(16,185,129,0.35)",
232
- activeBorder: "#10b981",
233
- text: "#10b981",
234
- },
235
- "bounce.inOut": {
236
- bg: "rgba(5,150,105,0.15)",
237
- border: "rgba(5,150,105,0.35)",
238
- activeBorder: "#059669",
239
- text: "#059669",
240
- },
241
- // Circ - cyan tones
242
- "circ.in": {
243
- bg: "rgba(34,211,238,0.15)",
244
- border: "rgba(34,211,238,0.35)",
245
- activeBorder: "#22d3ee",
246
- text: "#22d3ee",
247
- },
248
- "circ.out": {
249
- bg: "rgba(6,182,212,0.15)",
250
- border: "rgba(6,182,212,0.35)",
251
- activeBorder: "#06b6d4",
252
- text: "#06b6d4",
253
- },
254
- "circ.inOut": {
255
- bg: "rgba(8,145,178,0.15)",
256
- border: "rgba(8,145,178,0.35)",
257
- activeBorder: "#0891b2",
258
- text: "#0891b2",
259
- },
260
- // Elastic - pink tones
261
- "elastic.in": {
262
- bg: "rgba(244,114,182,0.15)",
263
- border: "rgba(244,114,182,0.35)",
264
- activeBorder: "#f472b6",
265
- text: "#f472b6",
266
- },
267
- "elastic.out": {
268
- bg: "rgba(236,72,153,0.15)",
269
- border: "rgba(236,72,153,0.35)",
270
- activeBorder: "#ec4899",
271
- text: "#ec4899",
272
- },
273
- "elastic.inOut": {
274
- bg: "rgba(219,39,119,0.15)",
275
- border: "rgba(219,39,119,0.35)",
276
- activeBorder: "#db2777",
277
- text: "#db2777",
278
- },
279
- // Expo - orange tones
280
- "expo.in": {
281
- bg: "rgba(251,146,60,0.15)",
282
- border: "rgba(251,146,60,0.35)",
283
- activeBorder: "#fb923c",
284
- text: "#fb923c",
285
- },
286
- "expo.out": {
287
- bg: "rgba(249,115,22,0.15)",
288
- border: "rgba(249,115,22,0.35)",
289
- activeBorder: "#f97316",
290
- text: "#f97316",
291
- },
292
- "expo.inOut": {
293
- bg: "rgba(234,88,12,0.15)",
294
- border: "rgba(234,88,12,0.35)",
295
- activeBorder: "#ea580c",
296
- text: "#ea580c",
297
- },
298
- // Sine - teal tones
299
- "sine.in": {
300
- bg: "rgba(94,234,212,0.15)",
301
- border: "rgba(94,234,212,0.35)",
302
- activeBorder: "#5eead4",
303
- text: "#5eead4",
304
- },
305
- "sine.out": {
306
- bg: "rgba(45,212,191,0.15)",
307
- border: "rgba(45,212,191,0.35)",
308
- activeBorder: "#2dd4bf",
309
- text: "#2dd4bf",
310
- },
311
- "sine.inOut": {
312
- bg: "rgba(20,184,166,0.15)",
313
- border: "rgba(20,184,166,0.35)",
314
- activeBorder: "#14b8a6",
315
- text: "#14b8a6",
316
- },
317
- };
318
-
319
- const DEFAULT_EASING_COLOR = EASING_COLORS.linear;
320
-
321
- // IconCamera track styling (blue accent)
322
- const CAMERA_COLOR = "#60a5fa";
323
-
324
- // Cursor track styling (blue accent)
325
- const CURSOR_COLOR = "#00B5FF";
326
-
327
- // ─── RangeBar ────────────────────────────────────────────────────────────────
328
- // AE/C4D-style navigator strip at the bottom of the bar area.
329
- // Left ▶ and right ◀ triangle handles define the visible window.
330
- // Drag the center region to pan. Double-click to reset to full view.
331
-
332
- interface RangeBarProps {
333
- viewStart: number;
334
- viewEnd: number;
335
- durationInFrames: number;
336
- onViewChange: (start: number, end: number) => void;
337
- }
338
-
339
- function RangeBar({
340
- viewStart,
341
- viewEnd,
342
- durationInFrames,
343
- onViewChange,
344
- }: RangeBarProps) {
345
- const containerRef = useRef<HTMLDivElement>(null);
346
- const MIN_WINDOW = Math.max(1, Math.round(durationInFrames * 0.05));
347
-
348
- const startPct = (viewStart / durationInFrames) * 100;
349
- const endPct = (viewEnd / durationInFrames) * 100;
350
-
351
- const HANDLE_W = 10;
352
-
353
- const startRangeDrag = (
354
- e: React.MouseEvent,
355
- mode: "start" | "end" | "pan",
356
- ) => {
357
- e.preventDefault();
358
- e.stopPropagation();
359
-
360
- const container = containerRef.current;
361
- if (!container) return;
362
-
363
- const rect = container.getBoundingClientRect();
364
- const startX = e.clientX;
365
- const initStart = viewStart;
366
- const initEnd = viewEnd;
367
-
368
- const onMouseMove = (ev: MouseEvent) => {
369
- const delta = ev.clientX - startX;
370
- const frameDelta = Math.round((delta / rect.width) * durationInFrames);
371
-
372
- let newStart = initStart;
373
- let newEnd = initEnd;
374
-
375
- if (mode === "start") {
376
- newStart = Math.max(
377
- 0,
378
- Math.min(initEnd - MIN_WINDOW, initStart + frameDelta),
379
- );
380
- } else if (mode === "end") {
381
- newEnd = Math.max(
382
- initStart + MIN_WINDOW,
383
- Math.min(durationInFrames, initEnd + frameDelta),
384
- );
385
- } else {
386
- const dur = initEnd - initStart;
387
- newStart = Math.max(
388
- 0,
389
- Math.min(durationInFrames - dur, initStart + frameDelta),
390
- );
391
- newEnd = newStart + dur;
392
- }
393
-
394
- onViewChange(newStart, newEnd);
395
- };
396
-
397
- const onMouseUp = () => {
398
- window.removeEventListener("mousemove", onMouseMove);
399
- window.removeEventListener("mouseup", onMouseUp);
400
- };
401
-
402
- window.addEventListener("mousemove", onMouseMove);
403
- window.addEventListener("mouseup", onMouseUp);
404
- };
405
-
406
- const isZoomed = viewStart > 0 || viewEnd < durationInFrames; // i18n-ignore scanner false positive
407
- const HANDLE_COLOR = isZoomed
408
- ? "rgba(0,181,255,0.85)"
409
- : "rgba(148,163,184,0.65)";
410
- const RANGE_COLOR = isZoomed
411
- ? "rgba(0,181,255,0.15)"
412
- : "rgba(148,163,184,0.12)";
413
- const BG_COLOR = "rgba(0,0,0,0.20)";
414
-
415
- return (
416
- <div
417
- ref={containerRef}
418
- className="w-full h-full select-none relative"
419
- onDoubleClick={(e) => {
420
- e.stopPropagation();
421
- onViewChange(0, durationInFrames);
422
- }}
423
- >
424
- {/* Background track */}
425
- <div
426
- className="absolute inset-0 rounded-sm"
427
- style={{ backgroundColor: BG_COLOR }}
428
- />
429
-
430
- {/* Active range highlight */}
431
- <div
432
- className="absolute top-0 bottom-0"
433
- style={{
434
- left: `${startPct}%`,
435
- width: `${endPct - startPct}%`,
436
- backgroundColor: RANGE_COLOR,
437
- cursor: "grab",
438
- }}
439
- onMouseDown={(e) => startRangeDrag(e, "pan")}
440
- />
441
-
442
- {/* Left handle — ▶ (inward-pointing right) */}
443
- <div
444
- className="absolute top-0 bottom-0 flex items-center justify-center cursor-ew-resize z-10"
445
- style={{
446
- left: `${startPct}%`,
447
- width: HANDLE_W,
448
- transform: "translateX(-50%)",
449
- }}
450
- onMouseDown={(e) => startRangeDrag(e, "start")}
451
- >
452
- <div
453
- style={{
454
- width: 0,
455
- height: 0,
456
- borderTop: "4px solid transparent",
457
- borderBottom: "4px solid transparent",
458
- borderLeft: `6px solid ${HANDLE_COLOR}`,
459
- }}
460
- />
461
- </div>
462
-
463
- {/* Right handle — ◀ (inward-pointing left) */}
464
- <div
465
- className="absolute top-0 bottom-0 flex items-center justify-center cursor-ew-resize z-10"
466
- style={{
467
- left: `${endPct}%`,
468
- width: HANDLE_W,
469
- transform: "translateX(-50%)",
470
- }}
471
- onMouseDown={(e) => startRangeDrag(e, "end")}
472
- >
473
- <div
474
- style={{
475
- width: 0,
476
- height: 0,
477
- borderTop: "4px solid transparent",
478
- borderBottom: "4px solid transparent",
479
- borderRight: `6px solid ${HANDLE_COLOR}`,
480
- }}
481
- />
482
- </div>
483
-
484
- {/* Zoom indicator — shows when not at full view */}
485
- {isZoomed && (
486
- <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
487
- <span
488
- className="text-[7px] font-mono tracking-widest select-none uppercase"
489
- style={{ color: "rgba(0,181,255,0.4)" }}
490
- >
491
- zoom
492
- </span>
493
- </div>
494
- )}
495
- </div>
496
- );
497
- }
498
-
499
- // ─── Timeline ────────────────────────────────────────────────────────────────
500
-
501
- export function Timeline({
502
- currentFrame,
503
- durationInFrames,
504
- fps,
505
- onSeek,
506
- tracks = [],
507
- selectedTrackId = null,
508
- onSelectTrack = () => {},
509
- onUpdateTrack = () => {},
510
- onDeleteTrack,
511
- viewStart,
512
- viewEnd,
513
- onViewChange: handleViewChange,
514
- onCameraKeyframeClick,
515
- isPlaying = false,
516
- }: TimelineProps) {
517
- const t = useT();
518
- const isMobile = useIsMobile();
519
- const LABEL_WIDTH = isMobile ? LABEL_WIDTH_MOBILE : LABEL_WIDTH_DESKTOP;
520
- const barAreaRef = useRef<HTMLDivElement>(null);
521
- const isDraggingRef = useRef(false);
522
- const keyframeDragRef = useRef<KeyframeDragState | null>(null);
523
- const selectionBoxJustCompletedRef = useRef(false);
524
- const isScrubbingRef = useRef(false);
525
-
526
- // Store original state for keyframe conflict undo (replaces window-based storage)
527
- const conflictUndoStateRef = useRef<{
528
- trackId: string;
529
- originalAnimatedProps: any;
530
- } | null>(null);
531
-
532
- // Local selection state per PRD
533
- const [selectedKeyframes, setSelectedKeyframes] = useState<
534
- Map<string, Set<number>>
535
- >(new Map());
536
- const [keyframeConflict, setKeyframeConflict] =
537
- useState<KeyframeConflict | null>(null);
538
- const [selectionBox, setSelectionBox] = useState<{
539
- trackId: string;
540
- startX: number;
541
- startY: number;
542
- currentX: number;
543
- currentY: number;
544
- } | null>(null);
545
- const [contextMenu, setContextMenu] = useState<{
546
- trackId: string;
547
- x: number;
548
- y: number;
549
- } | null>(null);
550
-
551
- const viewDuration = Math.max(1, viewEnd - viewStart);
552
- const viewSeconds = viewDuration / fps;
553
- const viewStartSeconds = viewStart / fps;
554
- const viewEndSeconds = viewEnd / fps;
555
-
556
- /** Convert a frame number to a % position within the current view window */
557
- const toViewPct = useCallback(
558
- (frame: number) => frameToViewPct(frame, viewStart, viewDuration),
559
- [viewStart, viewDuration],
560
- );
561
-
562
- // Helper to check if a keyframe is selected
563
- const isKeyframeSelected = useCallback(
564
- (trackId: string, frame: number): boolean => {
565
- const trackSelection = selectedKeyframes.get(trackId);
566
- return trackSelection ? trackSelection.has(frame) : false;
567
- },
568
- [selectedKeyframes],
569
- );
570
-
571
- // ─── Ruler tick logic (adapts to zoom level) ────────────────────────────
572
- const getTickInterval = (seconds: number) => {
573
- if (seconds <= 1) return 0.1;
574
- if (seconds <= 3) return 0.5;
575
- if (seconds <= 10) return 1;
576
- if (seconds <= 30) return 2;
577
- if (seconds <= 60) return 5;
578
- return 10;
579
- };
580
-
581
- const tickInterval = getTickInterval(viewSeconds);
582
- const firstTick = Math.ceil(viewStartSeconds / tickInterval) * tickInterval;
583
- const ticks: number[] = [];
584
- for (
585
- let t = firstTick;
586
- t <= viewEndSeconds + 0.0001;
587
- t = parseFloat((t + tickInterval).toFixed(6))
588
- ) {
589
- ticks.push(t);
590
- }
591
-
592
- /** Formats a frame count as seconds with 1 decimal place — always consistent */
593
- const fmtSec = (frames: number) => `${(frames / fps).toFixed(1)}s`;
594
-
595
- const formatTickLabel = (seconds: number) => {
596
- const m = Math.floor(seconds / 60);
597
- const s = Math.floor(seconds % 60);
598
- const frac = seconds % 1;
599
- if (m > 0) return `${m}:${s.toString().padStart(2, "0")}`;
600
- if (frac > 0) return `${s}.${Math.round(frac * 10)}s`;
601
- return `${s}s`;
602
- };
603
-
604
- // ─── Bar-area scrubbing with global mouse tracking ──────────────────────────
605
- const handleBarAreaMouseDown = useCallback(
606
- (e: React.MouseEvent<HTMLDivElement>) => {
607
- // Check if clicking on a track bar - handle track selection
608
- const barEl = (e.target as Element).closest(".track-bar");
609
- if (barEl) {
610
- if (isDraggingRef.current) return;
611
- const trackId = barEl.getAttribute("data-track-id");
612
- if (!trackId) return;
613
- onSelectTrack(trackId === selectedTrackId ? null : trackId);
614
- return;
615
- }
616
-
617
- // Don't start scrubbing when clicking the range bar strip
618
- if ((e.target as Element).closest(".range-bar-zone")) return;
619
-
620
- // Don't start scrubbing if already dragging something else
621
- if (isDraggingRef.current) return;
622
-
623
- // Start scrubbing
624
- const el = barAreaRef.current;
625
- if (!el) return;
626
-
627
- e.preventDefault();
628
- isScrubbingRef.current = true;
629
-
630
- const rect = el.getBoundingClientRect();
631
-
632
- // Seek immediately on mousedown
633
- const frame = clientXToFrame(e.clientX, rect, viewStart, viewDuration);
634
- onSeek(frame);
635
-
636
- const onMouseMove = (ev: MouseEvent) => {
637
- if (!isScrubbingRef.current) return;
638
-
639
- // Update seek position based on mouse X, ignoring Y movement
640
- const el = barAreaRef.current;
641
- if (!el) return;
642
- const rect = el.getBoundingClientRect();
643
- const frame = clientXToFrame(ev.clientX, rect, viewStart, viewDuration);
644
- onSeek(frame);
645
- };
646
-
647
- const onMouseUp = () => {
648
- isScrubbingRef.current = false;
649
- window.removeEventListener("mousemove", onMouseMove);
650
- window.removeEventListener("mouseup", onMouseUp);
651
- };
652
-
653
- window.addEventListener("mousemove", onMouseMove);
654
- window.addEventListener("mouseup", onMouseUp);
655
- },
656
- [viewStart, viewDuration, onSeek, onSelectTrack, selectedTrackId],
657
- );
658
-
659
- // ─── Track bar drag / resize ─────────────────────────────────────────────
660
- const startDrag = useCallback(
661
- (e: React.MouseEvent, track: AnimationTrack, mode: DragMode) => {
662
- e.preventDefault();
663
- const el = barAreaRef.current;
664
- if (!el) return;
665
-
666
- const rect = el.getBoundingClientRect();
667
- isDraggingRef.current = false;
668
-
669
- const dragState: DragState = {
670
- trackId: track.id,
671
- mode,
672
- startX: e.clientX,
673
- initialStart: track.startFrame,
674
- initialEnd: track.endFrame,
675
- barAreaWidth: rect.width,
676
- };
677
-
678
- const onMouseMove = (ev: MouseEvent) => {
679
- const delta = ev.clientX - dragState.startX;
680
- if (Math.abs(delta) > 2) isDraggingRef.current = true;
681
-
682
- // Map pixel delta to frame delta relative to the zoomed view
683
- const frameDelta = pxDeltaToFrameDelta(
684
- delta,
685
- dragState.barAreaWidth,
686
- viewDuration,
687
- );
688
-
689
- let newStart = dragState.initialStart;
690
- let newEnd = dragState.initialEnd;
691
-
692
- if (dragState.mode === "move") {
693
- const dur = dragState.initialEnd - dragState.initialStart;
694
- newStart = Math.max(
695
- 0,
696
- Math.min(
697
- durationInFrames - dur,
698
- dragState.initialStart + frameDelta,
699
- ),
700
- );
701
- newEnd = newStart + dur;
702
- } else if (dragState.mode === "resize-start") {
703
- newStart = Math.max(
704
- 0,
705
- Math.min(
706
- dragState.initialEnd - 1,
707
- dragState.initialStart + frameDelta,
708
- ),
709
- );
710
- } else {
711
- newEnd = Math.max(
712
- dragState.initialStart + 1,
713
- Math.min(durationInFrames, dragState.initialEnd + frameDelta),
714
- );
715
- }
716
-
717
- onUpdateTrack(dragState.trackId, {
718
- startFrame: newStart,
719
- endFrame: newEnd,
720
- });
721
- };
722
-
723
- const onMouseUp = () => {
724
- window.removeEventListener("mousemove", onMouseMove);
725
- window.removeEventListener("mouseup", onMouseUp);
726
- setTimeout(() => {
727
- isDraggingRef.current = false;
728
- }, 0);
729
- };
730
-
731
- window.addEventListener("mousemove", onMouseMove);
732
- window.addEventListener("mouseup", onMouseUp);
733
- },
734
- [durationInFrames, viewDuration, onUpdateTrack],
735
- );
736
-
737
- // Playhead is only shown when the current frame falls within the view window
738
- const playheadVisible = currentFrame >= viewStart && currentFrame <= viewEnd;
739
- const playheadPct = toViewPct(currentFrame);
740
-
741
- // Delete key handler for removing selected keyframes
742
- useEffect(() => {
743
- const handleKeyDown = (e: KeyboardEvent) => {
744
- // Don't trigger if user is typing in an input/textarea
745
- const target = e.target as HTMLElement;
746
- if (
747
- target.tagName === "INPUT" ||
748
- target.tagName === "TEXTAREA" ||
749
- target.isContentEditable
750
- ) {
751
- return;
752
- }
753
-
754
- if (e.key === "Delete" || e.key === "Backspace") {
755
- // Check if any keyframes are selected
756
- if (selectedKeyframes.size === 0) return;
757
-
758
- e.preventDefault();
759
-
760
- // Remove all selected keyframes from their respective tracks
761
- selectedKeyframes.forEach((frames, trackId) => {
762
- if (frames.size === 0) return;
763
-
764
- const track = tracks.find((t) => t.id === trackId);
765
- if (!track || !track.animatedProps) return;
766
-
767
- // Remove all selected frames from all properties
768
- const updatedProps = track.animatedProps.map((prop) => {
769
- if (!prop.keyframes) return prop;
770
-
771
- const newKeyframes = prop.keyframes.filter(
772
- (kf) => !frames.has(kf.frame),
773
- );
774
-
775
- return {
776
- ...prop,
777
- keyframes: newKeyframes.length > 0 ? newKeyframes : undefined,
778
- };
779
- });
780
-
781
- onUpdateTrack(trackId, { animatedProps: updatedProps });
782
- });
783
-
784
- // Clear selection after deleting
785
- setSelectedKeyframes(new Map());
786
- }
787
- };
788
-
789
- window.addEventListener("keydown", handleKeyDown);
790
- return () => window.removeEventListener("keydown", handleKeyDown);
791
- }, [selectedKeyframes, tracks, onUpdateTrack]);
792
-
793
- // Close context menu on click outside
794
- useEffect(() => {
795
- if (!contextMenu) return;
796
-
797
- const handleClick = () => setContextMenu(null);
798
- window.addEventListener("click", handleClick);
799
- return () => window.removeEventListener("click", handleClick);
800
- }, [contextMenu]);
801
-
802
- // Clear track handler - removes all keyframes but keeps the track
803
- const handleClearTrack = useCallback(
804
- (trackId: string) => {
805
- const track = tracks.find((t) => t.id === trackId);
806
- if (!track) return;
807
-
808
- // Clear all keyframes from all properties
809
- const clearedProps = track.animatedProps?.map((prop) => ({
810
- ...prop,
811
- keyframes: [],
812
- }));
813
-
814
- onUpdateTrack(trackId, { animatedProps: clearedProps });
815
- setContextMenu(null);
816
- },
817
- [tracks, onUpdateTrack],
818
- );
819
-
820
- // Delete track handler
821
- const handleDeleteTrack = useCallback(
822
- (trackId: string) => {
823
- if (onDeleteTrack) {
824
- onDeleteTrack(trackId);
825
- }
826
- setContextMenu(null);
827
- },
828
- [onDeleteTrack],
829
- );
830
-
831
- return (
832
- <div className="w-full bg-card/60 border border-border border-t-0 rounded-b-xl overflow-hidden select-none">
833
- <div className="flex h-full overflow-hidden">
834
- {/* ── Label column ──────────────────────────────────────────────── */}
835
- <div
836
- className="flex-shrink-0 flex flex-col border-r border-border/40 bg-card/30"
837
- style={{ width: LABEL_WIDTH }}
838
- >
839
- {/* Ruler header placeholder */}
840
- <div
841
- className="border-b border-border flex items-center justify-between px-2.5"
842
- style={{ height: RULER_HEIGHT }}
843
- >
844
- <span className="text-[9px] text-muted-foreground/25 font-mono uppercase tracking-widest">
845
- Track
846
- </span>
847
- </div>
848
-
849
- {/* Range bar header placeholder */}
850
- <div
851
- className="border-b border-border flex items-center px-2.5 flex-shrink-0"
852
- style={{ height: RANGE_BAR_HEIGHT + 4 }}
853
- >
854
- <span className="text-[9px] text-muted-foreground/25 font-mono uppercase tracking-widest">
855
- View
856
- </span>
857
- </div>
858
-
859
- {/* Scrollable track labels */}
860
- <div className="flex-1 overflow-y-auto scrollbar-thin">
861
- {/* Animation track labels */}
862
- {tracks.map((track) => {
863
- const isSelected = selectedTrackId === track.id;
864
- const colors =
865
- EASING_COLORS[track.easing] ?? DEFAULT_EASING_COLOR;
866
- const isExpr = hasExpressions(track);
867
- const isCamera = track.id === "camera";
868
- const isCursor = track.id === "cursor";
869
- const isKeyframeTrack = track.startFrame === track.endFrame;
870
-
871
- return (
872
- <Tooltip key={track.id}>
873
- <TooltipTrigger asChild>
874
- <div
875
- className="flex items-center gap-1.5 px-2.5 cursor-default"
876
- style={{ height: TRACK_HEIGHT }}
877
- onContextMenu={(e) => {
878
- // Only show context menu for camera and cursor tracks
879
- if (!isCamera && !isCursor) return;
880
- e.preventDefault();
881
- setContextMenu({
882
- trackId: track.id,
883
- x: e.clientX,
884
- y: e.clientY,
885
- });
886
- }}
887
- >
888
- {isCamera ? (
889
- <IconCamera
890
- className="flex-shrink-0"
891
- size={12}
892
- style={{
893
- color: isSelected
894
- ? CAMERA_COLOR
895
- : `${CAMERA_COLOR}99`,
896
- }}
897
- />
898
- ) : isCursor ? (
899
- <IconMouse
900
- className="flex-shrink-0"
901
- size={12}
902
- style={{
903
- color: isSelected
904
- ? CURSOR_COLOR
905
- : `${CURSOR_COLOR}99`,
906
- }}
907
- />
908
- ) : isExpr ? (
909
- <span
910
- className="text-[7px] font-mono font-bold px-1 py-px rounded flex-shrink-0 uppercase tracking-wider leading-none"
911
- style={{
912
- backgroundColor: `${EXPR_COLOR}${isSelected ? "30" : "18"}`,
913
- color: isSelected ? EXPR_COLOR : `${EXPR_COLOR}99`,
914
- }}
915
- >
916
- fx
917
- </span>
918
- ) : isKeyframeTrack ? (
919
- <div
920
- className="w-2 h-2 rotate-45 flex-shrink-0"
921
- style={{
922
- backgroundColor: colors.text,
923
- opacity: isSelected ? 1 : 0.5,
924
- }}
925
- />
926
- ) : (
927
- <div
928
- className="w-1.5 h-1.5 rounded-full flex-shrink-0"
929
- style={{
930
- backgroundColor: colors.text,
931
- opacity: isSelected ? 1 : 0.5,
932
- }}
933
- />
934
- )}
935
- <span
936
- className={cn(
937
- "text-[10px] min-w-0",
938
- isSelected ? "" : "text-muted-foreground/55",
939
- )}
940
- style={{
941
- color: isSelected
942
- ? isCamera
943
- ? CAMERA_COLOR
944
- : isCursor
945
- ? CURSOR_COLOR
946
- : isExpr
947
- ? EXPR_COLOR
948
- : colors.text
949
- : undefined,
950
- // Allow the label to shrink/wrap rather than hard-truncate
951
- overflow: "hidden",
952
- textOverflow: "ellipsis",
953
- whiteSpace: "nowrap",
954
- }}
955
- >
956
- {track.label}
957
- </span>
958
- </div>
959
- </TooltipTrigger>
960
- <TooltipContent>{track.label}</TooltipContent>
961
- </Tooltip>
962
- );
963
- })}
964
- </div>
965
- </div>
966
-
967
- {/* ── Bar area (ruler + range bar + tracks + playhead) ─────────── */}
968
- <div
969
- ref={barAreaRef}
970
- className="flex-1 flex flex-col relative cursor-pointer overflow-hidden"
971
- onMouseDown={handleBarAreaMouseDown}
972
- >
973
- {/* Time ruler — shows ticks within the current view window */}
974
- <div
975
- className="relative w-full border-b border-border flex-shrink-0"
976
- style={{ height: RULER_HEIGHT }}
977
- >
978
- {ticks.map((sec) => {
979
- const pct = ((sec - viewStartSeconds) / viewSeconds) * 100;
980
- return (
981
- <div
982
- key={sec}
983
- className="absolute top-0 flex flex-col items-center"
984
- style={{ left: `${pct}%` }}
985
- >
986
- <div className="w-px h-2.5 bg-border" />
987
- <span className="text-[9px] text-muted-foreground/50 font-mono mt-0.5 -translate-x-1/2">
988
- {formatTickLabel(sec)}
989
- </span>
990
- </div>
991
- );
992
- })}
993
- {ticks.slice(0, -1).map((sec) => {
994
- const subTick = parseFloat((sec + tickInterval / 2).toFixed(6));
995
- if (subTick >= viewEndSeconds) return null;
996
- const pct = ((subTick - viewStartSeconds) / viewSeconds) * 100;
997
- return (
998
- <div
999
- key={`sub-${sec}`}
1000
- className="absolute top-0 w-px h-1.5 bg-border/40"
1001
- style={{ left: `${pct}%` }}
1002
- />
1003
- );
1004
- })}
1005
- </div>
1006
-
1007
- {/* ── Range navigator bar ──────────────────────────────────── */}
1008
- <div
1009
- className="range-bar-zone relative flex-shrink-0"
1010
- style={{ height: RANGE_BAR_HEIGHT + 4 }}
1011
- >
1012
- <RangeBar
1013
- viewStart={viewStart}
1014
- viewEnd={viewEnd}
1015
- durationInFrames={durationInFrames}
1016
- onViewChange={handleViewChange}
1017
- />
1018
- </div>
1019
-
1020
- {/* Scrollable tracks area */}
1021
- <div className="flex-1 overflow-y-auto scrollbar-thin relative">
1022
- {/* ── Animation track rows ──────────────────────────────────── */}
1023
- {tracks.map((track) => {
1024
- const isSelected = selectedTrackId === track.id;
1025
- const colors =
1026
- EASING_COLORS[track.easing] ?? DEFAULT_EASING_COLOR;
1027
- const isExpr = hasExpressions(track);
1028
- const isCamera = track.id === "camera";
1029
- const isCursor = track.id === "cursor";
1030
- const leftPct = toViewPct(track.startFrame);
1031
- const widthPct = toViewPct(track.endFrame) - leftPct;
1032
-
1033
- // Collect all keyframe frames for camera/cursor tracks
1034
- const allKeyframeFrames = new Set<number>();
1035
- if (isCamera || isCursor) {
1036
- track.animatedProps?.forEach((prop) => {
1037
- prop.keyframes?.forEach((kf) =>
1038
- allKeyframeFrames.add(kf.frame),
1039
- );
1040
- });
1041
- }
1042
-
1043
- // Camera/Cursor track: only show keyframes, no track bar
1044
- if (isCamera || isCursor) {
1045
- const trackColor = isCamera ? CAMERA_COLOR : CURSOR_COLOR;
1046
-
1047
- return (
1048
- <div
1049
- key={track.id}
1050
- className="relative w-full"
1051
- style={{ height: TRACK_HEIGHT }}
1052
- onMouseDown={(e) => {
1053
- if (isPlaying) return;
1054
- // Only start box selection if clicking directly on track (not a keyframe)
1055
- if (e.target !== e.currentTarget) return;
1056
-
1057
- e.preventDefault();
1058
- e.stopPropagation();
1059
-
1060
- const el = barAreaRef.current;
1061
- if (!el) return;
1062
- const rect = el.getBoundingClientRect();
1063
- const startX = e.clientX - rect.left;
1064
- const startY = e.clientY;
1065
-
1066
- let hasMoved = false;
1067
-
1068
- // Initialize selection box
1069
- setSelectionBox({
1070
- trackId: track.id,
1071
- startX,
1072
- startY,
1073
- currentX: startX,
1074
- currentY: startY,
1075
- });
1076
-
1077
- const handleMove = (ev: MouseEvent) => {
1078
- const currentX = ev.clientX - rect.left;
1079
- const currentY = ev.clientY;
1080
-
1081
- // Check if mouse has moved significantly (3px threshold)
1082
- const deltaX = Math.abs(currentX - startX);
1083
- const deltaY = Math.abs(currentY - startY);
1084
- if (deltaX > 3 || deltaY > 3) {
1085
- hasMoved = true;
1086
- }
1087
-
1088
- // Update selection box position
1089
- setSelectionBox((prev) =>
1090
- prev
1091
- ? {
1092
- ...prev,
1093
- currentX,
1094
- currentY,
1095
- }
1096
- : null,
1097
- );
1098
-
1099
- // Calculate which keyframes are in the box
1100
- const boxStartPct =
1101
- Math.min(startX / rect.width, currentX / rect.width) *
1102
- 100;
1103
- const boxEndPct =
1104
- Math.max(startX / rect.width, currentX / rect.width) *
1105
- 100;
1106
-
1107
- const framesInBox = new Set<number>();
1108
- allKeyframeFrames.forEach((f) => {
1109
- const framePct = toViewPct(f);
1110
- if (
1111
- framePct >= boxStartPct &&
1112
- framePct <= boxEndPct
1113
- ) {
1114
- framesInBox.add(f);
1115
- }
1116
- });
1117
-
1118
- // Update selection continuously during drag
1119
- setSelectedKeyframes((prev) => {
1120
- const next = new Map(prev);
1121
- next.set(track.id, framesInBox);
1122
- return next;
1123
- });
1124
- };
1125
-
1126
- const handleUp = () => {
1127
- window.removeEventListener("mousemove", handleMove);
1128
- window.removeEventListener("mouseup", handleUp);
1129
- setSelectionBox(null);
1130
-
1131
- // If we had a selection box drag, prevent the next onClick from clearing
1132
- if (hasMoved) {
1133
- selectionBoxJustCompletedRef.current = true;
1134
- // Reset after a short delay (after onClick would have fired)
1135
- setTimeout(() => {
1136
- selectionBoxJustCompletedRef.current = false;
1137
- }, 10);
1138
- }
1139
- };
1140
-
1141
- window.addEventListener("mousemove", handleMove);
1142
- window.addEventListener("mouseup", handleUp);
1143
- }}
1144
- onClick={(e) => {
1145
- // Clear selection when clicking empty area (unless box selection just completed)
1146
- if (
1147
- e.target === e.currentTarget &&
1148
- !selectionBoxJustCompletedRef.current
1149
- ) {
1150
- setSelectedKeyframes(new Map());
1151
- }
1152
- }}
1153
- >
1154
- <div className="absolute inset-x-0 bottom-0 h-px bg-border/20" />
1155
-
1156
- {/* Selection box rendering */}
1157
- {selectionBox &&
1158
- selectionBox.trackId === track.id &&
1159
- barAreaRef.current &&
1160
- (() => {
1161
- const rect = barAreaRef.current.getBoundingClientRect();
1162
- const left = Math.min(
1163
- selectionBox.startX,
1164
- selectionBox.currentX,
1165
- );
1166
- const width = Math.abs(
1167
- selectionBox.currentX - selectionBox.startX,
1168
- );
1169
-
1170
- return (
1171
- <div
1172
- className="absolute pointer-events-none"
1173
- style={{
1174
- left: `${(left / rect.width) * 100}%`,
1175
- width: `${(width / rect.width) * 100}%`,
1176
- top: 0,
1177
- bottom: 0,
1178
- backgroundColor: `${trackColor}1a`, // 10% opacity fill
1179
- border: `1px dashed ${trackColor}99`, // 60% opacity dashed border
1180
- borderRadius: 2,
1181
- }}
1182
- />
1183
- );
1184
- })()}
1185
-
1186
- {/* Keyframe markers only */}
1187
- {Array.from(allKeyframeFrames).map((frame) => {
1188
- const kfPct = toViewPct(frame);
1189
- if (kfPct < 0 || kfPct > 100) return null;
1190
- const isCurrentKeyframe = frame === currentFrame;
1191
-
1192
- // Determine cursor keyframe style based on which properties changed
1193
- let cursorStyle:
1194
- | "position"
1195
- | "type"
1196
- | "both"
1197
- | "clickStart"
1198
- | "clickEnd" = "position";
1199
- if (isCursor) {
1200
- const xProp = track.animatedProps?.find(
1201
- (p) => p.property === "x",
1202
- );
1203
- const yProp = track.animatedProps?.find(
1204
- (p) => p.property === "y",
1205
- );
1206
- const typeProp = track.animatedProps?.find(
1207
- (p) => p.property === "type",
1208
- );
1209
- const clickProp = track.animatedProps?.find(
1210
- (p) => p.property === "isClicking",
1211
- );
1212
-
1213
- const hasPositionKf =
1214
- xProp?.keyframes?.some((kf) => kf.frame === frame) ||
1215
- yProp?.keyframes?.some((kf) => kf.frame === frame);
1216
- const hasTypeKf = typeProp?.keyframes?.some(
1217
- (kf) => kf.frame === frame,
1218
- );
1219
- const clickKf = clickProp?.keyframes?.find(
1220
- (kf) => kf.frame === frame,
1221
- );
1222
-
1223
- // Click keyframes get their own distinct style
1224
- if (clickKf) {
1225
- cursorStyle =
1226
- clickKf.value === "1" ? "clickStart" : "clickEnd";
1227
- } else if (hasPositionKf && hasTypeKf) {
1228
- cursorStyle = "both";
1229
- } else if (hasTypeKf) {
1230
- cursorStyle = "type";
1231
- } else {
1232
- cursorStyle = "position";
1233
- }
1234
- }
1235
-
1236
- const keyframeColor = isCurrentKeyframe
1237
- ? "#ffffff"
1238
- : isCursor
1239
- ? CURSOR_COLOR
1240
- : CAMERA_COLOR;
1241
- const selected = isKeyframeSelected(track.id, frame);
1242
-
1243
- return (
1244
- <Tooltip key={frame}>
1245
- <TooltipTrigger asChild>
1246
- <div
1247
- data-keyframe="true"
1248
- className={cn(
1249
- "absolute top-1/2 -translate-y-1/2 group",
1250
- isPlaying
1251
- ? "cursor-not-allowed opacity-50"
1252
- : "cursor-grab active:cursor-grabbing",
1253
- )}
1254
- style={{
1255
- left: `${kfPct}%`,
1256
- // Extended clickable area with padding on both sides
1257
- transform: "translateY(-50%) translateX(-50%)",
1258
- width: "24px", // Wider clickable area (3× the 8px diamond)
1259
- height: "24px",
1260
- display: "flex",
1261
- alignItems: "center",
1262
- justifyContent: "center",
1263
- zIndex: selected ? 30 : 20, // Selected keyframes on top
1264
- }}
1265
- onMouseDown={(e) => {
1266
- if (isPlaying) return;
1267
- e.preventDefault();
1268
- e.stopPropagation();
1269
-
1270
- // Check if this keyframe is already in the selection
1271
- const isAlreadySelected = isKeyframeSelected(
1272
- track.id,
1273
- frame,
1274
- );
1275
-
1276
- // Determine which frames we'll be dragging
1277
- // If already selected, drag all selected frames; otherwise just this one
1278
- const framesToDrag = isAlreadySelected
1279
- ? selectedKeyframes.get(track.id) ||
1280
- new Set([frame])
1281
- : new Set([frame]);
1282
- const selectedFrames = Array.from(framesToDrag);
1283
-
1284
- // Update selection if needed (for visual feedback)
1285
- if (!isAlreadySelected) {
1286
- setSelectedKeyframes(
1287
- new Map([[track.id, new Set([frame])]]),
1288
- );
1289
- }
1290
-
1291
- // If track is camera or cursor, notify sidebar to open controls
1292
- if (isCamera && onCameraKeyframeClick) {
1293
- onCameraKeyframeClick("camera");
1294
- } else if (isCursor && onCameraKeyframeClick) {
1295
- onCameraKeyframeClick("cursor");
1296
- }
1297
-
1298
- const startX = e.clientX;
1299
- const startY = e.clientY;
1300
- let isDragging = false;
1301
-
1302
- const el = barAreaRef.current;
1303
- if (!el) return;
1304
- const rect = el.getBoundingClientRect();
1305
-
1306
- // Save original state for undo in case of conflict
1307
- conflictUndoStateRef.current = {
1308
- trackId: track.id,
1309
- originalAnimatedProps: track.animatedProps,
1310
- };
1311
-
1312
- // Store original positions of all selected keyframes
1313
- const originalFrames = new Map<
1314
- number,
1315
- number
1316
- >();
1317
- selectedFrames.forEach((f) =>
1318
- originalFrames.set(f, f),
1319
- );
1320
-
1321
- keyframeDragRef.current = {
1322
- originalFrame: frame,
1323
- currentFrame: frame,
1324
- startX,
1325
- barAreaWidth: rect.width,
1326
- movingKeys: selectedFrames.map(
1327
- (f) => `${track.id}:${f}`,
1328
- ),
1329
- originalFrames,
1330
- };
1331
-
1332
- const handleMove = (ev: MouseEvent) => {
1333
- if (!keyframeDragRef.current) return;
1334
-
1335
- // Check if moved significantly (3px threshold)
1336
- const dist = Math.sqrt(
1337
- Math.pow(ev.clientX - startX, 2) +
1338
- Math.pow(ev.clientY - startY, 2),
1339
- );
1340
- if (dist > 3) {
1341
- isDragging = true;
1342
- }
1343
-
1344
- // Calculate delta in pixels, convert to frames
1345
- const deltaX =
1346
- ev.clientX - keyframeDragRef.current.startX;
1347
- const frameChange = pxDeltaToFrameDelta(
1348
- deltaX,
1349
- keyframeDragRef.current.barAreaWidth,
1350
- viewDuration,
1351
- );
1352
-
1353
- // Calculate new positions for all selected keyframes
1354
- const newPositions = new Map<
1355
- number,
1356
- number
1357
- >();
1358
- let minNewFrame = Infinity;
1359
- let maxNewFrame = -Infinity;
1360
-
1361
- selectedFrames.forEach((origFrame) => {
1362
- const newFrame = clampFrame(
1363
- origFrame + frameChange,
1364
- durationInFrames,
1365
- );
1366
- newPositions.set(origFrame, newFrame);
1367
- minNewFrame = Math.min(
1368
- minNewFrame,
1369
- newFrame,
1370
- );
1371
- maxNewFrame = Math.max(
1372
- maxNewFrame,
1373
- newFrame,
1374
- );
1375
- });
1376
-
1377
- // Move ALL selected keyframes
1378
- if (track.animatedProps) {
1379
- const updatedProps =
1380
- track.animatedProps.map((prop) => {
1381
- if (!prop.keyframes) return prop;
1382
-
1383
- const updatedKeyframes =
1384
- prop.keyframes.map((kf) => {
1385
- const newFrame = newPositions.get(
1386
- kf.frame,
1387
- );
1388
- return newFrame !== undefined
1389
- ? { ...kf, frame: newFrame }
1390
- : kf;
1391
- });
1392
-
1393
- return {
1394
- ...prop,
1395
- keyframes: updatedKeyframes,
1396
- };
1397
- });
1398
-
1399
- onUpdateTrack(track.id, {
1400
- animatedProps: updatedProps,
1401
- });
1402
- }
1403
-
1404
- // Update current frame for the dragged keyframe
1405
- const newFrame = newPositions.get(
1406
- keyframeDragRef.current.originalFrame,
1407
- );
1408
- if (newFrame !== undefined) {
1409
- keyframeDragRef.current.currentFrame =
1410
- newFrame;
1411
- // Seek playhead to follow the dragged keyframe
1412
- onSeek(newFrame);
1413
- }
1414
- };
1415
-
1416
- const handleUp = () => {
1417
- window.removeEventListener(
1418
- "mousemove",
1419
- handleMove,
1420
- );
1421
- window.removeEventListener(
1422
- "mouseup",
1423
- handleUp,
1424
- );
1425
-
1426
- if (!keyframeDragRef.current) return;
1427
-
1428
- const finalFrame =
1429
- keyframeDragRef.current.currentFrame;
1430
-
1431
- if (!isDragging) {
1432
- // It was just a click - already handled selection above
1433
- onSeek(frame);
1434
- if (isCamera && onCameraKeyframeClick) {
1435
- onCameraKeyframeClick("camera");
1436
- } else if (
1437
- isCursor &&
1438
- onCameraKeyframeClick
1439
- ) {
1440
- onCameraKeyframeClick("cursor");
1441
- }
1442
- } else {
1443
- // Calculate delta for all keyframes
1444
- const frameChange =
1445
- finalFrame -
1446
- keyframeDragRef.current.originalFrame;
1447
-
1448
- // Build set of all new positions
1449
- const newFrameSet = new Set<number>();
1450
- selectedFrames.forEach((origFrame) => {
1451
- const newPos = clampFrame(
1452
- origFrame + frameChange,
1453
- durationInFrames,
1454
- );
1455
- newFrameSet.add(newPos);
1456
- });
1457
-
1458
- // Check for conflicts: any of the moved keyframes overlap with non-selected keyframes
1459
- if (track.animatedProps) {
1460
- for (const prop of track.animatedProps) {
1461
- if (prop.keyframes) {
1462
- // Get all keyframe positions that are NOT being moved
1463
- const staticFrames = prop.keyframes
1464
- .filter(
1465
- (kf) =>
1466
- !selectedFrames.includes(
1467
- kf.frame,
1468
- ),
1469
- )
1470
- .map((kf) => kf.frame);
1471
-
1472
- // Check if any new position conflicts with a static keyframe
1473
- for (const newPos of newFrameSet) {
1474
- if (staticFrames.includes(newPos)) {
1475
- // Conflict detected - show modal
1476
- setKeyframeConflict({
1477
- trackId: track.id,
1478
- newFrame: newPos,
1479
- originalFrame:
1480
- keyframeDragRef.current
1481
- .originalFrame,
1482
- conflictingFrame: newPos,
1483
- });
1484
- keyframeDragRef.current = null;
1485
- return; // Don't finalize - let modal handle it
1486
- }
1487
- }
1488
- }
1489
- }
1490
- }
1491
-
1492
- // Update selection to reflect new positions
1493
- setSelectedKeyframes((prev) => {
1494
- const next = new Map(prev);
1495
- next.set(track.id, newFrameSet);
1496
- return next;
1497
- });
1498
-
1499
- // No conflict - success!
1500
- conflictUndoStateRef.current = null;
1501
- }
1502
-
1503
- keyframeDragRef.current = null;
1504
- };
1505
-
1506
- window.addEventListener(
1507
- "mousemove",
1508
- handleMove,
1509
- );
1510
- window.addEventListener("mouseup", handleUp);
1511
- }}
1512
- >
1513
- <div
1514
- className={cn("w-2 h-2 rotate-45 relative")}
1515
- style={{
1516
- // Different styles for cursor keyframes based on what changed
1517
- backgroundColor: selected
1518
- ? "#ffffff" // White fill for selected keyframes
1519
- : isCursor &&
1520
- (cursorStyle === "clickStart" ||
1521
- cursorStyle === "clickEnd")
1522
- ? "#facc15" // Yellow for click keyframes
1523
- : isCursor && cursorStyle === "type"
1524
- ? "transparent"
1525
- : keyframeColor,
1526
- border:
1527
- isCursor &&
1528
- cursorStyle === "type" &&
1529
- !selected
1530
- ? `1.5px solid ${keyframeColor}`
1531
- : isCursor &&
1532
- cursorStyle === "both" &&
1533
- !selected
1534
- ? `1.5px solid #38bdf8` // Brighter blue border for both
1535
- : "none",
1536
- opacity:
1537
- isCursor && cursorStyle === "clickEnd"
1538
- ? 0.4 // Semi-transparent for click end
1539
- : 1, // Solid for everything else (including clickStart)
1540
- boxShadow: selected
1541
- ? `0 0 0 2px #ffffffcc, 0 0 8px #ffffff99` // White outline + glow for selected
1542
- : isCursor &&
1543
- (cursorStyle === "clickStart" ||
1544
- cursorStyle === "clickEnd")
1545
- ? `0 0 ${isCurrentKeyframe ? 6 : 4}px #facc15${isCurrentKeyframe ? "cc" : "88"}` // Yellow glow for click keyframes
1546
- : isCursor && cursorStyle === "type"
1547
- ? `0 0 ${isCurrentKeyframe ? 6 : 4}px ${keyframeColor}${isCurrentKeyframe ? "88" : "66"}`
1548
- : isCursor && cursorStyle === "both"
1549
- ? `0 0 ${isCurrentKeyframe ? 8 : 6}px #38bdf8${isCurrentKeyframe ? "cc" : "88"}`
1550
- : `0 0 ${isCurrentKeyframe ? 6 : 4}px ${keyframeColor}${isCurrentKeyframe ? "cc" : "88"}`,
1551
- }}
1552
- />
1553
- </div>
1554
- </TooltipTrigger>
1555
- <TooltipContent>
1556
- {isPlaying
1557
- ? "Pause to select keyframe"
1558
- : `Drag to reposition • Click to select`}
1559
- </TooltipContent>
1560
- </Tooltip>
1561
- );
1562
- })}
1563
- </div>
1564
- );
1565
- }
1566
-
1567
- // Keyframe-style track (instant state change - startFrame === endFrame)
1568
- // These render as diamond markers instead of duration bars
1569
- // Used for: tab switches, modal toggles, instant state changes
1570
- // See AGENTS.md: "Keyframe-Style Tracks" section
1571
- const isKeyframeTrack = track.startFrame === track.endFrame;
1572
-
1573
- if (isKeyframeTrack) {
1574
- const keyframePos = toViewPct(track.startFrame);
1575
- const keyframeColor = isExpr ? EXPR_COLOR : colors.text;
1576
- const keyframeFrame = track.startFrame;
1577
-
1578
- // Check if this keyframe is selected
1579
- const selectedFrames =
1580
- selectedKeyframes.get(track.id) || new Set<number>();
1581
- const keyframeSelected = selectedFrames.has(keyframeFrame);
1582
-
1583
- return (
1584
- <div
1585
- key={track.id}
1586
- className="relative w-full"
1587
- style={{ height: TRACK_HEIGHT }}
1588
- onMouseDown={(e) => {
1589
- if (isPlaying) return;
1590
- // Only start box selection if clicking directly on track (not the keyframe)
1591
- if (e.target !== e.currentTarget) return;
1592
-
1593
- e.preventDefault();
1594
- e.stopPropagation();
1595
-
1596
- const el = barAreaRef.current;
1597
- if (!el) return;
1598
- const rect = el.getBoundingClientRect();
1599
- const startX = e.clientX - rect.left;
1600
- const startY = e.clientY;
1601
-
1602
- let hasMoved = false;
1603
-
1604
- // Initialize selection box
1605
- setSelectionBox({
1606
- trackId: track.id,
1607
- startX,
1608
- startY,
1609
- currentX: startX,
1610
- currentY: startY,
1611
- });
1612
-
1613
- const handleMove = (ev: MouseEvent) => {
1614
- const currentX = ev.clientX - rect.left;
1615
- const currentY = ev.clientY;
1616
-
1617
- // Check if mouse has moved significantly (3px threshold)
1618
- const deltaX = Math.abs(currentX - startX);
1619
- const deltaY = Math.abs(currentY - startY);
1620
- if (deltaX > 3 || deltaY > 3) {
1621
- hasMoved = true;
1622
- }
1623
-
1624
- // Update selection box position
1625
- setSelectionBox((prev) =>
1626
- prev
1627
- ? {
1628
- ...prev,
1629
- currentX,
1630
- currentY,
1631
- }
1632
- : null,
1633
- );
1634
-
1635
- // Calculate which keyframes are in the box
1636
- const boxStartPct =
1637
- Math.min(startX / rect.width, currentX / rect.width) *
1638
- 100;
1639
- const boxEndPct =
1640
- Math.max(startX / rect.width, currentX / rect.width) *
1641
- 100;
1642
-
1643
- const framesInBox = new Set<number>();
1644
- const framePct = toViewPct(keyframeFrame);
1645
- if (framePct >= boxStartPct && framePct <= boxEndPct) {
1646
- framesInBox.add(keyframeFrame);
1647
- }
1648
-
1649
- // Update selection continuously during drag
1650
- setSelectedKeyframes((prev) => {
1651
- const next = new Map(prev);
1652
- next.set(track.id, framesInBox);
1653
- return next;
1654
- });
1655
- };
1656
-
1657
- const handleUp = () => {
1658
- window.removeEventListener("mousemove", handleMove);
1659
- window.removeEventListener("mouseup", handleUp);
1660
- setSelectionBox(null);
1661
-
1662
- // If we had a selection box drag, prevent the next onClick from clearing
1663
- if (hasMoved) {
1664
- selectionBoxJustCompletedRef.current = true;
1665
- // Reset after a short delay (after onClick would have fired)
1666
- setTimeout(() => {
1667
- selectionBoxJustCompletedRef.current = false;
1668
- }, 10);
1669
- }
1670
- };
1671
-
1672
- window.addEventListener("mousemove", handleMove);
1673
- window.addEventListener("mouseup", handleUp);
1674
- }}
1675
- onClick={(e) => {
1676
- // Clear selection when clicking empty area (unless box selection just completed)
1677
- if (
1678
- e.target === e.currentTarget &&
1679
- !selectionBoxJustCompletedRef.current
1680
- ) {
1681
- setSelectedKeyframes(new Map());
1682
- }
1683
- }}
1684
- >
1685
- <div className="absolute inset-x-0 bottom-0 h-px bg-border/20" />
1686
-
1687
- {/* Selection box rendering */}
1688
- {selectionBox &&
1689
- selectionBox.trackId === track.id &&
1690
- barAreaRef.current &&
1691
- (() => {
1692
- const rect = barAreaRef.current.getBoundingClientRect();
1693
- const left = Math.min(
1694
- selectionBox.startX,
1695
- selectionBox.currentX,
1696
- );
1697
- const width = Math.abs(
1698
- selectionBox.currentX - selectionBox.startX,
1699
- );
1700
-
1701
- return (
1702
- <div
1703
- className="absolute pointer-events-none"
1704
- style={{
1705
- left,
1706
- top: 0,
1707
- width,
1708
- height: TRACK_HEIGHT,
1709
- backgroundColor: "rgba(99, 102, 241, 0.15)",
1710
- border: "1px solid rgba(99, 102, 241, 0.4)",
1711
- borderRadius: "4px",
1712
- }}
1713
- />
1714
- );
1715
- })()}
1716
-
1717
- {/* Keyframe diamond marker */}
1718
- <div
1719
- className={cn(
1720
- "absolute top-1/2 -translate-y-1/2 -translate-x-1/2",
1721
- "w-2 h-2 rotate-45 cursor-pointer",
1722
- )}
1723
- style={{
1724
- left: `${keyframePos}%`,
1725
- backgroundColor: keyframeSelected
1726
- ? "#ffffff"
1727
- : keyframeColor,
1728
- border: keyframeSelected ? "none" : undefined,
1729
- boxShadow: keyframeSelected
1730
- ? `0 0 0 2px #ffffffcc, 0 0 8px #ffffff99`
1731
- : `0 0 4px ${keyframeColor}88`,
1732
- }}
1733
- onMouseDown={(e) => {
1734
- if (isPlaying) return;
1735
- e.preventDefault();
1736
- e.stopPropagation();
1737
-
1738
- const el = barAreaRef.current;
1739
- if (!el) return;
1740
- const rect = el.getBoundingClientRect();
1741
- const startX = e.clientX;
1742
- let isDragging = false;
1743
-
1744
- // Click selects the keyframe
1745
- if (!keyframeSelected) {
1746
- setSelectedKeyframes((prev) => {
1747
- const next = new Map(prev);
1748
- next.set(track.id, new Set([keyframeFrame]));
1749
- return next;
1750
- });
1751
- }
1752
-
1753
- const handleMove = (ev: MouseEvent) => {
1754
- const dist = Math.abs(ev.clientX - startX);
1755
- if (dist > 3) {
1756
- isDragging = true;
1757
- const deltaX = ev.clientX - startX;
1758
- const frameChange = pxDeltaToFrameDelta(
1759
- deltaX,
1760
- rect.width,
1761
- viewDuration,
1762
- );
1763
- const newFrame = clampFrame(
1764
- track.startFrame + frameChange,
1765
- durationInFrames,
1766
- );
1767
-
1768
- // Update both start and end to keep it as a keyframe
1769
- onUpdateTrack(track.id, {
1770
- startFrame: newFrame,
1771
- endFrame: newFrame,
1772
- });
1773
-
1774
- // Update selection to reflect new position
1775
- setSelectedKeyframes((prev) => {
1776
- const next = new Map(prev);
1777
- next.set(track.id, new Set([newFrame]));
1778
- return next;
1779
- });
1780
- }
1781
- };
1782
-
1783
- const handleUp = () => {
1784
- window.removeEventListener("mousemove", handleMove);
1785
- window.removeEventListener("mouseup", handleUp);
1786
-
1787
- if (!isDragging) {
1788
- // Just a click - select track and seek
1789
- onSelectTrack(track.id);
1790
- onSeek(track.startFrame);
1791
- }
1792
- };
1793
-
1794
- window.addEventListener("mousemove", handleMove);
1795
- window.addEventListener("mouseup", handleUp);
1796
- }}
1797
- />
1798
- </div>
1799
- );
1800
- }
1801
-
1802
- // Regular animation tracks (with duration)
1803
- return (
1804
- <div
1805
- key={track.id}
1806
- className="relative w-full"
1807
- style={{ height: TRACK_HEIGHT }}
1808
- >
1809
- <div className="absolute inset-x-0 bottom-0 h-px bg-border/20" />
1810
-
1811
- <div
1812
- className="track-bar absolute inset-y-1.5 rounded cursor-grab active:cursor-grabbing"
1813
- data-track-id={track.id}
1814
- style={{
1815
- left: `${leftPct}%`,
1816
- width: `${Math.max(widthPct, 0.5)}%`,
1817
- minWidth: 12,
1818
- backgroundColor: colors.bg,
1819
- border: `1px solid ${
1820
- isSelected ? colors.activeBorder : colors.border
1821
- }`,
1822
- opacity: isSelected ? 1 : 0.65,
1823
- boxShadow: isSelected
1824
- ? `0 0 0 1px ${colors.activeBorder}22, 0 2px 8px ${colors.activeBorder}20`
1825
- : "none",
1826
- }}
1827
- onMouseDown={(e) => {
1828
- startDrag(e, track, "move");
1829
- }}
1830
- >
1831
- {/* Left resize handle */}
1832
- <div
1833
- className="resize-handle absolute left-0 top-0 bottom-0 cursor-ew-resize z-10 flex items-center justify-center"
1834
- style={{ width: 8 }}
1835
- onMouseDown={(e) => {
1836
- e.stopPropagation();
1837
- startDrag(e, track, "resize-start");
1838
- }}
1839
- >
1840
- <div
1841
- className="w-px h-3 rounded-full"
1842
- style={{
1843
- backgroundColor: colors.text,
1844
- opacity: 0.5,
1845
- }}
1846
- />
1847
- </div>
1848
-
1849
- {/* Time range label */}
1850
- <div className="absolute inset-0 flex items-center justify-center px-2 pointer-events-none overflow-hidden">
1851
- <span
1852
- className="text-[8px] font-mono whitespace-nowrap"
1853
- style={{
1854
- color: colors.text,
1855
- opacity: isSelected ? 0.9 : 0.6,
1856
- }}
1857
- >
1858
- {fmtSec(track.startFrame)}–{fmtSec(track.endFrame)}
1859
- </span>
1860
- </div>
1861
-
1862
- {/* Right resize handle */}
1863
- <div
1864
- className="resize-handle absolute right-0 top-0 bottom-0 cursor-ew-resize z-10 flex items-center justify-center"
1865
- style={{ width: 8 }}
1866
- onMouseDown={(e) => {
1867
- e.stopPropagation();
1868
- startDrag(e, track, "resize-end");
1869
- }}
1870
- >
1871
- <div
1872
- className="w-px h-3 rounded-full"
1873
- style={{
1874
- backgroundColor: colors.text,
1875
- opacity: 0.5,
1876
- }}
1877
- />
1878
- </div>
1879
- </div>
1880
- </div>
1881
- );
1882
- })}
1883
- </div>
1884
-
1885
- {/* ── Playhead ────────────────────────────────────────────── */}
1886
- {playheadVisible && (
1887
- <div
1888
- className="absolute z-20 pointer-events-none"
1889
- style={{
1890
- left: `${playheadPct}%`,
1891
- // Span from range bar to bottom of tracks
1892
- top: RANGE_BAR_HEIGHT + 4,
1893
- bottom: 0,
1894
- }}
1895
- >
1896
- <div className="absolute inset-y-0 w-px bg-foreground/65 -translate-x-px" />
1897
- <div className="absolute top-0 -translate-x-1/2 w-2 h-3 bg-foreground rounded-b-sm" />
1898
- </div>
1899
- )}
1900
- </div>
1901
- </div>
1902
-
1903
- {/* ── Keyframe Conflict Resolution Modal ──────────────────────────── */}
1904
- <AlertDialog
1905
- open={!!keyframeConflict}
1906
- onOpenChange={(open) => {
1907
- if (!open) {
1908
- // Closing = undo
1909
- const conflictState = conflictUndoStateRef.current;
1910
- if (
1911
- keyframeConflict &&
1912
- conflictState &&
1913
- conflictState.trackId === keyframeConflict.trackId
1914
- ) {
1915
- onUpdateTrack(conflictState.trackId, {
1916
- animatedProps: conflictState.originalAnimatedProps,
1917
- });
1918
- conflictUndoStateRef.current = null;
1919
- }
1920
- setKeyframeConflict(null);
1921
- }
1922
- }}
1923
- >
1924
- <AlertDialogContent className="max-w-md">
1925
- <AlertDialogHeader>
1926
- <AlertDialogTitle className="flex items-center gap-3 text-sm">
1927
- <IconAlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0" />
1928
- {t("raw.timeline.keyframeOverlap")}
1929
- </AlertDialogTitle>
1930
- <AlertDialogDescription className="text-xs leading-relaxed">
1931
- Moving the keyframe(s) would create an overlap at frame{" "}
1932
- <span className="font-mono font-medium text-foreground">
1933
- {keyframeConflict?.newFrame}
1934
- </span>
1935
- .
1936
- </AlertDialogDescription>
1937
- </AlertDialogHeader>
1938
- <AlertDialogFooter className="flex-col sm:flex-col space-y-2 sm:space-x-0">
1939
- <AlertDialogAction
1940
- onClick={() => {
1941
- if (!keyframeConflict) return;
1942
- // Replace existing keyframes using Map-based deduplication
1943
- const targetTrack = tracks.find(
1944
- (t) => t.id === keyframeConflict.trackId,
1945
- );
1946
- if (targetTrack?.animatedProps) {
1947
- const updatedProps = targetTrack.animatedProps.map((prop) => {
1948
- if (!prop.keyframes) return prop;
1949
-
1950
- // Group keyframes by frame number
1951
- const frameMap = new Map<
1952
- number,
1953
- (typeof prop.keyframes)[0]
1954
- >();
1955
- prop.keyframes.forEach((kf) => {
1956
- // Later keyframes overwrite earlier ones at the same frame
1957
- frameMap.set(kf.frame, kf);
1958
- });
1959
-
1960
- // Convert back to array
1961
- const updatedKeyframes = Array.from(frameMap.values());
1962
- return { ...prop, keyframes: updatedKeyframes };
1963
- });
1964
- onUpdateTrack(keyframeConflict.trackId, {
1965
- animatedProps: updatedProps,
1966
- });
1967
- }
1968
- conflictUndoStateRef.current = null;
1969
- setKeyframeConflict(null);
1970
- }}
1971
- className="w-full"
1972
- >
1973
- {t("raw.timeline.replaceExistingKeyframes")}
1974
- </AlertDialogAction>
1975
- <AlertDialogCancel
1976
- onClick={() => {
1977
- // Undo - restore from saved state
1978
- const conflictState = conflictUndoStateRef.current;
1979
- if (
1980
- keyframeConflict &&
1981
- conflictState &&
1982
- conflictState.trackId === keyframeConflict.trackId
1983
- ) {
1984
- onUpdateTrack(conflictState.trackId, {
1985
- animatedProps: conflictState.originalAnimatedProps,
1986
- });
1987
- conflictUndoStateRef.current = null;
1988
- }
1989
- setKeyframeConflict(null);
1990
- }}
1991
- className="w-full"
1992
- >
1993
- {t("raw.timeline.undoMove")}
1994
- </AlertDialogCancel>
1995
- </AlertDialogFooter>
1996
- </AlertDialogContent>
1997
- </AlertDialog>
1998
-
1999
- {/* ── Context Menu for Track Management ──────────────────────────── */}
2000
- {contextMenu &&
2001
- (() => {
2002
- const trackName =
2003
- contextMenu.trackId === "camera" ? "Camera" : "Cursor";
2004
-
2005
- // Estimate menu height (header ~36px + 2 options ~28px each + padding ~8px = ~100px)
2006
- const estimatedMenuHeight = 100;
2007
- const estimatedMenuWidth = 160;
2008
- const padding = 8; // Keep menu away from edges
2009
-
2010
- // Calculate position to prevent overflow
2011
- let left = contextMenu.x;
2012
- let top = contextMenu.y;
2013
-
2014
- // Prevent right edge overflow
2015
- if (left + estimatedMenuWidth > window.innerWidth - padding) {
2016
- left = window.innerWidth - estimatedMenuWidth - padding;
2017
- }
2018
-
2019
- // Prevent bottom edge overflow - position above click if needed
2020
- if (top + estimatedMenuHeight > window.innerHeight - padding) {
2021
- top = contextMenu.y - estimatedMenuHeight;
2022
- }
2023
-
2024
- // Prevent top edge overflow (in case mouse is very near top)
2025
- if (top < padding) {
2026
- top = padding;
2027
- }
2028
-
2029
- return (
2030
- <div
2031
- className="fixed bg-card border border-border rounded-lg shadow-xl z-50 min-w-[160px] overflow-hidden"
2032
- style={{
2033
- left,
2034
- top,
2035
- }}
2036
- onClick={(e) => e.stopPropagation()}
2037
- >
2038
- {/* Header */}
2039
- <div className="px-3 py-2 bg-secondary/30 border-b border-border">
2040
- <div className="text-xs font-medium text-foreground">
2041
- {trackName} Track
2042
- </div>
2043
- </div>
2044
-
2045
- {/* Options */}
2046
- <div className="py-1">
2047
- <button
2048
- onClick={() => handleClearTrack(contextMenu.trackId)}
2049
- className="w-full px-3 py-1.5 text-left text-xs hover:bg-secondary transition-colors flex items-center gap-2 text-foreground/80 hover:text-foreground"
2050
- >
2051
- <IconRotate className="w-3.5 h-3.5 text-muted-foreground" />
2052
- {t("raw.timeline.clearTrack")}
2053
- </button>
2054
- <button
2055
- onClick={() => handleDeleteTrack(contextMenu.trackId)}
2056
- className="w-full px-3 py-1.5 text-left text-xs hover:bg-destructive/10 transition-colors flex items-center gap-2 text-foreground/80 hover:text-destructive"
2057
- >
2058
- <IconTrash className="w-3.5 h-3.5 text-muted-foreground" />
2059
- {t("raw.timeline.deleteTrack")}
2060
- </button>
2061
- </div>
2062
- </div>
2063
- );
2064
- })()}
2065
- </div>
2066
- );
2067
- }