@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
@@ -6,25 +6,54 @@
6
6
  //
7
7
  // `image` and `video` are one pipeline everywhere it matters: the runtime
8
8
  // collapses both to {asset, fit, crop} and draws them through one plane. So the
9
- // controls they share are authored once here, and the two fields that differ —
10
- // `alt` on an image, `offsetFrames` on a video — are rows that only their own
11
- // kind renders rather than a second section.
9
+ // controls they share are authored once here, and the one field that differs —
10
+ // `alt` on an image — is a row that only its own kind renders rather than a
11
+ // second section.
12
12
  //
13
13
  // ── WHY THIS SECTION HAD TO EXIST ───────────────────────────────────────
14
14
  //
15
- // `fit`, `crop`, `alt` and `offsetFrames` were all reachable ONLY through an
16
- // MCP call. A document can therefore hold a crop or a source offset that the
17
- // author cannot see, let alone undo, from the chrome that wrote the layer.
15
+ // `fit`, `crop` and `alt` were all reachable ONLY through an MCP call. A
16
+ // document can therefore hold a crop the author cannot see, let alone undo,
17
+ // from the chrome that wrote the layer.
18
+ //
19
+ // ── WHAT THIS SECTION IS NOT, AND WHERE THOSE THREE WENT ────────────────
20
+ //
21
+ // `offsetFrames` ("Starts at"), `loop` ("Past end") and `timeScale` ("Rate")
22
+ // USED TO BE HERE and are now `TimeSection`'s, because all three are WHEN and
23
+ // this section is WHAT. Each had exactly one home before and still does: a
24
+ // field with two homes is two readouts of one number that disagree the moment
25
+ // either one compensates and the other does not.
26
+ //
27
+ // They did not simply move, either. The source offset is now spelled in the
28
+ // ruler an author states an edit in — "play the recording from source frame
29
+ // 120", through `setVideoSource` — and the rate goes through `retimeVideo`, so
30
+ // a speed change keeps the window and the frame it opens on instead of scrubbing
31
+ // the footage sideways under a cut that is already timed. Neither behaviour was
32
+ // reachable from the raw fields that stood here.
18
33
  import {
34
+ IconMinus,
35
+ IconPlus,
19
36
  InspectorRow,
20
37
  InspectorSection,
21
38
  NumberField,
22
39
  PropertyGrid,
23
40
  SegmentedControl,
24
41
  TextField,
42
+ useLocked,
43
+ wordOptions,
25
44
  } from "@dailies-studio/ui";
45
+ import { useRef, useState } from "react";
46
+ import { KeyframeDot } from "../KeyframeDot.js";
47
+ import { useShotEditor, useStore } from "../../../state/context.js";
48
+ import { IMAGE_ACCEPT, VIDEO_ACCEPT } from "../../toolbar/useAddRows.js";
49
+ import { cropAnchor, cropMagnification, zoomCrop } from "@dailies-studio/shot/doc";
26
50
  import type { Crop, ImageLayer3D, Layer3D, LayerFit, VideoLayer3D } from "@dailies-studio/shot/doc";
27
- import { useSelectedLayers, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
51
+ import {
52
+ useSelectedLayers,
53
+ useSelectionScrub,
54
+ useSelectionValue,
55
+ useSelectionWrite,
56
+ } from "../useDocWrite.js";
28
57
 
29
58
  /**
30
59
  * The two content blocks as ONE shape.
@@ -46,132 +75,211 @@ const FITS = ["fill", "fit", "stretch"] as const satisfies readonly LayerFit[];
46
75
  * source" is exactly what an absent crop already means. */
47
76
  const FULL_CROP: Crop = { x: 0, y: 0, w: 1, h: 1 };
48
77
 
78
+ /**
79
+ * The crop's quantum, and it is NOT 0.01.
80
+ *
81
+ * These edges are min 0 / max 1. What 0.05 buys is the KEYBOARD and the drag's
82
+ * snap — an arrow key crosses the source in 20 presses instead of 100, and the
83
+ * pill lands on 21 stops rather than 101 two-pixel ones. It is also what the
84
+ * rest of this chrome already picked for a bounded 0..1 field (EffectsSection's
85
+ * vignette and ghost amounts). Alt divides the quantum by ten and places
86
+ * continuously; typing is unquantised either way.
87
+ */
88
+ const CROP_STEP = 0.05;
89
+
90
+ /** The crop's four edges, the order the fields list them in, and how the
91
+ * Zoom diamond's tooltip names them (it keys all four together). */
92
+ const CROP_AXES = ["x", "y", "w", "h"] as const satisfies readonly (keyof Crop)[];
93
+ const CROP_PARTS = ["Crop X", "Crop Y", "Crop W", "Crop H"] as const;
94
+
49
95
  export function MediaSection() {
50
96
  const write = useSelectionWrite();
51
97
  const layers = useSelectedLayers();
52
- const media = useSelectionValue((layer) => mediaOf(layer));
98
+ // A SCALAR, one hook per row, and it is `CropField` below that holds the last
99
+ // of them. This section used to read the whole media block here and hand its
100
+ // `mixed` to every field, and `useSelectionValue` compares with `Object.is` —
101
+ // so every multi-selection reported "Mixed" on all five fields, including a
102
+ // selection whose blocks are identical, for ever and with no way to tell it
103
+ // from a real disagreement.
104
+ //
105
+ // The zoom's own drag bracket. Re-derived from the gesture's BASE document
106
+ // like every other capture here, so a drag compounds nothing: without it the
107
+ // magnification would read its own last write and run away from the cursor.
108
+ const zoomScrub = useSelectionScrub((layer, raw) =>
109
+ applyMedia(layer, (m) => ({ ...m, crop: zoomCrop(cropAnchor(m.crop), raw as number) })),
110
+ );
111
+ // Mixed over what the Zoom field SHOWS, the magnification, per layer: two
112
+ // crops of one size at different places agree on it.
113
+ const zoom = useSelectionValue((layer) => {
114
+ const own = mediaOf(layer)?.crop;
115
+ return own === undefined ? undefined : cropMagnification(own);
116
+ });
53
117
  // The first media layer in the selection, which is what the title names and
54
118
  // what the per-kind rows are chosen by. Read from the LIST rather than from
55
119
  // `media.value` (which is the first SELECTED layer's, media or not), so a
56
120
  // rect clicked before an image does not empty the section.
57
121
  const subject = layers.find(isMedia);
122
+ const store = useStore();
123
+ const fileRef = useRef<HTMLInputElement>(null);
124
+ const [busy, setBusy] = useState(false);
125
+ // The crop action and Replace are plain buttons, so they read a viewer's
126
+ // lock here; every other row is a widget that reads it itself.
127
+ const locked = useLocked();
128
+ const assetPath = useShotEditor((s) => {
129
+ const id = subject === undefined ? undefined : mediaOf(subject)?.asset;
130
+ return id === undefined ? undefined : s.doc?.assets[id]?.path;
131
+ });
58
132
  if (subject === undefined) return null;
59
133
  const block = mediaOf(subject);
60
134
  if (block === undefined) return null;
61
135
 
62
- /** Every write goes through one mapper, KIND-GUARDED INSIDE IT: the seam fans
63
- * out over the whole selection, and a rect has no `fit` to set.
64
- *
65
- * `only` narrows that fan-out further, and the per-kind rows MUST use it.
66
- * The rows are chosen by the first media layer in the selection, but the
67
- * write reaches every one of them — so with an image and a video selected
68
- * together, the image's Alt row would otherwise write `alt` into the video
69
- * block and the video's Start row would write `offsetFrames` into the image.
70
- * `MediaBlock` is the intersection of the two, which makes both foreign keys
71
- * type-legal, and `validateShotDoc` is deliberately not stricter than the
72
- * resolver, so nothing downstream refuses them. They surface much later, as
73
- * an excess-property error in the generated `satisfies Doc3D` literal. */
74
136
  const patch = (next: (media: MediaBlock) => MediaBlock, only?: "image" | "video"): void =>
75
- write((layer) =>
76
- layer.kind === "image" && only !== "video"
77
- ? { ...layer, image: next(layer.image) }
78
- : layer.kind === "video" && only !== "image"
79
- ? { ...layer, video: next(layer.video) }
80
- : layer,
81
- );
82
-
83
- const setCrop = (key: keyof Crop, value: number): void =>
84
- patch((m) => ({ ...m, crop: { ...(m.crop ?? FULL_CROP), [key]: clamp01(value) } }));
137
+ write((layer) => applyMedia(layer, next, only));
85
138
 
86
139
  const crop = block.crop;
140
+ // ONE media layer only: a replace swaps one layer's file, and with two
141
+ // selected there is no single file for the row to name.
142
+ const media = subject;
143
+ const medias = layers.filter(isMedia);
144
+ const replaceable = medias.length === 1;
145
+ // Mixed among the layers each row WRITES, as the Html rows ask theirs: Fit
146
+ // reaches every media layer and Alt every image, and a rect beside them has
147
+ // no fit or description to disagree with. Both rows used to show the first
148
+ // media layer's value as if the selection shared it.
149
+ const fitMixed = medias.some((l) => mediaOf(l)?.fit !== block.fit);
150
+ const altMixed = medias.some((l) => l.kind === "image" && (l.image.alt ?? "") !== (block.alt ?? ""));
151
+ const pick = (file: File): void => {
152
+ setBusy(true);
153
+ void store
154
+ .replaceLayerMedia(media.id, file)
155
+ .catch((e: unknown) => store.reportError(e instanceof Error ? e.message : String(e)))
156
+ .finally(() => setBusy(false));
157
+ };
87
158
 
88
159
  return (
89
160
  <InspectorSection
161
+ className="insp-object"
90
162
  title={subject.kind === "video" ? "Video" : "Image"}
91
163
  persistKey="shot:media"
92
164
  action={
93
165
  <button
94
166
  type="button"
95
- className="section-action"
167
+ className="btn is-icon insp-section-action"
96
168
  title={crop === undefined ? "Add crop" : "Remove crop"}
169
+ aria-label={crop === undefined ? "Add crop" : "Remove crop"}
170
+ disabled={locked}
97
171
  onClick={() =>
98
172
  patch((m) => (m.crop === undefined ? { ...m, crop: FULL_CROP } : without(m, "crop")))
99
173
  }
100
174
  >
101
- {crop === undefined ? "+" : "−"}
175
+ {crop === undefined ? <IconPlus /> : <IconMinus />}
102
176
  </button>
103
177
  }
104
178
  >
179
+ {replaceable ? (
180
+ <InspectorRow label="Source">
181
+ {/* A file NAME, so the row's own type and not the numeral face a
182
+ number reads in; it ellipsizes before the button. */}
183
+ <span className="insp-dim is-text" title={assetPath}>
184
+ {assetPath === undefined ? "File missing" : assetPath.replace(/^[0-9a-f]{8}-/u, "")}
185
+ </span>
186
+ <button type="button" className="btn" disabled={locked || busy} onClick={() => fileRef.current?.click()}>
187
+ Replace…
188
+ </button>
189
+ <input
190
+ ref={fileRef}
191
+ type="file"
192
+ hidden
193
+ aria-label={subject.kind === "video" ? "Replace video file" : "Replace image file"}
194
+ accept={subject.kind === "video" ? VIDEO_ACCEPT : IMAGE_ACCEPT}
195
+ onChange={(event) => {
196
+ const file = event.currentTarget.files?.[0];
197
+ event.currentTarget.value = "";
198
+ if (file !== undefined) pick(file);
199
+ }}
200
+ />
201
+ </InspectorRow>
202
+ ) : null}
203
+
105
204
  <InspectorRow label="Fit">
106
205
  <SegmentedControl
107
206
  ariaLabel="Media fit"
108
- value={block.fit}
109
- options={FITS.map((fit) => ({ value: fit, label: fit }))}
207
+ // An unmatched value is how a segmented control says Mixed.
208
+ value={fitMixed ? "" : block.fit}
209
+ options={wordOptions(FITS)}
110
210
  onChange={(next) => patch((m) => ({ ...m, fit: next as LayerFit }))}
111
211
  />
112
212
  </InspectorRow>
113
213
 
214
+ {/* THE PUNCH-IN, as one number.
215
+
216
+ "Zoom into that button" is this product class's defining gesture, and
217
+ it was expressible only by typing four crop edges by hand. It is a CROP
218
+ and not a scale or a camera move, because those two magnify the wrong
219
+ thing: a camera `zoom` takes the background and the border with it, and
220
+ a layer scale works about `transform.position`, which IS the layer's
221
+ centre — `Transform3D` has no anchor, so a scale can only ever punch
222
+ into the middle of a recording.
223
+
224
+ It writes the same four `video.crop.*` / `image.crop.*` numbers the
225
+ edges below do, so an ANIMATED punch-in is the diamond workflow: key
226
+ the zoom at 1x, move the playhead, key it at 2x. Its box carries ONE
227
+ diamond over the four crop tracks, keyed together like the Corner
228
+ row's four radii. */}
229
+ {crop !== undefined ? (
230
+ <InspectorRow>
231
+ <NumberField
232
+ value={cropMagnification(crop)}
233
+ mixed={zoom.mixed}
234
+ label="Zoom"
235
+ title="Magnify a region of the source. Keeps the plate, the border and the background where they are"
236
+ ariaLabel="Media zoom"
237
+ unit="x"
238
+ min={1}
239
+ // Below 1 there is no source left to sample.
240
+ step={0.1}
241
+ trailing={
242
+ <KeyframeDot
243
+ path={CROP_AXES.map((axis) => `${subject.kind}.crop.${axis}`)}
244
+ label="Zoom"
245
+ parts={CROP_PARTS}
246
+ />
247
+ }
248
+ onScrubStart={zoomScrub.start}
249
+ onScrub={zoomScrub.move}
250
+ onScrubEnd={zoomScrub.end}
251
+ onCommit={(next) =>
252
+ write((layer) =>
253
+ applyMedia(layer, (m) => ({ ...m, crop: zoomCrop(cropAnchor(m.crop), next) })),
254
+ )
255
+ }
256
+ />
257
+ </InspectorRow>
258
+ ) : null}
259
+
114
260
  {/* Fractions of the SOURCE, not of the box, and y counts DOWN from the
115
261
  top row the way a screenshot's own pixels are numbered — media.ts owns
116
262
  the flip into uv space, so these are the numbers a design tool would
117
263
  report for the same rectangle. */}
118
264
  {crop !== undefined ? (
119
265
  <PropertyGrid>
120
- <InspectorRow>
121
- <NumberField
122
- value={crop.x}
123
- mixed={media.mixed}
124
- label="X"
125
- ariaLabel="Crop x"
126
- min={0}
127
- max={1}
128
- step={0.01}
129
- onCommit={(next) => setCrop("x", next)}
130
- />
131
- </InspectorRow>
132
- <InspectorRow>
133
- <NumberField
134
- value={crop.y}
135
- mixed={media.mixed}
136
- label="Y"
137
- ariaLabel="Crop y"
138
- min={0}
139
- max={1}
140
- step={0.01}
141
- onCommit={(next) => setCrop("y", next)}
142
- />
143
- </InspectorRow>
144
- <InspectorRow>
145
- <NumberField
146
- value={crop.w}
147
- mixed={media.mixed}
148
- label="W"
149
- ariaLabel="Crop width"
150
- min={0}
151
- max={1}
152
- step={0.01}
153
- onCommit={(next) => setCrop("w", next)}
154
- />
155
- </InspectorRow>
156
- <InspectorRow>
157
- <NumberField
158
- value={crop.h}
159
- mixed={media.mixed}
160
- label="H"
161
- ariaLabel="Crop height"
162
- min={0}
163
- max={1}
164
- step={0.01}
165
- onCommit={(next) => setCrop("h", next)}
166
- />
167
- </InspectorRow>
266
+ {/* THE KEN BURNS MOVE. The track path is per KIND — there is no
267
+ top-level `crop`, each media kind owns one — so the subject's own
268
+ kind names it. A mixed image+video selection hides the diamonds by
269
+ itself: `KeyframeDot` needs every selected layer to accept the
270
+ path, and neither kind accepts the other's. */}
271
+ <CropField axis="x" label="X" ariaLabel="Crop x" value={crop.x} kind={subject.kind} />
272
+ <CropField axis="y" label="Y" ariaLabel="Crop y" value={crop.y} kind={subject.kind} />
273
+ <CropField axis="w" label="W" ariaLabel="Crop width" value={crop.w} kind={subject.kind} />
274
+ <CropField axis="h" label="H" ariaLabel="Crop height" value={crop.h} kind={subject.kind} />
168
275
  </PropertyGrid>
169
276
  ) : null}
170
277
 
171
278
  {subject.kind === "image" ? (
172
279
  <InspectorRow label="Alt">
173
280
  <TextField
174
- value={block.alt ?? ""}
281
+ value={altMixed ? "" : (block.alt ?? "")}
282
+ {...(altMixed ? { placeholder: "Mixed" } : {})}
175
283
  ariaLabel="Alt text"
176
284
  onCommit={(next) =>
177
285
  patch((m) => {
@@ -185,46 +293,91 @@ export function MediaSection() {
185
293
  />
186
294
  </InspectorRow>
187
295
  ) : null}
188
-
189
- {subject.kind === "video" ? (
190
- // PLACEMENT, NOT A TRIM. This is the scene frame the source's own t=0
191
- // lands on, so every frame before it holds the source's FIRST frame and
192
- // nothing is cut off the head. Audio's identically-named `offsetFrames`
193
- // is the other thing — a trim into the source — and confusing the two
194
- // is why the label says "starts at" rather than "start".
195
- <InspectorRow label="Starts at" labelTitle="The scene frame this clip's first frame lands on — a delay, never a trim into the source">
196
- <NumberField
197
- value={block.offsetFrames ?? 0}
198
- mixed={media.mixed}
199
- ariaLabel="Video start frame"
200
- unit="f"
201
- step={1}
202
- // NO `min`. NumberField clamps against it on every commit path —
203
- // typing, the stepper and the drag — so `min={0}` would turn the
204
- // one value this feature exists to preserve into 0 the first time
205
- // the author touched the field, and the `frames === 0` branch below
206
- // would then DELETE it, jumping the footage. A negative offset is
207
- // what `splitScene` produces for a clip already playing at the cut,
208
- // and it is untypeable, so erasing one is unrecoverable.
209
- //
210
- // 0 DELETES the key: absent already means "starts at frame 0", and
211
- // an explicit 0 is a second way to say the same thing that codegen
212
- // then prints. `Math.round` covers the only bound the save gate
213
- // actually holds — the value must be an integer.
214
- onCommit={(next) => {
215
- const frames = Math.round(next);
216
- patch(
217
- (m) => (frames === 0 ? without(m, "offsetFrames") : { ...m, offsetFrames: frames }),
218
- "video",
219
- );
220
- }}
221
- />
222
- </InspectorRow>
223
- ) : null}
224
296
  </InspectorSection>
225
297
  );
226
298
  }
227
299
 
300
+ /**
301
+ * One crop edge, and its own drag bracket.
302
+ *
303
+ * A row rather than inline JSX because a bracket is a HOOK and there is one
304
+ * gesture per edge. Both halves go through `withCrop`, because a capture is
305
+ * handed a layer out of the gesture's BASE document: an edge that re-derived
306
+ * itself from what the panel last rendered would compound its own moves and run
307
+ * away from the cursor.
308
+ */
309
+ function CropField(props: {
310
+ axis: keyof Crop;
311
+ label: string;
312
+ ariaLabel: string;
313
+ value: number;
314
+ kind: Layer3D["kind"];
315
+ }) {
316
+ const { axis } = props;
317
+ const write = useSelectionWrite();
318
+ // This edge's own agreement, read as the SCALAR the row displays.
319
+ const { mixed } = useSelectionValue((layer) => mediaOf(layer)?.crop?.[axis]);
320
+ const scrub = useSelectionScrub((layer, raw) =>
321
+ applyMedia(layer, (m) => withCrop(m, axis, raw as number)),
322
+ );
323
+ return (
324
+ <InspectorRow>
325
+ <NumberField
326
+ value={props.value}
327
+ mixed={mixed}
328
+ label={props.label}
329
+ ariaLabel={props.ariaLabel}
330
+ min={0}
331
+ max={1}
332
+ step={CROP_STEP}
333
+ trailing={<KeyframeDot path={`${props.kind}.crop.${axis}`} label={props.ariaLabel} />}
334
+ onCommit={(next) => write((layer) => applyMedia(layer, (m) => withCrop(m, axis, next)))}
335
+ onScrubStart={scrub.start}
336
+ onScrub={scrub.move}
337
+ onScrubEnd={scrub.end}
338
+ />
339
+ </InspectorRow>
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Every write goes through one mapper, KIND-GUARDED INSIDE IT: the seam fans
345
+ * out over the whole selection, and a rect has no `fit` to set.
346
+ *
347
+ * `only` narrows that fan-out further, and the per-kind rows MUST use it. The
348
+ * rows are chosen by the first media layer in the selection, but the write
349
+ * reaches every one of them — so with an image and a video selected together,
350
+ * the image's Alt row would otherwise write `alt` into the video block.
351
+ * `MediaBlock` is the intersection of the two, which makes such a foreign key
352
+ * type-legal, and `validateShotDoc` is deliberately not stricter than the
353
+ * resolver, so nothing downstream refuses it. It surfaces much later, as an
354
+ * excess-property error in the generated `satisfies Doc3D` literal.
355
+ *
356
+ * ONE row uses it today (`alt`), because the three video-only rows that used to
357
+ * need it are `TimeSection`'s now and that section writes through the clip ops
358
+ * over ONE layer. The parameter stays: `only` is the rule, not a workaround for
359
+ * the rows that happened to exist, and the next per-kind field needs it on its
360
+ * first day.
361
+ *
362
+ * PURE and module-level rather than closed over `write`, because the drag needs
363
+ * the same mapper and a capture only ever gets a layer handed to it.
364
+ */
365
+ function applyMedia(
366
+ layer: Layer3D,
367
+ next: (media: MediaBlock) => MediaBlock,
368
+ only?: "image" | "video",
369
+ ): Layer3D {
370
+ return layer.kind === "image" && only !== "video"
371
+ ? { ...layer, image: next(layer.image) }
372
+ : layer.kind === "video" && only !== "image"
373
+ ? { ...layer, video: next(layer.video) }
374
+ : layer;
375
+ }
376
+
377
+ function withCrop(media: MediaBlock, key: keyof Crop, value: number): MediaBlock {
378
+ return { ...media, crop: { ...(media.crop ?? FULL_CROP), [key]: clamp01(value) } };
379
+ }
380
+
228
381
  function isMedia(layer: Layer3D): boolean {
229
382
  return layer.kind === "image" || layer.kind === "video";
230
383
  }
@@ -238,7 +391,7 @@ function mediaOf(layer: Layer3D): MediaBlock | undefined {
238
391
  /** Drop one optional key, rather than setting it to `undefined`. Under
239
392
  * exactOptionalPropertyTypes those are different values, and codegen prints
240
393
  * the block by key order — a present-but-undefined key is a key. */
241
- function without(media: MediaBlock, key: "crop" | "alt" | "offsetFrames"): MediaBlock {
394
+ function without(media: MediaBlock, key: "crop" | "alt"): MediaBlock {
242
395
  const { [key]: _drop, ...rest } = media;
243
396
  return rest;
244
397
  }