@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
@@ -0,0 +1,272 @@
1
+ // The workspace layout — three panel sizes and three collapse flags —
2
+ // remembered across reloads.
3
+ //
4
+ // `workspacePrefs` in the store already carries these six across a DOCUMENT
5
+ // switch, which is the right instinct applied to the wrong boundary: the
6
+ // layout is a fact about the person and their screen, not about the film they
7
+ // happen to have open. An author who dragged the dock tall to read a curve had
8
+ // it back at 300 the moment they refreshed the tab, which is why the reach for
9
+ // the handle became a habit rather than a decision.
10
+ //
11
+ // ONE KEY HOLDING ONE OBJECT, not six keys. The six move together — every one
12
+ // of them is answering "how much of this window is chrome" — so there is no
13
+ // case where half of them should be restored and the other half not, and a
14
+ // single `setItem` is also the whole of the write cost this module adds.
15
+ //
16
+ // Guarded on both sides, like `editorPrefs`: private mode and a locked-down
17
+ // storage policy each throw on access, and a preference is never worth an
18
+ // exception. A read that fails answers `{}`, which is "no opinion" — `bootLayout`
19
+ // then fits the window, which is the state the chrome had before this existed.
20
+ //
21
+ // ── A SIZE THIS WINDOW CANNOT HONOUR IS SUSPENDED, NOT OVERWRITTEN ────────
22
+ //
23
+ // `bootLayout` puts a saved size through the SETTERS so the bounds this session
24
+ // enforces win over whatever storage remembers: a 600px dock dragged on a
25
+ // desktop arrives on a 700px laptop as 490, and that is right — a dock with no
26
+ // stage behind it is worse than a short dock. But the store was then the only
27
+ // copy of that number, so the next thing the author moved wrote 490 back as if
28
+ // they had chosen it, and reconnecting the monitor did not bring the 600 back.
29
+ // A size suspended for one session had become a size deleted.
30
+ //
31
+ // So the RECORD and the LIVE VALUE are allowed to disagree. A size this boot
32
+ // could not apply as written is held aside and re-written verbatim on every
33
+ // save, until the author drags that panel — at which point the number on screen
34
+ // is theirs, about this window, and the held one is dropped for good. Nothing
35
+ // else moves: the session still runs at the clamped size, `authorLayout` still
36
+ // keeps the window's own fitted numbers out of the record, and focus mode's
37
+ // collapse is still not a preference.
38
+ //
39
+ // That buys the module its one ORDERING rule. `persistLayoutPrefs` reads the
40
+ // record itself and compares it against the store, so it has to be called AFTER
41
+ // `bootLayout` (main.tsx does, and says why). Subscribe first and the boot's own
42
+ // application of the record is indistinguishable from a drag — see the test.
43
+ import { layoutFor } from "./panelSize.js";
44
+ import type { BootLayoutPrefs } from "./panelSize.js";
45
+ import type { ShotEditorStore } from "./store.js";
46
+
47
+ /** The one key, exported so the suite asserts against THIS string rather than
48
+ * a second copy of it. A test that retypes the name still passes when the
49
+ * name here changes, and a renamed key silently forgets every saved layout. */
50
+ export const WORKSPACE_PREFS_KEY = "dailies.workspace";
51
+
52
+ const SIZE_FIELDS = ["railWidth", "inspectorWidth", "dockHeight"] as const;
53
+ const COLLAPSE_FIELDS = [
54
+ "railCollapsed",
55
+ "inspectorCollapsed",
56
+ "dockCollapsed",
57
+ ] as const;
58
+
59
+ type SizeField = (typeof SIZE_FIELDS)[number];
60
+
61
+ /** A size the author chose (`chosen`) and the one this boot left in its place
62
+ * (`boot`); see `suspendedSizes`. */
63
+ interface SuspendedSize {
64
+ chosen: number;
65
+ boot: number;
66
+ }
67
+
68
+ /**
69
+ * The saved layout, validated FIELD BY FIELD rather than object by object.
70
+ *
71
+ * A whole-object check would make one bad number throw away a good one, and
72
+ * the object is exactly the shape most likely to go half-bad: it is written by
73
+ * an older build, hand-edited in devtools, or truncated by a quota error
74
+ * mid-write. `bootLayout` already treats every field as independently absent,
75
+ * so an unusable `dockHeight` is best expressed as a missing one — the author
76
+ * keeps the rail they dragged and gets a fitted dock.
77
+ *
78
+ * Numbers must be FINITE: `JSON.parse` cannot produce a NaN, but it happily
79
+ * produces `1e400` as `Infinity`, and an infinite width reaches `clampSize`,
80
+ * which answers `min` — silently the wrong panel rather than no opinion.
81
+ */
82
+ export function readLayoutPrefs(): BootLayoutPrefs {
83
+ let parsed: unknown;
84
+ try {
85
+ const text = localStorage.getItem(WORKSPACE_PREFS_KEY);
86
+ if (text === null) return {};
87
+ parsed = JSON.parse(text);
88
+ } catch {
89
+ return {};
90
+ }
91
+ if (typeof parsed !== "object" || parsed === null) return {};
92
+ const saved = parsed as Record<string, unknown>;
93
+ const prefs: BootLayoutPrefs = {};
94
+ for (const field of SIZE_FIELDS) {
95
+ const value = saved[field];
96
+ if (typeof value === "number" && Number.isFinite(value))
97
+ prefs[field] = value;
98
+ }
99
+ for (const field of COLLAPSE_FIELDS) {
100
+ const value = saved[field];
101
+ if (typeof value === "boolean") prefs[field] = value;
102
+ }
103
+ return prefs;
104
+ }
105
+
106
+ export function writeLayoutPrefs(prefs: BootLayoutPrefs): void {
107
+ try {
108
+ localStorage.setItem(WORKSPACE_PREFS_KEY, JSON.stringify(prefs));
109
+ } catch {
110
+ /* storage disabled or full — the session still honours the drag, it just forgets */
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The layout as the AUTHOR has it, which is not the layout the store has.
116
+ *
117
+ * Two things in the state look like a preference and are not, and saving
118
+ * either hands the next session a decision nobody made.
119
+ *
120
+ * A FITTED SIZE IS THE WINDOW'S OPINION. Seeding the signature after
121
+ * `bootLayout` only defers that: `App`'s resize refit puts the NEW fitted
122
+ * numbers through the very setters a drag uses, so docking a 1366 laptop to a
123
+ * 2560 monitor would save 232/288/300, and the next boot on the laptop would
124
+ * prefer that record over the fit — permanently, because 232 is no longer the
125
+ * fitted number and refit reads it as a panel the author dragged. The test
126
+ * here is the one refit already uses: a panel holding exactly `layoutFor`'s
127
+ * number is a panel nobody has dragged. A drag that lands on the fitted value
128
+ * to the pixel is indistinguishable from no drag, and being re-fitted next
129
+ * time is the right answer for it anyway.
130
+ *
131
+ * A COLLAPSED PANEL MAY BE FOCUS MODE'S. Video at the scene level collapses
132
+ * both side rails and records what it found in `focusRestore*`; the collapse
133
+ * verbs null that slot the moment the author takes ownership. So a non-null
134
+ * slot reads exactly "this bit is the mode's, and here is the author's", the
135
+ * same rule `workspacePrefs` carries across a document switch. Without it a
136
+ * reload taken in Video comes back with those rails collapsed and no pending
137
+ * restore to bring them back: the mode's opinion promoted to a preference.
138
+ *
139
+ * THE DOCK HAS NO SLOT, because no view collapses the timeline. A collapsed
140
+ * dock is always the author's, so it is recorded as it stands.
141
+ *
142
+ * A SUSPENDED SIZE IS STILL THE AUTHOR'S. `held` carries the sizes this boot
143
+ * clamped (see `suspendedSizes`); while a panel is untouched they answer for it
144
+ * instead of the store, so the record keeps the number the author picked rather
145
+ * than the one this window allowed. This function is where the suspension is
146
+ * RELEASED as well as read — it is the only place that sees every notification
147
+ * with the store beside it, and a `held` entry outliving its panel's first drag
148
+ * would pin the record to a number the author has since replaced.
149
+ */
150
+ function authorLayout(
151
+ store: ShotEditorStore,
152
+ held: Map<SizeField, SuspendedSize>,
153
+ ): BootLayoutPrefs {
154
+ const s = store.getState();
155
+ const prefs: BootLayoutPrefs = {
156
+ railCollapsed: s.focusRestoreRail ?? s.railCollapsed,
157
+ inspectorCollapsed: s.focusRestoreInspector ?? s.inspectorCollapsed,
158
+ dockCollapsed: s.dockCollapsed,
159
+ };
160
+ // No window is no fit to compare against, so nothing can be ruled out as
161
+ // the window's doing — same fallback posture as `setDockHeight`.
162
+ const fitted =
163
+ typeof window === "undefined"
164
+ ? null
165
+ : layoutFor(window.innerWidth, window.innerHeight);
166
+ for (const field of SIZE_FIELDS) {
167
+ const suspended = held.get(field);
168
+ if (suspended !== undefined) {
169
+ // Still exactly what the boot left it at, so the author has said nothing
170
+ // about this panel since: the record keeps the size they last chose.
171
+ if (s[field] === suspended.boot) {
172
+ prefs[field] = suspended.chosen;
173
+ continue;
174
+ }
175
+ // They have dragged it. Permanently — a later drag back onto the boot
176
+ // value is a statement about THIS window and belongs in the record.
177
+ held.delete(field);
178
+ }
179
+ if (fitted === null || s[field] !== fitted[field]) prefs[field] = s[field];
180
+ }
181
+ return prefs;
182
+ }
183
+
184
+ /**
185
+ * The sizes the boot did not apply as written, kept out of the record's way.
186
+ *
187
+ * Read at subscribe time, which is the one moment the record on disk and the
188
+ * store agree about what the author asked for: `bootLayout` has just put each
189
+ * saved size through the setters, so a field the store does not hold verbatim
190
+ * is a field this session refused — the dock past `dockCeiling` on a shorter
191
+ * window, or any size a build with different bounds once allowed.
192
+ *
193
+ * The refit gets no exception. It only touches a panel still holding the number
194
+ * `layoutFor` gave it, and a clamp lands on a BOUND: 168/460 for the rail,
195
+ * 264/480 for the inspector, neither of which `layoutFor` returns. The dock is
196
+ * the one that can coincide: under ~536px of window its fit is also its 120
197
+ * floor (`layoutFor` stays under the footer's ceiling), so a sub-minimum record
198
+ * clamps onto the fitted number, and under ~472px the boot's own ceiling is that
199
+ * number too, so any record does. On a window that short the
200
+ * refit is what releases the suspension and the record falls back to fitted.
201
+ * That is a viewport with no editor left in it, and the price is one forgotten
202
+ * size, so it is named here rather than coded around.
203
+ *
204
+ * ONE READ, no argument. `main.tsx` already has the record in hand, but adding
205
+ * a parameter would let a caller pass a record that is not the one the boot
206
+ * saw, which is the only input that could make this lie. A second `getItem` at
207
+ * startup is cheaper than that.
208
+ */
209
+ function suspendedSizes(store: ShotEditorStore): Map<SizeField, SuspendedSize> {
210
+ const s = store.getState();
211
+ const saved = readLayoutPrefs();
212
+ const held = new Map<SizeField, SuspendedSize>();
213
+ for (const field of SIZE_FIELDS) {
214
+ const chosen = saved[field];
215
+ if (chosen !== undefined && chosen !== s[field])
216
+ held.set(field, { chosen, boot: s[field] });
217
+ }
218
+ return held;
219
+ }
220
+
221
+ /**
222
+ * Keep storage in step with the store for the life of the page.
223
+ *
224
+ * ── WHY THE SIGNATURE AND NOT JUST THE SUBSCRIPTION ───────────────────────
225
+ *
226
+ * `subscribe` is the raw channel: it fires on EVERY observable change, and
227
+ * during playback that is once per frame for the transport tick alone. A
228
+ * `setItem` per frame is a synchronous serialise-and-store sixty times a
229
+ * second for a value that has not moved since the author last let go of a
230
+ * handle. So the write is gated on `authorLayout` actually changing, which is
231
+ * the only event this module cares about.
232
+ *
233
+ * The signature is SEEDED at subscribe time, after `bootLayout` has run, so a
234
+ * boot that only restored what was already on disk writes nothing. What keeps
235
+ * the window's own opinions OUT of the record is `authorLayout`, not the seed.
236
+ *
237
+ * CALL IT AFTER `bootLayout`. The seed is also where `suspendedSizes` compares
238
+ * the record against the store, and before the boot there is nothing to
239
+ * compare: the boot's own application of a saved size then arrives as a change
240
+ * like any other, and a size it clamps is written back clamped — the loss the
241
+ * suspension exists to prevent. It is an ordering rule, not a guess, so it is
242
+ * stated here and in `main.tsx` rather than defended against.
243
+ */
244
+ export function persistLayoutPrefs(store: ShotEditorStore): () => void {
245
+ const held = suspendedSizes(store);
246
+ let last = authorLayout(store, held);
247
+ // A RAW subscription: this runs on every store notification, sixty times a
248
+ // second while the transport plays and once per coalesced pointermove of
249
+ // every drag. It used to `JSON.stringify` the record each time to detect a
250
+ // change that only ever happens on a panel resize; six `Object.is` reads is
251
+ // the same answer.
252
+ return store.subscribe(() => {
253
+ const prefs = authorLayout(store, held);
254
+ if (sameLayoutPrefs(prefs, last)) return;
255
+ last = prefs;
256
+ writeLayoutPrefs(prefs);
257
+ });
258
+ }
259
+
260
+ const LAYOUT_PREF_KEYS: readonly (keyof BootLayoutPrefs)[] = [
261
+ "railWidth",
262
+ "inspectorWidth",
263
+ "dockHeight",
264
+ "railCollapsed",
265
+ "inspectorCollapsed",
266
+ "dockCollapsed",
267
+ ];
268
+
269
+ function sameLayoutPrefs(a: BootLayoutPrefs, b: BootLayoutPrefs): boolean {
270
+ for (const key of LAYOUT_PREF_KEYS) if (!Object.is(a[key], b[key])) return false;
271
+ return true;
272
+ }
@@ -0,0 +1,205 @@
1
+ // THE LAYOUT TABLE: the three editors Edit can show, and every word, key, menu
2
+ // id and refusal sentence that names one of them.
3
+ //
4
+ // ── WHY THIS FILE EXISTS ────────────────────────────────────────────────
5
+ //
6
+ // Renaming the third layout from "Frame" to "Camera" (founder order,
7
+ // 2026-09-05) cost edits in the toolbar, the View menu, the shortcut table,
8
+ // two refusal sentences in `keymap.ts`, the store's error strip, four tests
9
+ // and two docs — and STILL left the in-app manual teaching the withdrawn
10
+ // word, because nothing tied the manual's prose to the control's chip. Every
11
+ // one of those files had written the same three rows out by hand.
12
+ //
13
+ // So the rows live here once, exactly the way `tools.ts` holds the tool rows:
14
+ // one literal, `as const satisfies Record<...>`, and every list, lookup and
15
+ // sentence below DERIVED from it. A layout cannot be renamed on one surface
16
+ // and not another, because no surface owns a word any more.
17
+ //
18
+ // ── THREE SPELLINGS PER LAYOUT IS THE FACT, NOT THE BUG ─────────────────
19
+ //
20
+ // The chip says "3D", the menu row and the manual say "3D view", and a clause
21
+ // in the middle of a sentence says "the 3D view". Those are three different
22
+ // jobs: a word in a 36px segmented track, a row that has to read as a noun
23
+ // beside "Split", and a fragment inside "…this layout shows ___ only". The
24
+ // table carries all three so that the SURFACES stop inventing a fourth.
25
+ //
26
+ // ── WHY NOT IN `stage/panes.ts` ─────────────────────────────────────────
27
+ //
28
+ // The stage owns the MODES (`StageLayoutMode` there is the vocabulary the
29
+ // bridge versions and the scissor reads); this file owns what the CHROME
30
+ // calls them. The stage has no business carrying a menu id, and the chrome
31
+ // has no business inventing a fourth mode. Types flow one way: this file
32
+ // imports the union, never the reverse.
33
+ import type { StageLayoutMode, StagePane } from "../stage/panes.js";
34
+
35
+ export interface LayoutSpec {
36
+ /** The segmented control's chip. Short: the three sit in one track beside
37
+ * the Edit and Video glyphs, and the track does not grow. */
38
+ label: string;
39
+ /** The App menu row and the manual's own name for it. Reads as a noun on
40
+ * its own, which "3D" does not. */
41
+ title: string;
42
+ /** The same layout inside a sentence, article included: "this layout shows
43
+ * ___ only". Written out rather than composed from `title` because "the
44
+ * split" is not "the Split" and English will not derive that. */
45
+ noun: string;
46
+ /** The combo that goes there, in the house glyphs ("⇧1"). The manual, the
47
+ * menu accelerator, the tooltips and the refusal sentences all print THIS,
48
+ * so a rebind is one edit here. */
49
+ key: string;
50
+ /** The physical key the combo is pressed on, which is what `keymap.ts`
51
+ * reads (`layoutForEvent`). Shift turns "1" into "!" on a US board and
52
+ * leaves it "1" on AZERTY, so the character is no witness to the key. */
53
+ code: string;
54
+ /** The App menu row's id, which is also how `shortcuts.ts` points a row at
55
+ * a menu entry and how the menu asks for its accelerator. */
56
+ menuId: string;
57
+ /** The manual's line for the key. One sentence, no trailing stop: the
58
+ * shortcut table's other rows have none either. */
59
+ does: string;
60
+ }
61
+
62
+ /**
63
+ * The three layouts, IN KEY ORDER (⇧1, ⇧2, ⇧3), which is also the order the
64
+ * toolbar draws them and the order the View menu lists them.
65
+ *
66
+ * `Object.keys` on an object literal is insertion order for string keys, so
67
+ * `LAYOUT_MODES` below inherits that order rather than restating it. The
68
+ * `satisfies` is what makes a fourth mode in `stage/panes.ts` a compile error
69
+ * here instead of a layout with no word for it.
70
+ */
71
+ export const LAYOUT_SPECS = {
72
+ working: {
73
+ label: "3D",
74
+ title: "3D view",
75
+ noun: "the 3D view",
76
+ key: "⇧1",
77
+ code: "Digit1",
78
+ menuId: "view.layout.working",
79
+ does: "3D view",
80
+ },
81
+ split: {
82
+ label: "Split",
83
+ title: "Split",
84
+ noun: "the split",
85
+ key: "⇧2",
86
+ code: "Digit2",
87
+ menuId: "view.layout.split",
88
+ // Names the other two through the table rather than spelling them, so the
89
+ // manual's description of the split cannot outlive their names.
90
+ does: "Split (the 3D view with the camera beside it)",
91
+ },
92
+ camera: {
93
+ label: "Camera",
94
+ title: "Camera",
95
+ noun: "the camera view",
96
+ key: "⇧3",
97
+ code: "Digit3",
98
+ menuId: "view.layout.camera",
99
+ does: "Camera: the shot camera's picture, with handles",
100
+ },
101
+ } as const satisfies Record<StageLayoutMode, LayoutSpec>;
102
+
103
+ /** Every layout, in key order. The cast is the one place this file asserts
104
+ * what `satisfies` has already proved: the literal above is the only writer
105
+ * of the object and its key set IS `StageLayoutMode`, which `Object.keys`
106
+ * cannot say because its return type is `string[]` for every object. */
107
+ export const LAYOUT_MODES = Object.keys(LAYOUT_SPECS) as readonly StageLayoutMode[];
108
+
109
+ /**
110
+ * One row, seen as a `LayoutSpec` rather than as its own literal type.
111
+ *
112
+ * `as const satisfies` narrows every row to exactly the strings it wrote,
113
+ * which is excellent for the table and useless for anything iterating it.
114
+ * This is the one widening, and it is in one place — the same bargain
115
+ * `specOf` makes in `tools.ts`.
116
+ */
117
+ export function layoutSpec(mode: StageLayoutMode): LayoutSpec {
118
+ return LAYOUT_SPECS[mode];
119
+ }
120
+
121
+ /** Keyed on the physical key, built from the table so a rebind cannot leave
122
+ * the key lane pointing at the old one. */
123
+ const BY_CODE: ReadonlyMap<string, StageLayoutMode> = new Map(
124
+ LAYOUT_MODES.map((mode) => [LAYOUT_SPECS[mode].code, mode]),
125
+ );
126
+
127
+ /**
128
+ * THE KEYMAP'S WHOLE LAYOUT LANE: the layout this event picks, or null when
129
+ * no layout claims it.
130
+ *
131
+ * SHIFTED, AND BY `code`. The bare digits are left free for the editor's
132
+ * numeric verbs, and a shifted digit reports a different character on every
133
+ * keyboard layout ("!" on US, "1" on AZERTY), so only the physical key names
134
+ * it. The keymap asks this after the modifier lanes, so ⌘ and ⌥ never reach
135
+ * it; the shift is checked here because the bare digit must not match.
136
+ */
137
+ export function layoutForEvent(event: { shiftKey: boolean; code?: string }): StageLayoutMode | null {
138
+ if (!event.shiftKey || event.code === undefined) return null;
139
+ return BY_CODE.get(event.code) ?? null;
140
+ }
141
+
142
+ /**
143
+ * The TAIL every "this layout cannot do that" refusal shares, given the pane
144
+ * that IS on screen.
145
+ *
146
+ * ── WHY A FRAGMENT AND NOT A SENTENCE ───────────────────────────────────
147
+ *
148
+ * Three callers refuse for three different reasons and each owns its own lead
149
+ * clause: a tool says "Rect draws in the camera view, and …", the menu says
150
+ * "That aims the 3D view, and …", the key lane says "that key aims the 3D
151
+ * view, and …" (lower case, because the store prefixes the strip's own word).
152
+ * Only the second half is one rule, so only the second half is shared. A
153
+ * function that returned the whole sentence would have to take the lead as an
154
+ * argument, which is the same duplication with extra steps.
155
+ *
156
+ * What it says: which view you are looking at, the key for the other one,
157
+ * and the key for the split that has both. The keys are in the house glyphs
158
+ * ("⇧1"); a caller that shows the sentence runs it through `formatCombo`, which
159
+ * this file cannot import (`shortcuts.ts` imports this one). Every one of those comes out of
160
+ * the table, so renaming a layout or rebinding a digit rewrites all three
161
+ * refusals with no edit here.
162
+ */
163
+ export function paneOnlyRefusal(shown: StagePane): string {
164
+ const here = LAYOUT_SPECS[shown];
165
+ const other = LAYOUT_SPECS[shown === "camera" ? "working" : "camera"];
166
+ const split = LAYOUT_SPECS.split;
167
+ return (
168
+ `this layout shows ${here.noun} only. ` +
169
+ `Press ${other.key} for ${other.noun}, or ${split.key} for ${split.noun}.`
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Why a view the author asked for cannot be entered here, or null when it can.
175
+ *
176
+ * Its own sentence rather than an arm of `toolRefusal`: these are not tools,
177
+ * and what is refused is a VIEW rather than a place to draw.
178
+ *
179
+ * ONE REFUSAL LEFT. This used to turn the 3D view and the split away above a
180
+ * shot too ("the scene view has one pane"), and that was withdrawn by founder
181
+ * order on 2026-09-05: all three layouts are live at the scene level, where
182
+ * the working pane draws the shot under the playhead, exactly as the stage
183
+ * already resolved it (`StageApp` reads `masterToSceneCached`, never the
184
+ * entered shot). What remains is the definition, which has no shot camera at
185
+ * all.
186
+ *
187
+ * It lives beside the table rather than in `keymap.ts` because the three
188
+ * things that ask it — the digits, the segmented control and the View menu —
189
+ * are the three things that read the table, and the answer is about layouts
190
+ * rather than about keys.
191
+ */
192
+ export function layoutRefusal(inDefinition: boolean): string | null {
193
+ // A DEFINITION HAS NO SHOT CAMERA AT ALL, so neither Video nor a layout over
194
+ // it names anything it can show. The stage forces the working-only layout
195
+ // while one is open (StageApp's `inComponent`), so without this the keys
196
+ // (and the control that shares this rule) changed a mode nothing read and
197
+ // then reported it as active.
198
+ if (inDefinition) {
199
+ return (
200
+ `A component definition has no shot camera, so there is nothing to ` +
201
+ `switch to. Leave the definition (Esc) to get Edit and Video back.`
202
+ );
203
+ }
204
+ return null;
205
+ }
@@ -0,0 +1,167 @@
1
+ // The one impure half of "a dropped recording should land at its own aspect":
2
+ // asking the browser to decode enough of an image or a video to report how wide
3
+ // and how tall it is.
4
+ //
5
+ // ── WHY IT IS NOT IN insert.ts ──────────────────────────────────────────────
6
+ //
7
+ // That file's contract is stated in its own header: no document, no history, no
8
+ // asynchrony, no DOM. That is exactly what makes `boxOf` provable by calling it,
9
+ // and a throwaway `<video>` on an object URL is all four of the things it
10
+ // forbids. So the measurement happens here, the number crosses as
11
+ // `InsertContext.sourceAspect`, and the geometry stays pure.
12
+ //
13
+ // ── WHY IT NEVER REJECTS, AND WHY IT HAS A DEADLINE ─────────────────────────
14
+ //
15
+ // Every failure resolves to `null`, and `null` reproduces the pre-aspect
16
+ // geometry byte for byte. That is not defensiveness, it is the requirement:
17
+ //
18
+ // - jsdom, which the editor's own test lane runs in, decodes no media at all.
19
+ // `loadedmetadata` never fires and `videoWidth` stays 0. Without a deadline
20
+ // the existing `completeImageDraw` tests would each become a 30s timeout
21
+ // (the fast lane's own `testTimeout`), which is a suite that "still passes"
22
+ // while taking four minutes to say so.
23
+ // - A real browser fails too: a codec it cannot decode, a file that is not
24
+ // the media its extension claims, a revoked blob.
25
+ //
26
+ // Nothing here is on the render path and nothing here reaches a document, so a
27
+ // wrong answer costs a badly proportioned box the author can drag. A HANG would
28
+ // cost the insert itself, which is why the deadline is not optional.
29
+ //
30
+ // ── NOT PERSISTED ON THE ASSET ──────────────────────────────────────────────
31
+ //
32
+ // `ShotAsset` is `{id, path, hash, kind}` and stays that way. Storing width and
33
+ // height would mean a probe at upload (ffmpeg, which the CLI only shells for
34
+ // audio today), a schema field, and two places that can disagree about one
35
+ // file's dimensions. The runtime already learns the real size from the decoded
36
+ // texture; this is only the INSERT's guess at a good starting box.
37
+
38
+ /** How long a decode may take before the insert gives up and uses the default
39
+ * geometry. Long enough for a large local file to report metadata, short
40
+ * enough that a host which will never answer (jsdom) does not stall a click. */
41
+ const MEASURE_TIMEOUT_MS = 2000;
42
+
43
+ /** The slice of the media elements this needs. Named so a test can hand over a
44
+ * fake without a DOM, the same seam `textureCache` uses for its videos. */
45
+ export interface MeasurableElement {
46
+ addEventListener(type: string, listener: () => void): void;
47
+ removeEventListener(type: string, listener: () => void): void;
48
+ }
49
+
50
+ export interface MeasureDeps {
51
+ /** Absent = `URL.createObjectURL` / `URL.revokeObjectURL`. */
52
+ objectUrl?: { create: (file: Blob) => string; revoke: (url: string) => void };
53
+ /** Absent = a real `<video>` or `<img>`. */
54
+ createElement?: (kind: "image" | "video") => MeasurableElement;
55
+ setTimeout?: (fn: () => void, ms: number) => unknown;
56
+ clearTimeout?: (handle: unknown) => void;
57
+ }
58
+
59
+ /**
60
+ * width / height of the media at `url`, or null if it cannot be determined.
61
+ *
62
+ * NEVER REJECTS. See the header: every failure path is a `null` that puts the
63
+ * caller back on the geometry it had before this function existed.
64
+ */
65
+ export async function measureMediaUrl(
66
+ url: string,
67
+ kind: "image" | "video",
68
+ deps: MeasureDeps = {},
69
+ ): Promise<number | null> {
70
+ const create =
71
+ deps.createElement ??
72
+ ((k: "image" | "video") =>
73
+ (k === "video"
74
+ ? Object.assign(document.createElement("video"), { preload: "metadata", muted: true })
75
+ : document.createElement("img")) as unknown as MeasurableElement);
76
+ const later = deps.setTimeout ?? ((fn: () => void, ms: number) => globalThis.setTimeout(fn, ms));
77
+ const cancel = deps.clearTimeout ?? ((h: unknown) => globalThis.clearTimeout(h as number));
78
+
79
+ return new Promise<number | null>((resolve) => {
80
+ let settled = false;
81
+ let timer: unknown = null;
82
+ const el = create(kind);
83
+ // The event that says the dimensions exist. A video reports them at
84
+ // `loadedmetadata`, which is well before it could play; an image at `load`.
85
+ const ready = kind === "video" ? "loadedmetadata" : "load";
86
+
87
+ const finish = (aspect: number | null): void => {
88
+ if (settled) return;
89
+ settled = true;
90
+ if (timer !== null) cancel(timer);
91
+ el.removeEventListener(ready, onReady);
92
+ el.removeEventListener("error", onError);
93
+ // The src is cleared so a video that has not finished buffering stops,
94
+ // and so the element drops its reference to a URL the caller is about to
95
+ // revoke. Guarded, because a fake element has no `src`.
96
+ try {
97
+ (el as unknown as { src?: string }).src = "";
98
+ } catch {
99
+ // A fake with a getter-only `src`. Nothing to release.
100
+ }
101
+ resolve(aspect);
102
+ };
103
+
104
+ function onReady(): void {
105
+ const e = el as unknown as { videoWidth?: number; videoHeight?: number; naturalWidth?: number; naturalHeight?: number };
106
+ // Chosen BY KIND rather than by `videoWidth ?? naturalWidth`. The
107
+ // coalesce works only because a real `<img>` has no `videoWidth` at all,
108
+ // which makes it correct by accident: anything exposing both (a fake, a
109
+ // polyfill, a future element) reads the video pair's 0 and reports
110
+ // nothing. The kind is already known here, so ask for the right pair.
111
+ const w = (kind === "video" ? e.videoWidth : e.naturalWidth) ?? 0;
112
+ const h = (kind === "video" ? e.videoHeight : e.naturalHeight) ?? 0;
113
+ // `!(...)` so a NaN from a host that reports one lands here rather than
114
+ // becoming a NaN aspect the geometry would then have to defend against.
115
+ finish(!(w > 0) || !(h > 0) ? null : w / h);
116
+ }
117
+ function onError(): void {
118
+ finish(null);
119
+ }
120
+
121
+ el.addEventListener(ready, onReady);
122
+ el.addEventListener("error", onError);
123
+ timer = later(() => finish(null), MEASURE_TIMEOUT_MS);
124
+ try {
125
+ (el as unknown as { src: string }).src = url;
126
+ } catch {
127
+ finish(null);
128
+ }
129
+ });
130
+ }
131
+
132
+ /**
133
+ * The same, for a `File` the author just picked: wraps it in an object URL and
134
+ * always revokes it, including on the timeout path.
135
+ *
136
+ * A blob left unrevoked pins the whole file in memory for the tab's lifetime,
137
+ * and this runs on a path whose files are screen recordings.
138
+ */
139
+ export async function measureMediaFile(
140
+ file: Blob,
141
+ kind: "image" | "video",
142
+ deps: MeasureDeps = {},
143
+ ): Promise<number | null> {
144
+ const urls = deps.objectUrl ?? {
145
+ create: (f: Blob) => URL.createObjectURL(f),
146
+ revoke: (u: string) => {
147
+ URL.revokeObjectURL(u);
148
+ },
149
+ };
150
+ let url: string;
151
+ try {
152
+ url = urls.create(file);
153
+ } catch {
154
+ return null; // a host with no object-URL support measures nothing
155
+ }
156
+ try {
157
+ return await measureMediaUrl(url, kind, deps);
158
+ } finally {
159
+ try {
160
+ urls.revoke(url);
161
+ } catch {
162
+ // Already gone. Revoking twice is not an error worth surfacing.
163
+ }
164
+ }
165
+ }
166
+
167
+ export { MEASURE_TIMEOUT_MS };