@dailies-studio/shot-editor 0.1.0 → 0.2.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 (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -1,110 +1,572 @@
1
- // The floating header: which document, what state it is in, and the transport.
1
+ // The top bar: which document, which workspace, and what state it is in.
2
2
  //
3
3
  // Everything here reads the store and writes through it. No panel holds a copy
4
4
  // of the document, so there is no second place for the open document's identity
5
5
  // to live and drift.
6
- import { useEffect } from "react";
6
+ //
7
+ // ── NO TRANSPORT UP HERE ─────────────────────────────────────────────────
8
+ //
9
+ // It used to carry a play button and a `frame / total` readout that were the
10
+ // timeline dock's, character for character (TimelineDock.tsx's `.dock-play` and
11
+ // `DockReadout`). The dock's head strip is OUTSIDE `.dock-body`, and only the
12
+ // body takes the `hidden` attribute on collapse, so both stay on screen at
13
+ // every moment the header's pair did. Two controls that cannot disagree are
14
+ // still two controls to find.
15
+ //
16
+ // What did NOT move to the dock is the playback CLOCK, which was living in that
17
+ // transport bar and is not chrome at all. See `usePlaybackClock`.
18
+ import { useEffect, useRef, type ReactNode } from "react";
19
+ import { PanelBoundary, SegmentedControl } from "@dailies-studio/ui";
20
+ import { frameToPx } from "@dailies-studio/timeline";
21
+ import { DEFAULT_SHOT_FPS } from "@dailies-studio/shot/doc";
22
+ import { defClockFrames, playbackLoops, playbackWindow } from "../../state/types.js";
23
+ import type { ShotEditorState } from "../../state/types.js";
24
+ // Re-exported where it has always been imported from; see `playbackWindow`.
25
+ export { playbackWindow };
26
+ import { refreshAgentCredentials } from "../../state/agentActions.js";
27
+ import { agentInEditor } from "../agent/agentGate.js";
7
28
  import { useShotEditor, useStore } from "../../state/context.js";
8
- import { DocSwitcher } from "./DocSwitcher.js";
29
+ import { shortcutHint } from "../../state/shortcuts.js";
30
+ import { VIEW_ONLY, exportBlocker, workspaceOf } from "../../state/workspace.js";
31
+ import { COMPONENT_ICON, DOWNLOAD_ICON, STORYBOARD_ICON, STUDIO_ICON } from "../../icons.js";
32
+ import { useCodeBufferGuard } from "../code/codeBufferGuard.js";
33
+ import { EYE_ICON } from "../comments/parts.js";
34
+ import { InboxButton } from "../comments/InboxButton.js";
35
+ import { AccountAvatar } from "./AccountAvatar.js";
36
+ import { AgentChip } from "./AgentChip.js";
37
+ import { AppMenu } from "./AppMenu.js";
9
38
  import { HistoryControls } from "./HistoryControls.js";
10
- import { StatusBadges } from "./StatusBadges.js";
11
- import { TransportBar } from "./TransportBar.js";
12
- import { ZoomControls } from "./ZoomControls.js";
39
+ import { PresenceStack } from "./PresenceStack.js";
40
+ import { ProjectTitle } from "./ProjectTitle.js";
41
+ import { SaveBadges } from "./StatusBadges.js";
42
+ import { ShareButton } from "./ShareButton.js";
43
+ import { SyncRenamedNote } from "./SyncDeletedDialog.js";
13
44
 
14
45
  export function Header() {
15
46
  const store = useStore();
16
47
  useEffect(() => {
17
- void store.refreshShots();
48
+ // The agent probe, from the header because the header is mounted for the
49
+ // whole session: the right column's Agent tab is gated on its answer, and
50
+ // a probe that ran only once the drawer opened could not answer that. One
51
+ // request per session either way, since the drawer's own effect finds the
52
+ // answer already in the store. No drawer, no probe.
53
+ if (agentInEditor()) void refreshAgentCredentials(store);
18
54
  }, [store]);
19
55
 
56
+ usePlaybackClock();
57
+
20
58
  const name = useShotEditor((s) => s.name);
59
+ // The Library is an aria-modal Dialog that starts BELOW this bar
60
+ // (styles/library.css), so the bar stays in sight but must not stay live: a
61
+ // menu opened here would paint under the dialog (--z-menu 1000 against
62
+ // --z-dialog 1100), and a click here would move focus out of its trap, where
63
+ // Escape and Tab no longer reach it. `inert` takes the bar out of pointer,
64
+ // focus and the accessibility tree together, which is what aria-modal claims.
65
+ const libraryOpen = useShotEditor((s) => s.libraryBrowserOpen);
66
+ // The role's verdict: a version preview is read only too, but not a viewer.
67
+ const readOnly = useShotEditor((s) => s.versionPreview?.roleReadOnly ?? s.readOnly);
21
68
 
22
69
  return (
23
- <header className="shell-header" data-testid="shot-header">
24
- <DocSwitcher />
25
- <HistoryControls />
26
- {name === null ? null : <TransportBar />}
27
- <div className="shell-header-spacer" />
28
- {name === null ? null : <PanelToggles />}
29
- <ZoomControls />
30
- <StatusBadges />
31
- <AgentToggle />
32
- {name !== null && <ExportButton />}
70
+ <header className="shell-header" data-testid="shot-header" inert={libraryOpen}>
71
+ {/* INSIDE the positioned container, per the boundary's placement rule:
72
+ a catch swaps the contents for the card, never the header's box. */}
73
+ {/* THREE COLUMNS, Paper's top bar:
74
+
75
+ menu │ Projects › Project › Shot │ history ··· Studio | Storyboard ··· saved Export Agent avatar
76
+
77
+ The switch sits in the MIDDLE track of a 1fr auto 1fr grid, so it is
78
+ centred on the window whatever the two ends hold.
79
+
80
+ The Agent chip is the outside agent's door (MCP health and the
81
+ connect rows, AgentChip.tsx), in a boundary of its own so a crash in
82
+ it leaves the bar standing.
83
+
84
+ WHAT IS NOT HERE. The panel toggles are on the canvas bar
85
+ (PanelToggles.tsx), the in-app Agent is the right column's tab, and
86
+ the Library opens from View > Panels and the Storyboard. Zoom and the
87
+ stage's badges are in the canvas bar too, over the picture they
88
+ describe. The monitor meter is in the timeline's meters cell. */}
89
+ <PanelBoundary label="Header">
90
+ <div className="header-start">
91
+ <AppMenu />
92
+ <span className="header-sep" aria-hidden="true" />
93
+ <ProjectTitle />
94
+ {/* "Renamed to X by Maya" (R10), for a few seconds. */}
95
+ <SyncRenamedNote />
96
+ {/* A Viewer's standing note (Paper page 21, "View only"): an edit
97
+ attempt says the same thing in a toast (ViewOnlyToast). */}
98
+ {readOnly && name !== null ? (
99
+ <span className="view-only-pill" data-testid="view-only-pill" title={VIEW_ONLY}>
100
+ {EYE_ICON}
101
+ View only
102
+ </span>
103
+ ) : null}
104
+ <span className="header-sep" aria-hidden="true" />
105
+ <HistoryControls />
106
+ </div>
107
+ <div className="header-center">{name === null ? null : <WorkspaceSwitch />}</div>
108
+ <div className="header-end">
109
+ <SaveBadges />
110
+ {/* Who else is here (Paper page 21), live sync only. */}
111
+ <PresenceStack />
112
+ <ShareButton />
113
+ {name !== null && <ExportButton />}
114
+ <AgentChip />
115
+ <InboxButton />
116
+ <AccountAvatar />
117
+ </div>
118
+ </PanelBoundary>
33
119
  </header>
34
120
  );
35
121
  }
36
122
 
37
123
  /**
38
- * Show and hide the three panels.
39
- *
40
- * IN THE HEADER, and not on the panels themselves, for one reason: a control
41
- * that lives on the thing it hides disappears with it. A chevron on the left
42
- * rail can collapse the rail and then has nowhere to be, which is how these
43
- * end up needing a second floating "reopen" affordance nobody finds. The
44
- * header never moves, so one button per panel is the whole story, and each is
45
- * `aria-pressed` so its state is readable rather than inferred from the glyph.
124
+ * Playback: WALL TIME IN, INTEGER FRAMES OUT.
125
+ *
126
+ * It never accumulates frames per tick. `frame++` per rAF makes playback speed
127
+ * a function of the monitor's refresh rate (a 120Hz laptop plays a 30fps shot
128
+ * at double speed) and makes one dropped frame permanently shift everything
129
+ * after it. Deriving the frame from elapsed time instead plays at the
130
+ * document's own rate on any display, drops frames under load rather than
131
+ * slowing down, and puts the same integer frame on screen that the export will
132
+ * render, because the seek target is `floor(elapsed * fps)` at 1x — see
133
+ * `playbackTargetFrame`, which owns that arithmetic and the shuttle's rate
134
+ * along with it.
135
+ *
136
+ * ── THE WALL TIME IS THE REFRESH'S, NOT THE CALLBACK'S ───────────────────
137
+ *
138
+ * A tick reads its time from rAF's own timestamp, the start of the refresh
139
+ * it draws for, on `performance.now()`'s timeline like `playOrigin.at`, and
140
+ * never from `performance.now()` inside the callback. The callback runs late
141
+ * by whatever the frame did before it (a median of 0.3 to 0.7 ms and up to
142
+ * 17.6 ms in the benchmark's headless Chrome), so a frame boundary that fell
143
+ * a few ms after a refresh landed before the reading on one tick and after
144
+ * it on the next: a healthy film skipped a frame and repeated the next,
145
+ * which the render scale controller read as late frames
146
+ * (renderScaleController.ts `presented`). Elapsed time is clamped at zero,
147
+ * since a refresh can begin before the origin it is the first tick of.
148
+ *
149
+ * ── AND THE BOUNDARIES BETWEEN REFRESHES ──────────────────────────────────
150
+ *
151
+ * The timestamps themselves sit 0.05 to 0.35 ms off the display's grid,
152
+ * coarsened to 0.1 ms, so a boundary on a refresh, or that near one, still
153
+ * fell either side of it from tick to tick. A frame period of p/q refreshes
154
+ * meets the refreshes at q phases R/q apart, and `placedBase` moves the
155
+ * boundaries to the middle of those gaps, R/(2q) from the nearest refresh:
156
+ * half a refresh at 60 and 30 fps on 60 Hz, a quarter at 24. A display whose
157
+ * rate drifts against the document's (the headless lane's 59.978 Hz against
158
+ * 60 fps) carries them back toward a refresh, where the wander made a run
159
+ * of skips and repeats of each crossing; so they are kept in the middle,
160
+ * and the drift costs one slip of a refresh at a time instead. The refresh
161
+ * is measured over a run's latest ticks (`refreshOf`) and kept for the next
162
+ * run, so a page's first run places at its `REFRESH_STAMPS`th tick and every
163
+ * later run at its second, once its first interval agrees with the refresh
164
+ * kept (the window may be on another display). The base stays within half a
165
+ * refresh of the origin the sound counts from too (`playOrigin`), and the
166
+ * audio's drift check allows the picture its refresh (audio/preview.ts).
167
+ *
168
+ * IN THE HEADER, which renders none of it. The loop needs a mount that lasts
169
+ * the whole session: it used to ride the header's transport bar, and the
170
+ * obvious home for it now would be the timeline dock, whose play button is the
171
+ * one that starts it. That dock is collapsible, and a clock that unmounts with
172
+ * a panel is a play button that stops working.
173
+ *
174
+ * The window, the fps and the shuttle rate are subscribed as NUMBERS, never as
175
+ * `doc`. This runs inside the header, and a `doc` subscription would re-render
176
+ * every control up there on each keystroke of an edit.
46
177
  */
47
- function PanelToggles() {
48
- const store = useStore();
49
- const rail = useShotEditor((s) => s.railCollapsed);
50
- const inspector = useShotEditor((s) => s.inspectorCollapsed);
51
- const dock = useShotEditor((s) => s.dockCollapsed);
52
- const rows: ReadonlyArray<{ on: boolean; glyph: string; label: string; key: string; run: () => void }> = [
53
- { on: !rail, glyph: "▙", label: "Left panel", key: "[", run: () => store.setRailCollapsed(!rail) },
54
- { on: !dock, glyph: "▄", label: "Timeline", key: "", run: () => store.setDockCollapsed(!dock) },
55
- {
56
- on: !inspector,
57
- glyph: "▟",
58
- label: "Right panel",
59
- key: "]",
60
- run: () => store.setInspectorCollapsed(!inspector),
61
- },
62
- ];
178
+ /**
179
+ * The frame a tick should land on: wall time in, integer frame out, wrapped
180
+ * into the window being played.
181
+ *
182
+ * Extracted from the rAF loop so the arithmetic can be tested without a clock.
183
+ * The loop's own half, which instant a tick reads and where its boundaries
184
+ * sit, is held by header.test.tsx through a fake `requestAnimationFrame`.
185
+ *
186
+ * ── FLOOR, NOT ROUND ─────────────────────────────────────────────────────
187
+ *
188
+ * `Math.floor` is what the shipped player (`player/src/stage.ts`) and the
189
+ * legacy editor both use, and this loop was the only transport in the tree
190
+ * rounding instead. Rounding shows frame N from half a frame BEFORE frame N's
191
+ * instant, so the preview ran a consistent half-frame ahead of the same
192
+ * document's export — small, invisible in isolation, and exactly the kind of
193
+ * disagreement that costs an afternoon when someone finally A/Bs a preview
194
+ * against an MP4. Every frame it draws was always exact; only which instant
195
+ * chose it has changed.
196
+ *
197
+ * ── OFFSET-FORM WRAP ─────────────────────────────────────────────────────
198
+ *
199
+ * A bare `target % total` is right only for a window that begins at 0: inside a
200
+ * shot at [90, 120) it wraps frame 95 to 5, which is a different shot — and
201
+ * `seek`'s clamp would then pin the playhead on frame 90 forever, so playback
202
+ * would look frozen rather than wrong. The same form is what lets a REVERSE
203
+ * rate wrap off the front of the window onto its last frame instead of running
204
+ * to a negative one the clamp would freeze on.
205
+ *
206
+ * ── AND THE START HAS TO BE IN THE WINDOW FOR THAT FORM TO MEAN ANYTHING ─
207
+ *
208
+ * The wrap is a modulus, so it answers for ANY start frame — including one
209
+ * outside the window entirely, which is the ordinary case for a WORK AREA: the
210
+ * bracket is set with ⌥I/⌥O and the playhead is then free to wander off it. Hit
211
+ * play with the playhead at frame 10 and a bracket of [100, 140) and the first
212
+ * tick landed on `100 + ((10 - 100) mod 40)` = frame 130, which is an arbitrary
213
+ * frame three quarters of the way through the loop. It looked like the
214
+ * transport had jumped somewhere random, because it had.
215
+ *
216
+ * So a start OUTSIDE the window becomes the window's IN POINT, which is what
217
+ * pressing play on a bracket means in every editor that has one. A start
218
+ * already inside is untouched, so the shot and definition arms — whose windows
219
+ * always contain the clock they are driving — are unchanged frame for frame.
220
+ *
221
+ * ── THE RATE IS APPLIED TO THE MAGNITUDE, THEN THE SIGN ──────────────────
222
+ *
223
+ * `floor(elapsed * fps * rate)` would be wrong in reverse and silently so:
224
+ * flooring a negative product is `-ceil(|product|)`, which steps back a frame
225
+ * the instant the clock moves at all, so reverse would run a consistent frame
226
+ * AHEAD of forward over the same wall time. Flooring the magnitude and then
227
+ * applying the sign keeps every frame shown from its own instant in both
228
+ * directions, which is the property the floor was chosen for.
229
+ *
230
+ * The default of 1 is not politeness: every caller that is not the shuttle —
231
+ * and every existing test — means 1x, and a required argument would let a new
232
+ * one guess.
233
+ *
234
+ * ── WITH THE LOOP OFF (`loop: false`, the Project context's Loop button) ──
235
+ *
236
+ * The target is CLAMPED to the window instead of wrapped, and the clock stops
237
+ * once it reads the far edge (`playbackEndFrame`). A start already ON that far
238
+ * edge plays again from the opposite one, which is what Play means on a
239
+ * transport that stopped at the end.
240
+ */
241
+ export function playbackTargetFrame(
242
+ startedFrame: number,
243
+ windowStart: number,
244
+ windowFrames: number,
245
+ fps: number,
246
+ elapsedSeconds: number,
247
+ rate: number = 1,
248
+ loop: boolean = true,
249
+ ): number {
250
+ const steps = Math.floor(elapsedSeconds * fps * Math.abs(rate));
251
+ const last = windowStart + windowFrames - 1;
252
+ // See the header: outside the window, playback begins at the in point.
253
+ let from =
254
+ startedFrame >= windowStart && startedFrame < windowStart + windowFrames
255
+ ? startedFrame
256
+ : windowStart;
257
+ if (!loop && from === playbackEndFrame(windowStart, windowFrames, rate)) {
258
+ from = rate < 0 ? last : windowStart;
259
+ }
260
+ const target = from + (rate < 0 ? -steps : steps);
261
+ if (!loop) return Math.min(last, Math.max(windowStart, target));
63
262
  return (
64
- <div className="panel-toggles" role="group" aria-label="Panels">
65
- {rows.map(({ on, glyph, label, key, run }) => (
66
- <button
67
- key={label}
68
- type="button"
69
- className={`btn btn-icon${on ? " is-active" : ""}`}
70
- aria-pressed={on}
71
- title={`${on ? "Hide" : "Show"} ${label.toLowerCase()}${key === "" ? "" : ` (${key})`}`}
72
- onClick={run}
73
- >
74
- {glyph}
75
- </button>
76
- ))}
77
- <button
78
- type="button"
79
- className="btn btn-icon"
80
- title="Hide every panel (.)"
81
- onClick={() => store.togglePanels()}
82
- >
83
- ⛶
84
- </button>
85
- </div>
263
+ windowStart +
264
+ ((((target - windowStart) % windowFrames) + windowFrames) % windowFrames)
86
265
  );
87
266
  }
88
267
 
89
- /** Opens the drawer, which REPLACES the inspector rather than sitting beside
90
- * it. One right rail: the shell grid has one column there, and a second
91
- * floating panel over a 3D stage is a panel that covers the thing being
92
- * discussed. */
93
- function AgentToggle() {
268
+ /** The frame a non-looping run stops on: the window's last frame forward, its
269
+ * first in reverse. */
270
+ export function playbackEndFrame(
271
+ windowStart: number,
272
+ windowFrames: number,
273
+ rate: number = 1,
274
+ ): number {
275
+ return rate < 0 ? windowStart : windowStart + windowFrames - 1;
276
+ }
277
+
278
+ /** The latest tick timestamps of a run `refreshOf` measures over. */
279
+ export const REFRESH_STAMPS = 16;
280
+ /** An interval between two ticks this long, ms, is a stall or a hidden tab,
281
+ * and measuring starts again after it. */
282
+ const REFRESH_GAP_MS = 100;
283
+ /** How near a run's own interval must come to the refresh an earlier run
284
+ * measured, as a share of it, before that refresh places this run's
285
+ * boundaries: the window may be on another display now. */
286
+ const REFRESH_AGREES = 0.05;
287
+ /** The largest q `boundaryGap` looks for in a frame period of p/q refreshes:
288
+ * past it the gaps are too narrow for the timestamps' wander. */
289
+ const PATTERN_MAX = 5;
290
+ /** How far the measured ratio may sit from p/q, as a share of it, and still
291
+ * be that pattern: room for a rate a thousandth off its nominal (29.97 fps,
292
+ * a 59.94 Hz display) and for `refreshOf`'s own error. */
293
+ const PATTERN_TOLERANCE = 0.003;
294
+ /** The narrowest gap, as a share of the refresh, whose boundaries are kept
295
+ * clear as the display drifts (q up to 2): a narrower gap's quarter is
296
+ * within the timestamps' wander, and a ratio the estimate took for such a
297
+ * pattern may be none (25 fps on 144 Hz, 5.76 refreshes, is within 0.2% of
298
+ * 23/4), and keeping it clear would slip it again and again. */
299
+ const KEPT_GAP = 0.5;
300
+
301
+ /**
302
+ * The display's refresh interval, ms, from a run's latest tick timestamps:
303
+ * their span over the whole number of refreshes in it, counted with the
304
+ * median interval (one refresh, unless the main thread missed most). Null
305
+ * until there are `REFRESH_STAMPS` of them: one interval carries a
306
+ * timestamp's wander, 2% of a 165 Hz refresh, where fifteen carry a
307
+ * fifteenth of it.
308
+ */
309
+ export function refreshOf(stamps: readonly number[]): number | null {
310
+ if (stamps.length < REFRESH_STAMPS) return null;
311
+ const intervals = stamps.slice(1).map((at, i) => at - stamps[i]!).sort((a, b) => a - b);
312
+ const span = stamps[stamps.length - 1]! - stamps[0]!;
313
+ return span / Math.round(span / intervals[intervals.length >> 1]!);
314
+ }
315
+
316
+ /** The gap, ms, between the phases at which the boundaries of a frame period
317
+ * of p/q refreshes meet the refreshes, R/q, or null for a ratio that is no
318
+ * such fraction with q up to `PATTERN_MAX`: its boundaries take every phase
319
+ * in turn, and no shift keeps them clear. */
320
+ export function boundaryGap(refreshMs: number, periodMs: number): number | null {
321
+ const ratio = periodMs / refreshMs;
322
+ for (let q = 1; q <= PATTERN_MAX; q += 1) {
323
+ if (Math.abs(ratio - Math.round(ratio * q) / q) <= PATTERN_TOLERANCE * ratio) return refreshMs / q;
324
+ }
325
+ return null;
326
+ }
327
+
328
+ const mod = (value: number, by: number): number => ((value % by) + by) % by;
329
+
330
+ /**
331
+ * Where a run's frame boundaries sit after a tick at `tickAt`, a refresh:
332
+ * the instant the run counts its frames from, `base`, null until placed.
333
+ *
334
+ * Placed in the middle of the gaps (`boundaryGap`), R/(2q) from the nearest
335
+ * refresh, and kept there for q up to 2: a display whose rate drifts against
336
+ * the document's carries them back toward a refresh, so once one comes
337
+ * within a quarter of a gap they go back to the middle. Neither move takes a
338
+ * boundary across a refresh, so neither changes a frame on screen. The base
339
+ * stays within half a refresh of the origin: when the drift has used that
340
+ * up, the move goes the other way and the boundaries slip a whole refresh,
341
+ * once, which is the two rates' own skip.
342
+ */
343
+ export function placedBase(base: number | null, originAt: number, tickAt: number, refreshMs: number, periodMs: number): number | null {
344
+ const gap = boundaryGap(refreshMs, periodMs);
345
+ if (gap === null) return base;
346
+ const from = base ?? originAt;
347
+ // From the boundary nearest the tick, so the refresh estimate's error is
348
+ // not multiplied by the run's length.
349
+ const nearest = from + periodMs * Math.round((tickAt - from) / periodMs);
350
+ let centre = mod(gap / 2 - mod(nearest - tickAt, gap), gap);
351
+ if (centre > gap / 2) centre -= gap;
352
+ if (base !== null && (gap < KEPT_GAP * refreshMs || Math.abs(centre) <= gap / 4)) return base;
353
+ const next = from + centre;
354
+ return next - Math.round((next - originAt) / refreshMs) * refreshMs;
355
+ }
356
+
357
+ /**
358
+ * THE WORK AREA, DRAWN. The bracket over the ruler that says what a press of
359
+ * Space will actually loop.
360
+ *
361
+ * ── IT HAD NO REPRESENTATION AT ALL ──────────────────────────────────────
362
+ *
363
+ * ⌥I / ⌥O / ⌥X wrote `workArea`, `playbackWindow` read it, the shortcuts manual
364
+ * described it, and NOTHING on screen showed it. So the only evidence an author
365
+ * had that they had bracketed anything was that playback stopped going where
366
+ * they expected, which reads as a broken transport rather than as a feature
367
+ * they turned on — and ⌥X, the way out, is only findable if you already know
368
+ * the thing exists.
369
+ *
370
+ * ── IN THIS FILE BECAUSE THE NUMBERS ARE ─────────────────────────────────
371
+ *
372
+ * It draws `playbackWindow(s)` and not `s.workArea`. Those differ: the window
373
+ * is CLAMPED to the document on every read (a trim can shorten the film under
374
+ * a bracket nobody touched) and falls back to the whole film when the clamp
375
+ * leaves nothing. A bracket painted from the raw field would be a bracket the
376
+ * transport does not honour, which is worse than none — so the two come off the
377
+ * one function, in the one file, and cannot disagree. The dock mounts it into
378
+ * its ruler; see `TimelineDock`.
379
+ *
380
+ * WITHHELD OUTSIDE THE SCENE CONTEXT, the same rule `playbackWindow` and
381
+ * `setWorkAreaEdge` both keep: an entered shot and a component definition each
382
+ * own the transport's window already, so a bracket visible in either would name
383
+ * a loop nothing is running.
384
+ */
385
+ export function WorkAreaBracket(props: { ppf: number }) {
386
+ const area = useShotEditor((s) => s.workArea);
387
+ const owned = useShotEditor((s) => defClockFrames(s) !== null || s.enteredScene !== null);
388
+ const start = useShotEditor((s) => playbackWindow(s).start);
389
+ const frames = useShotEditor((s) => playbackWindow(s).frames);
390
+ if (area === null || owned || frames <= 0) return null;
391
+ return (
392
+ <span
393
+ className="dock-work-area"
394
+ data-testid="work-area-bracket"
395
+ aria-hidden="true"
396
+ style={{ left: frameToPx(start, props.ppf), width: frameToPx(frames, props.ppf) }}
397
+ />
398
+ );
399
+ }
400
+
401
+ function usePlaybackClock(): void {
94
402
  const store = useStore();
95
- const open = useShotEditor((s) => s.agentOpen);
96
- const running = useShotEditor((s) => s.agentRunning);
403
+ const playing = useShotEditor((s) => s.playing);
404
+ // The COMPONENT CONTEXT swaps the whole triple (unify P2): total, the seek
405
+ // verb and the starting frame become def-local together, so the same loop
406
+ // wraps a definition's own duration and never touches master time — the
407
+ // scene render, the audio preview and every playhead-paired verb keep the
408
+ // master frame they were on.
409
+ // EITHER SUBJECT, one bit: a definition and a code component both put the
410
+ // stage in the component context, so both swap the triple. `defClockFrames`
411
+ // is the one answer, and it is what `seekDefTo` clamps on too.
412
+ const inDef = useShotEditor((s) => defClockFrames(s) !== null);
413
+ // ── AND THE WINDOW IT LOOPS INSIDE ────────────────────────────────────
414
+ //
415
+ // Two NUMBERS, never `doc` — this file's own rule, and the reason is that a
416
+ // `doc` subscription here re-renders every control in the header on each
417
+ // keystroke of an edit. Both come off ONE function (`playbackWindow`), so a
418
+ // start and a length can never be read from two different subjects, and both
419
+ // are in the effect's deps, so a trim of the entered shot or a moved work
420
+ // area restarts the loop against the window it actually has now.
421
+ const windowStart = useShotEditor((s) => playbackWindow(s).start);
422
+ const total = useShotEditor((s) => playbackWindow(s).frames);
423
+ const fps = useShotEditor((s) =>
424
+ s.doc === null
425
+ ? DEFAULT_SHOT_FPS.num / DEFAULT_SHOT_FPS.den
426
+ : s.doc.fps.num / s.doc.fps.den,
427
+ );
428
+ // THE SHUTTLE'S SPEED, read here and nowhere else in the picture lane: the
429
+ // stage is seeked frame by frame either way, so a rate never reaches it and
430
+ // the bridge needs no new message. `setPlaying(true)` resets this to 1, so
431
+ // the loop that a Space bar starts is always 1x.
432
+ const rate = useShotEditor((s) => s.playRate);
433
+ // WRAP OR STOP at the window's end. Always a wrap inside a shot or a
434
+ // definition; the Project context follows the dock's Loop button.
435
+ const loop = useShotEditor(playbackLoops);
436
+ // ── ONE CLOCK FOR PICTURE AND SOUND ────────────────────────────────────
437
+ //
438
+ // `playOrigin` is where and when the transport started, set by the same
439
+ // write that sets `playing`. The audio session anchors on it too
440
+ // (audio/preview.ts), so the loop starts from it rather than from this
441
+ // effect, which runs a React commit later, its boundaries placed at most
442
+ // half a refresh off it. ONCE per origin: a restart for the window, the
443
+ // rate or the loop anchors on its own reading, as before.
444
+ const origin = useShotEditor((s) => s.playOrigin);
445
+ const startedFrom = useRef<ShotEditorState["playOrigin"]>(null);
446
+ // The display's refresh as the last run measured it (`refreshOf`), so a
447
+ // run after the first places its boundaries at its second tick, once its
448
+ // first interval agrees.
449
+ const refresh = useRef<number | null>(null);
450
+
451
+ useEffect(() => {
452
+ // Taken even by a run that cannot start (an empty window), which would
453
+ // find it stale by the time one could.
454
+ const fresh = origin === startedFrom.current ? null : origin;
455
+ startedFrom.current = origin;
456
+ if (!playing || total <= 0) return;
457
+ const startedAt = fresh?.at ?? performance.now();
458
+ const startedFrame = fresh?.frame ?? (inDef ? store.getState().defFrame : store.getState().frame);
459
+ const periodMs = 1000 / (fps * Math.abs(rate));
460
+ let raf = 0;
461
+ const stamps: number[] = [];
462
+ // The instant the run counts from: the origin's own until its boundaries
463
+ // are placed (`placedBase`), at its first tick with a refresh it trusts.
464
+ let base: number | null = null;
465
+ // An earlier run's refresh places this one's boundaries once an interval
466
+ // of this run agrees with it (`REFRESH_AGREES`). Placed on the grid of
467
+ // the display the window was on (60 Hz, a run now on 144 Hz), a base
468
+ // would move by more than a tick, and the playhead step back a frame.
469
+ let trusted = false;
470
+ const tick = (now: number): void => {
471
+ if (stamps.length > 0 && now - stamps[stamps.length - 1]! >= REFRESH_GAP_MS) stamps.length = 0;
472
+ stamps.push(now);
473
+ if (stamps.length > REFRESH_STAMPS) stamps.shift();
474
+ const measured = refreshOf(stamps);
475
+ if (measured !== null) refresh.current = measured;
476
+ const interval = stamps.length > 1 ? now - stamps[stamps.length - 2]! : null;
477
+ trusted ||=
478
+ measured !== null ||
479
+ (interval !== null && refresh.current !== null && Math.abs(interval - refresh.current) <= REFRESH_AGREES * refresh.current);
480
+ if (trusted && refresh.current !== null) base = placedBase(base, startedAt, now, refresh.current, periodMs);
481
+ const elapsed = Math.max(0, now - (base ?? startedAt)) / 1000;
482
+ const wrapped = playbackTargetFrame(
483
+ startedFrame,
484
+ windowStart,
485
+ total,
486
+ fps,
487
+ elapsed,
488
+ rate,
489
+ loop,
490
+ );
491
+ if (inDef) store.seekDef(wrapped);
492
+ else store.seek(wrapped);
493
+ // The end of a run that does not loop: hold the last frame and stop,
494
+ // through `setPlaying` like every other pause.
495
+ if (!loop && wrapped === playbackEndFrame(windowStart, total, rate)) {
496
+ store.setPlaying(false);
497
+ return;
498
+ }
499
+ raf = requestAnimationFrame(tick);
500
+ };
501
+ raf = requestAnimationFrame(tick);
502
+ return () => cancelAnimationFrame(raf);
503
+ }, [playing, origin, total, fps, store, inDef, windowStart, rate, loop]);
504
+ }
505
+
506
+ /** Studio | Storyboard | Components, the workspaces a Project has, centred in
507
+ * the bar.
508
+ *
509
+ * Storyboard is never disabled: from any mode it HANDS OVER
510
+ * (`setStoryboardOpen` closes the material nodes, then leaves Components,
511
+ * each through the exit that records or restores its work), and the one
512
+ * thing that hand-over could lose, Code Studio's unsaved buffers, is asked
513
+ * about first. Components is disabled WITH THE REASON under the material
514
+ * nodes. Studio is always open: it is where every other mode returns to.
515
+ * Primitive subscriptions rather than one derived object, the shell's
516
+ * rule. */
517
+ function WorkspaceSwitch() {
518
+ const store = useStore();
519
+ const open = useShotEditor((s) => s.storyboardOpen);
520
+ const materialWorkspace = useShotEditor((s) => s.materialWorkspace);
521
+ const inComponents = useShotEditor((s) => workspaceOf(s) === "components");
522
+ const componentsBlocker = materialWorkspace === null ? null : "Close the material nodes before opening Components.";
523
+ // Leaving Components drops a code component's buffers, so unsaved text
524
+ // there asks.
525
+ const { guard, dialog } = useCodeBufferGuard();
526
+ const label = (glyph: ReactNode, word: string): ReactNode => (
527
+ <>
528
+ <span className="header-glyph" aria-hidden="true">
529
+ {glyph}
530
+ </span>
531
+ {word}
532
+ </>
533
+ );
97
534
  return (
98
- <button
99
- type="button"
100
- className={`btn${open ? " is-active" : ""}`}
101
- aria-label="Agent"
102
- aria-pressed={open}
103
- title="Agent — describe the scene you want"
104
- onClick={() => store.setAgentOpen(!open)}
105
- >
106
- {running ? "Agent…" : "Agent"}
107
- </button>
535
+ <>
536
+ {dialog}
537
+ <SegmentedControl
538
+ ariaLabel="Workspace"
539
+ className="is-accent workspace-switch"
540
+ value={open ? "storyboard" : inComponents ? "components" : "studio"}
541
+ options={[
542
+ { value: "studio", label: label(STUDIO_ICON, "Studio"), testId: "workspace-studio" },
543
+ {
544
+ value: "storyboard",
545
+ label: label(STORYBOARD_ICON, "Storyboard"),
546
+ title: `Storyboard${shortcutHint("ctx.storyboard")}`,
547
+ testId: "workspace-storyboard",
548
+ },
549
+ {
550
+ value: "components",
551
+ label: label(COMPONENT_ICON, "Components"),
552
+ title: componentsBlocker ?? "Components",
553
+ disabled: componentsBlocker !== null,
554
+ testId: "workspace-components",
555
+ },
556
+ ]}
557
+ onChange={(next) => {
558
+ // The leave sits INSIDE the guard, so Discard drops the buffers
559
+ // before the store's own unsaved-code check runs.
560
+ const toBoard = (): void => {
561
+ store.leaveComponents();
562
+ store.setStoryboardOpen(true);
563
+ };
564
+ if (next === "storyboard") guard(toBoard);
565
+ else if (next === "components") guard(() => void store.openComponents());
566
+ else guard(() => (inComponents ? store.leaveComponents("stage") : store.setStoryboardOpen(false)));
567
+ }}
568
+ />
569
+ </>
108
570
  );
109
571
  }
110
572
 
@@ -115,17 +577,35 @@ function AgentToggle() {
115
577
  * runs, so a button that forgot would be the only place the progress lived. */
116
578
  function ExportButton() {
117
579
  const store = useStore();
118
- const job = useShotEditor((s) => s.renderJob);
119
- const running = job?.phase === "running";
120
- const pct = running && job.total > 0 ? Math.round((job.done / job.total) * 100) : null;
580
+ // THE LOCK, not the server job: an in-browser export is just as much a
581
+ // render in progress, and the header used to show nothing at all for one.
582
+ const exportLock = useShotEditor((s) => s.exportLock);
583
+ const running = exportLock !== null;
584
+ const blocked = useShotEditor(exportBlocker);
585
+ const pct =
586
+ running && exportLock.total > 0
587
+ ? Math.round((exportLock.done / exportLock.total) * 100)
588
+ : null;
121
589
  return (
122
590
  <button
123
591
  type="button"
124
592
  className={`btn is-primary header-export${running ? " is-running" : ""}`}
125
593
  aria-label="Export"
126
- title="Export (⌘E)"
594
+ // A running export stays reachable: the button is its progress.
595
+ disabled={!running && blocked !== null}
596
+ title={!running && blocked !== null ? blocked : `Export${shortcutHint("file.export")}`}
597
+ // The render's own progress, painted into the button that started it, so
598
+ // a closed export dialog is not the only place the number lived. Two
599
+ // stops off the SAME accent: still one primary control in one colour,
600
+ // not a second signal hue competing with it.
601
+ {...(pct === null
602
+ ? {}
603
+ : { style: { "--export-pct": pct } as React.CSSProperties })}
127
604
  onClick={() => store.setExportOpen(true)}
128
605
  >
606
+ <span className="header-glyph" aria-hidden="true">
607
+ {DOWNLOAD_ICON}
608
+ </span>
129
609
  {pct === null ? "Export" : `Exporting ${pct}%`}
130
610
  </button>
131
611
  );