@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,617 @@
1
+ // THE PLAN OF ONE PLANNED SHOT (Paper SB1 and SB2): the right panel of the
2
+ // shot board, and of the board when one shot card is selected.
3
+ //
4
+ // head SB2 "Plan" and "Shot 02" on the shot board, whose bar names the
5
+ // shot; SB1 the number, the name and the status badge on the
6
+ // board (`variant`). The name renames in place (`PartName`).
7
+ // words What we see (`part.notes`), How it moves (`part.motion`, the
8
+ // numbered beats: add, edit, remove, drag or Alt+arrow to
9
+ // reorder, at most `STORYBOARD_BEATS_MAX`), Voice (`part.voice`).
10
+ // timing Length (`part.durationFrames`, typed and shown in seconds,
11
+ // "2.00s") and Cut to next: the Shot's transition, edited with the
12
+ // Studio's own rows (`TransitionRows`) once the shot has a Shot.
13
+ // ground Paper, Dark or Brand for the sketch the panel follows (`sketchId`,
14
+ // else Sketch 1). Hidden while the shot has no sketch.
15
+ // pinned the cards pinned to this shot: Must or Optional for a
16
+ // requirement, "on sketch" when the sketch draws it. Each row drags
17
+ // as a board card (`STORYBOARD_MIME`), so it drops on a sketch.
18
+ // style how many Style cards apply to every shot.
19
+ // footer the caller's (`footer`), else on the shot board the agent line.
20
+ //
21
+ // THE BLOCKS KEEP THEIR CAPS LABELS (`.caps` is the design system's label for
22
+ // a form), and every control in them is a house widget at its standard
23
+ // geometry: TextFields, a NumberField, the select trigger, the segmented
24
+ // control. EVERY WRITE IS ONE UNDO STEP through `store.edit` and the pure
25
+ // part and sketch ops, so this panel needs no store verb of its own, and a
26
+ // value written back unchanged is no step at all (the ops return the same
27
+ // document). A VIEWER reads it all with the writes off (`LockedControls`).
28
+ import { useEffect, useId, useRef, useState } from "react";
29
+ import type { DragEvent as ReactDragEvent, KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
30
+ import { framesToSeconds } from "@dailies-studio/engine";
31
+ import { FieldBox, IconChevronDown, IconClose, LockedControls, NumberField, SegmentedControl, TextField, useLocked } from "@dailies-studio/ui";
32
+ import {
33
+ STORYBOARD_BEATS_MAX,
34
+ STORYBOARD_VOICE_MAX,
35
+ isRequirement,
36
+ referenceImportanceOf,
37
+ updatePart,
38
+ updateSketch,
39
+ } from "@dailies-studio/shot/doc";
40
+ import type { Doc3D, ReferenceItem3D, StoryboardBeat3D, StoryboardPart3D, StoryboardSketch3D } from "@dailies-studio/shot/doc";
41
+ import { useShotEditor, useStore } from "../../state/context.js";
42
+ import {
43
+ SHOT_STATUS_LABEL,
44
+ boardShots,
45
+ cardKindOf,
46
+ cutLabel,
47
+ shotStatusOf,
48
+ storyEntries,
49
+ styleItemsOf,
50
+ } from "../../state/storyboardModel.js";
51
+ import type { ShotStatus } from "../../state/storyboardModel.js";
52
+ import { useImageThumb } from "../../state/imageThumbs.js";
53
+ import { brandBackdrop } from "../../bridgeTypes.js";
54
+ import { ADD_ICON, SPARKLES_ICON } from "../../icons.js";
55
+ import { TransitionRows } from "../inspector/scene/TransitionSection.js";
56
+ import { useScrub } from "../inspector/useDocWrite.js";
57
+ import { formatDuration } from "../timeline/model.js";
58
+ import { assetUrl, pinGlyph } from "./ReferencePreview.js";
59
+ import { writeDrag } from "./storyboardDrop.js";
60
+ import "./plan.css";
61
+ import { useBuildingParts } from "../agent/AgentDots.js";
62
+
63
+ /** A beat row's own drag type: a reorder inside one list, never a board card. */
64
+ const BEAT_MIME = "application/x-dailies-plan-beat";
65
+
66
+ /** The badge tone each status reads in (G1 `.badge`). */
67
+ const STATUS_TONE: Record<ShotStatus, string> = { planned: "", building: " is-building", built: " is-success" };
68
+
69
+ /** "02": a planned shot's place in playing order, or null when it has none. */
70
+ export function partNumberOf(doc: Doc3D, partId: string): string | null {
71
+ const at = storyEntries(doc).findIndex((e) => (e.kind === "shot" ? e.shot.part?.id : e.part.id) === partId);
72
+ return at < 0 ? null : String(at + 1).padStart(2, "0");
73
+ }
74
+
75
+ /** Typed seconds ("2", "2.5s") or frames ("48f") back to frames, or null. */
76
+ export function parseLength(text: string, fps: number): number | null {
77
+ const m = /^\s*(\d+(?:\.\d+)?|\.\d+)\s*(s|f)?\s*$/i.exec(text);
78
+ if (m === null) return null;
79
+ const n = Number(m[1]);
80
+ const frames = m[2]?.toLowerCase() === "f" ? n : n * fps;
81
+ return Number.isFinite(frames) && frames > 0 ? frames / fps : null;
82
+ }
83
+
84
+ /** The beats with `at` moved to `to`, both clamped. */
85
+ export function moveBeat(beats: readonly StoryboardBeat3D[], at: number, to: number): StoryboardBeat3D[] {
86
+ const next = [...beats];
87
+ const target = Math.max(0, Math.min(next.length - 1, to));
88
+ if (at < 0 || at >= next.length || target === at) return next;
89
+ const [beat] = next.splice(at, 1);
90
+ next.splice(target, 0, beat!);
91
+ return next;
92
+ }
93
+
94
+ /** A trimmed text field's value as a patch: null drops the key. */
95
+ const textPatch = (text: string, max?: number): string | null => {
96
+ const trimmed = text.trim();
97
+ if (trimmed === "") return null;
98
+ return max === undefined ? trimmed : trimmed.slice(0, max).trim();
99
+ };
100
+
101
+ /**
102
+ * A PLANNED SHOT'S NAME, AND ITS RENAME, wherever the shot is named: the head
103
+ * of its Plan panel on the board, and the shot board's bar. The name is a
104
+ * button and the rename is the store's (`storyboardRenaming`), so F2 (on the
105
+ * board with the shot picked, or on its shot board), a double-click on the
106
+ * name, and Enter or F2 on the name with the keyboard all open the one field.
107
+ * A single press does nothing: it is a name, read far more than renamed. A
108
+ * viewer cannot rename, so for them it is plain words: a focusable button
109
+ * that did nothing and never said why was a dead stop on every Tab.
110
+ */
111
+ export function PartName(props: { part: StoryboardPart3D; className: string; testId?: string }) {
112
+ const store = useStore();
113
+ const readOnly = useShotEditor((s) => s.readOnly);
114
+ const renaming = useShotEditor((s) => s.storyboardRenaming === props.part.id);
115
+ const name = useRef<HTMLButtonElement>(null);
116
+ // A RENAME CLOSED FROM THE KEYBOARD HANDS THE FOCUS BACK TO THE NAME. Enter
117
+ // and Escape blur the field, which then unmounts with nothing focused, so
118
+ // the focus fell to the page and the next Tab started from its top. A click
119
+ // away keeps the focus where the click put it.
120
+ const refocus = useRef(false);
121
+ useEffect(() => {
122
+ if (renaming || !refocus.current) return;
123
+ refocus.current = false;
124
+ name.current?.focus();
125
+ }, [renaming]);
126
+ if (renaming && !readOnly) {
127
+ return (
128
+ <PartRename
129
+ key={props.part.id}
130
+ part={props.part}
131
+ onKeyClose={() => {
132
+ refocus.current = true;
133
+ }}
134
+ />
135
+ );
136
+ }
137
+ if (readOnly) {
138
+ return (
139
+ <span
140
+ className={props.className}
141
+ {...(props.testId === undefined ? {} : { "data-testid": props.testId })}
142
+ title={props.part.label}
143
+ >
144
+ {props.part.label}
145
+ </span>
146
+ );
147
+ }
148
+ const open = (): void => store.beginStoryboardRename(props.part.id);
149
+ return (
150
+ <button
151
+ ref={name}
152
+ type="button"
153
+ className={props.className}
154
+ {...(props.testId === undefined ? {} : { "data-testid": props.testId })}
155
+ title={`${props.part.label}: double-click or F2 to rename`}
156
+ onDoubleClick={open}
157
+ // A press with no pointer behind it (`detail` 0) is assistive tech's,
158
+ // which has no double press to give: it opens the rename too.
159
+ onClick={(event) => {
160
+ if (event.detail === 0) open();
161
+ }}
162
+ onKeyDown={(event) => {
163
+ if (event.key !== "F2" && event.key !== "Enter") return;
164
+ event.preventDefault();
165
+ event.stopPropagation();
166
+ open();
167
+ }}
168
+ >
169
+ {props.part.label}
170
+ </button>
171
+ );
172
+ }
173
+
174
+ /**
175
+ * The open rename. It commits on Enter or on a blur that changed the name,
176
+ * and it CLOSES on every blur and on Escape: TextField commits only a change,
177
+ * so the field used to stay open under a click away or an Escape, with
178
+ * nothing else to close it (the window's keys pass a focused field by).
179
+ * `onKeyClose` hears Enter and Escape in the CAPTURE phase, before the field
180
+ * blurs itself on them, so the name knows to take the focus back.
181
+ */
182
+ function PartRename({ part, onKeyClose }: { part: StoryboardPart3D; onKeyClose(): void }) {
183
+ const store = useStore();
184
+ const close = (): void => {
185
+ if (store.getState().storyboardRenaming === part.id) store.beginStoryboardRename(null);
186
+ };
187
+ return (
188
+ <span
189
+ className="plan-rename"
190
+ onBlur={close}
191
+ onKeyDownCapture={(event) => {
192
+ if (event.key === "Enter" || event.key === "Escape") onKeyClose();
193
+ }}
194
+ >
195
+ <TextField
196
+ autoFocus
197
+ value={part.label}
198
+ ariaLabel="Shot name"
199
+ onCommit={(text) => {
200
+ const label = text.trim();
201
+ if (label !== "" && label !== part.label) store.updateStoryboardPart(part.id, { label });
202
+ }}
203
+ />
204
+ </span>
205
+ );
206
+ }
207
+
208
+ export function PlanPanel(props: {
209
+ partId: string;
210
+ /** "shot" is the shot board's head (SB2), "board" the board's (SB1). */
211
+ variant?: "shot" | "board";
212
+ /** The sketch Ground writes and "on sketch" reads. Default: Sketch 1. */
213
+ sketchId?: string;
214
+ /** Replaces the default footer. */
215
+ footer?: ReactNode;
216
+ }) {
217
+ const store = useStore();
218
+ const doc = useShotEditor((s) => s.doc);
219
+ const readOnly = useShotEditor((s) => s.readOnly);
220
+ const building = useBuildingParts();
221
+ const variant = props.variant ?? "shot";
222
+ const part = doc?.storyboard?.parts.find((p) => p.id === props.partId);
223
+ if (doc === null || part === undefined) return null;
224
+
225
+ const write = (patch: Parameters<typeof updatePart>[2]): void => {
226
+ store.edit((base) => updatePart(base, part.id, patch));
227
+ };
228
+ const number = partNumberOf(doc, part.id);
229
+ const sketch = part.sketches?.find((s) => s.id === props.sketchId) ?? part.sketches?.[0];
230
+ const footer =
231
+ props.footer !== undefined ? (
232
+ <footer className="panel-foot">{props.footer}</footer>
233
+ ) : variant === "shot" ? (
234
+ <footer className="panel-foot is-note">
235
+ {SPARKLES_ICON}
236
+ The agent reads the plan and sketches.
237
+ </footer>
238
+ ) : null;
239
+
240
+ return (
241
+ <section className="plan-panel" data-testid="plan-panel" aria-label="Plan">
242
+ {variant === "shot" ? (
243
+ <header className="panel-head plan-head">
244
+ <h3 className="caps">Plan</h3>
245
+ {number === null ? null : <span className="plan-head-shot">{`Shot ${number}`}</span>}
246
+ </header>
247
+ ) : (
248
+ <header className="panel-head plan-head">
249
+ {number === null ? null : <span className="plan-head-number">{number}</span>}
250
+ <h3 className="plan-head-title">
251
+ <PartName part={part} className="plan-head-name" />
252
+ </h3>
253
+ <StatusBadge status={shotStatusOf(part, building)} />
254
+ </header>
255
+ )}
256
+ <LockedControls value={readOnly}>
257
+ {/* KEYED ON THE PART: another shot starts with no beat draft and the
258
+ cut rows closed, never the last shot's. */}
259
+ <div className="plan-body" key={part.id}>
260
+ <div className="plan-field">
261
+ <h4 className="caps">What we see</h4>
262
+ <TextField
263
+ multiline
264
+ value={part.notes ?? ""}
265
+ placeholder="What the viewer sees in this shot"
266
+ ariaLabel="What we see"
267
+ onCommit={(text) => write({ notes: textPatch(text) })}
268
+ />
269
+ </div>
270
+ <Beats part={part} onWrite={(motion) => write({ motion })} />
271
+ <div className="plan-field">
272
+ <h4 className="caps">Voice</h4>
273
+ <TextField
274
+ value={part.voice ?? ""}
275
+ placeholder="The line we hear"
276
+ ariaLabel="Voice"
277
+ onCommit={(text) => write({ voice: textPatch(text, STORYBOARD_VOICE_MAX) })}
278
+ />
279
+ </div>
280
+ <Timing doc={doc} part={part} onLength={(durationFrames) => write({ durationFrames })} />
281
+ {sketch === undefined ? null : <Ground doc={doc} sketch={sketch} />}
282
+ <Pinned doc={doc} part={part} sketch={sketch} />
283
+ <StyleRow count={styleItemsOf(doc).length} />
284
+ </div>
285
+ </LockedControls>
286
+ {footer}
287
+ </section>
288
+ );
289
+ }
290
+
291
+ function StatusBadge({ status }: { status: ShotStatus }) {
292
+ return (
293
+ <span className={`badge${STATUS_TONE[status]}`} data-testid="plan-status">
294
+ {SHOT_STATUS_LABEL[status]}
295
+ </span>
296
+ );
297
+ }
298
+
299
+ // ── HOW IT MOVES ────────────────────────────────────────────────────────
300
+
301
+ function Beats(props: { part: StoryboardPart3D; onWrite(motion: StoryboardBeat3D[] | null): void }) {
302
+ const beats = props.part.motion ?? [];
303
+ const readOnly = useShotEditor((s) => s.readOnly);
304
+ // The remove buttons write, so the panel's lock takes them with the fields.
305
+ const locked = useLocked();
306
+ const [adding, setAdding] = useState(false);
307
+ const dragFrom = useRef<number | null>(null);
308
+ const focusAt = useRef<number | null>(null);
309
+ const list = useRef<HTMLOListElement>(null);
310
+ const full = beats.length >= STORYBOARD_BEATS_MAX;
311
+ const write = (next: StoryboardBeat3D[]): void => props.onWrite(next.length === 0 ? null : next);
312
+
313
+ // An emptied beat is removed: a beat's text is never stored empty.
314
+ const setText = (at: number, text: string): void => {
315
+ const trimmed = text.trim();
316
+ if (trimmed === beats[at]?.text) return;
317
+ write(trimmed === "" ? beats.filter((_, i) => i !== at) : beats.map((b, i) => (i === at ? { ...b, text: trimmed } : b)));
318
+ };
319
+ const move = (at: number, to: number): void => {
320
+ if (to < 0 || to >= beats.length || to === at) return;
321
+ write(moveBeat(beats, at, to));
322
+ };
323
+ const onKey = (at: number) => (event: ReactKeyboardEvent) => {
324
+ if (!event.altKey || readOnly || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
325
+ event.preventDefault();
326
+ const to = at + (event.key === "ArrowUp" ? -1 : 1);
327
+ if (to < 0 || to >= beats.length) return;
328
+ focusAt.current = to;
329
+ move(at, to);
330
+ };
331
+ // THE FOCUS FOLLOWS THE MOVED BEAT: the move re-keys its row, so a second
332
+ // Alt+arrow would otherwise press nothing.
333
+ useEffect(() => {
334
+ const to = focusAt.current;
335
+ if (to === null) return;
336
+ focusAt.current = null;
337
+ list.current?.querySelector<HTMLElement>(`[aria-label="Beat ${to + 1}"]`)?.focus();
338
+ });
339
+ const onDrop = (at: number) => (event: ReactDragEvent) => {
340
+ const from = dragFrom.current;
341
+ dragFrom.current = null;
342
+ if (from === null || !event.dataTransfer.types.includes(BEAT_MIME)) return;
343
+ event.preventDefault();
344
+ move(from, at);
345
+ };
346
+
347
+ return (
348
+ <div className="plan-field">
349
+ <div className="plan-field-head">
350
+ <h4 className="caps">How it moves</h4>
351
+ <span className="plan-count">{beats.length === 1 ? "1 beat" : `${beats.length} beats`}</span>
352
+ </div>
353
+ <ol ref={list} className="plan-beats" aria-label="How it moves">
354
+ {beats.map((beat, at) => (
355
+ <li
356
+ // Text, then place: a reorder re-keys the moved rows, so a draft
357
+ // never follows the wrong beat.
358
+ key={`${at}:${beat.text}`}
359
+ className="plan-beat"
360
+ data-testid="plan-beat"
361
+ draggable={!readOnly}
362
+ title="Drag, or Alt+arrow, to reorder"
363
+ onDragStart={(event) => {
364
+ dragFrom.current = at;
365
+ event.dataTransfer.setData(BEAT_MIME, String(at));
366
+ event.dataTransfer.effectAllowed = "move";
367
+ }}
368
+ onDragOver={(event) => {
369
+ if (dragFrom.current === null) return;
370
+ event.preventDefault();
371
+ event.dataTransfer.dropEffect = "move";
372
+ }}
373
+ onDrop={onDrop(at)}
374
+ onDragEnd={() => {
375
+ dragFrom.current = null;
376
+ }}
377
+ onKeyDown={onKey(at)}
378
+ >
379
+ <span className="plan-beat-number">{at + 1}</span>
380
+ {/* THE ROW'S VERB IS INSIDE THE BEAT'S BOX, the house accessory
381
+ lane (`FieldBox`): a 16px key with the remove glyph that turns
382
+ to the danger ink under the pointer. So every beat's box ends
383
+ on one right edge, the open draft's too, which has no verb. */}
384
+ <FieldBox
385
+ trailing={
386
+ <button
387
+ type="button"
388
+ className="insp-key is-danger"
389
+ aria-label={`Remove beat ${at + 1}`}
390
+ title="Remove beat"
391
+ disabled={locked}
392
+ onClick={() => write(beats.filter((_, i) => i !== at))}
393
+ >
394
+ <IconClose />
395
+ </button>
396
+ }
397
+ >
398
+ <TextField value={beat.text} ariaLabel={`Beat ${at + 1}`} onCommit={(text) => setText(at, text)} />
399
+ </FieldBox>
400
+ </li>
401
+ ))}
402
+ {adding ? (
403
+ // Any blur ends the draft (Enter, Escape, a click away); the field's
404
+ // own blur commits first, and an empty draft writes nothing.
405
+ <li className="plan-beat is-draft" onBlur={() => setAdding(false)}>
406
+ <span className="plan-beat-number">{beats.length + 1}</span>
407
+ <FieldBox>
408
+ <TextField
409
+ value=""
410
+ autoFocus
411
+ placeholder="What moves next"
412
+ ariaLabel={`Beat ${beats.length + 1}`}
413
+ onCommit={(text) => {
414
+ const trimmed = text.trim();
415
+ if (trimmed !== "" && !full) write([...beats, { text: trimmed }]);
416
+ }}
417
+ />
418
+ </FieldBox>
419
+ </li>
420
+ ) : null}
421
+ </ol>
422
+ {adding ? null : (
423
+ <button
424
+ type="button"
425
+ className="plan-add-beat"
426
+ data-testid="plan-add-beat"
427
+ disabled={locked || full}
428
+ title={full ? `A shot holds at most ${STORYBOARD_BEATS_MAX} beats` : undefined}
429
+ onClick={() => setAdding(true)}
430
+ >
431
+ <span className="plan-add-beat-icon" aria-hidden="true">
432
+ {ADD_ICON}
433
+ </span>
434
+ Add beat
435
+ </button>
436
+ )}
437
+ </div>
438
+ );
439
+ }
440
+
441
+ // ── LENGTH AND CUT TO NEXT ──────────────────────────────────────────────
442
+
443
+ function Timing(props: { doc: Doc3D; part: StoryboardPart3D; onLength(frames: number): void }) {
444
+ const { doc, part } = props;
445
+ const [open, setOpen] = useState(false);
446
+ const rowsId = useId();
447
+ const fps = doc.fps.num / doc.fps.den;
448
+ const seconds = framesToSeconds(part.durationFrames, doc.fps);
449
+ const sceneId = part.scene !== undefined && doc.scenes.includes(part.scene) ? part.scene : null;
450
+ const shot = sceneId === null ? undefined : boardShots(doc).find((s) => s.sceneId === sceneId);
451
+ const label = shot === undefined ? "Cut" : cutLabel(doc, shot);
452
+ // "Wipe 0.40s": the name, then the played length in the numeric face.
453
+ const split = /^(.*?)\s(\d+\.\d{2}s)$/.exec(label);
454
+ /** Seconds on the field to whole frames, never below one. */
455
+ const framesOf = (next: number): number => Math.max(1, Math.round(next * fps));
456
+ // A DRAG ON THE LENGTH writes live, in one undo step, so the shot card's
457
+ // length and the board's total move with the pointer.
458
+ const lengthScrub = useScrub((base, raw) =>
459
+ typeof raw === "number" && Number.isFinite(raw) ? updatePart(base, part.id, { durationFrames: framesOf(raw) }) : base,
460
+ );
461
+ return (
462
+ <>
463
+ <div className="plan-timing">
464
+ <div className="plan-field plan-length">
465
+ <h4 className="caps">Length</h4>
466
+ <NumberField
467
+ value={seconds}
468
+ min={1 / fps}
469
+ step={1 / fps}
470
+ format={formatDuration}
471
+ parse={(text) => parseLength(text, fps)}
472
+ ariaLabel="Length"
473
+ onCommit={(next) => props.onLength(framesOf(next))}
474
+ onScrubStart={lengthScrub.start}
475
+ onScrub={lengthScrub.move}
476
+ onScrubEnd={lengthScrub.end}
477
+ />
478
+ </div>
479
+ <div className="plan-field plan-cut">
480
+ <h4 className="caps">Cut to next</h4>
481
+ {/* THE HOUSE SELECT TRIGGER, the Combobox's own anatomy (the value
482
+ ellipsized, its caret), on a button that discloses the cut's
483
+ rows under it rather than a list: the cut is edited with the
484
+ Studio's own transition rows, not picked from a menu. */}
485
+ <button
486
+ type="button"
487
+ className="select combobox-trigger plan-cut-select"
488
+ data-testid="plan-cut"
489
+ // NAMED BY ITS LABEL, then by what it shows: the caps label over it
490
+ // is a heading, so the button alone read "Wipe 0.40s".
491
+ aria-label={`Cut to next: ${label}`}
492
+ {...(sceneId === null ? {} : { "aria-expanded": open, "aria-controls": rowsId })}
493
+ disabled={sceneId === null}
494
+ title={sceneId === null ? "Set once this shot has a Shot in Studio" : "Edit the cut into the next shot"}
495
+ onClick={() => setOpen((was) => !was)}
496
+ >
497
+ <span className="combobox-value">{split?.[1] ?? label}</span>
498
+ {split === null ? null : <span className="plan-cut-length">{split[2]}</span>}
499
+ <IconChevronDown className="combobox-caret field-caret" />
500
+ </button>
501
+ </div>
502
+ </div>
503
+ {open && sceneId !== null ? (
504
+ <div className="plan-cut-rows" id={rowsId} data-testid="plan-cut-rows">
505
+ <TransitionRows sceneId={sceneId} lengthLabel="Length" />
506
+ </div>
507
+ ) : null}
508
+ </>
509
+ );
510
+ }
511
+
512
+ // ── GROUND ──────────────────────────────────────────────────────────────
513
+
514
+ function Ground(props: { doc: Doc3D; sketch: StoryboardSketch3D }) {
515
+ const store = useStore();
516
+ const { sketch } = props;
517
+ const brand = brandBackdrop(props.doc);
518
+ const swatch = (tone: string, colour?: string): ReactNode => (
519
+ <span className={`plan-swatch is-${tone}`} aria-hidden="true" style={colour === undefined ? undefined : { background: colour }} />
520
+ );
521
+ return (
522
+ <div className="plan-field">
523
+ <h4 className="caps">Ground</h4>
524
+ <SegmentedControl
525
+ ariaLabel="Ground"
526
+ value={sketch.ground ?? "paper"}
527
+ options={[
528
+ { value: "paper", label: <>{swatch("paper")}Paper</> },
529
+ { value: "dark", label: <>{swatch("dark")}Dark</> },
530
+ // The Project's brand colour when it has one (Paper Q10).
531
+ { value: "brand", label: <>{swatch("brand", brand ?? undefined)}Brand</> },
532
+ ]}
533
+ // Paper is the absent key; the lit chip pressed again writes nothing
534
+ // (`updateSketch` returns the same document).
535
+ onChange={(value) => store.edit((base) => updateSketch(base, sketch.id, { ground: value === "dark" || value === "brand" ? value : null }))}
536
+ />
537
+ </div>
538
+ );
539
+ }
540
+
541
+ // ── PINNED TO THIS SHOT ─────────────────────────────────────────────────
542
+
543
+ /** A card kind as the row says it: "Image", "Component". */
544
+ const kindWord = (item: ReferenceItem3D, assets: Doc3D["assets"]): string => {
545
+ const kind = cardKindOf(item, assets);
546
+ return kind.charAt(0).toUpperCase() + kind.slice(1);
547
+ };
548
+
549
+ function Pinned(props: { doc: Doc3D; part: StoryboardPart3D; sketch: StoryboardSketch3D | undefined }) {
550
+ const { doc, part, sketch } = props;
551
+ const readOnly = useShotEditor((s) => s.readOnly);
552
+ const pins = (doc.resources?.references ?? []).filter((r) => r.part === part.id);
553
+ const onSketch = new Set((sketch?.items ?? []).flatMap((item) => (item.kind === "ref" ? [item.ref] : [])));
554
+ return (
555
+ <div className="plan-field">
556
+ <div className="plan-field-head">
557
+ <h4 className="caps">Pinned to this shot</h4>
558
+ <span className="plan-count is-numeric">{pins.length}</span>
559
+ </div>
560
+ {pins.length === 0 ? (
561
+ <p className="plan-none">Nothing pinned yet. Drop cards on this shot.</p>
562
+ ) : (
563
+ <ul className="plan-pins" aria-label="Pinned to this shot">
564
+ {pins.map((item) => (
565
+ <PinRow key={item.id} doc={doc} item={item} onSketch={onSketch.has(item.id)} readOnly={readOnly} />
566
+ ))}
567
+ </ul>
568
+ )}
569
+ </div>
570
+ );
571
+ }
572
+
573
+ function PinRow(props: { doc: Doc3D; item: ReferenceItem3D; onSketch: boolean; readOnly: boolean }) {
574
+ const { doc, item } = props;
575
+ const asset = item.type === "asset" ? doc.assets[item.asset] : undefined;
576
+ const image = asset?.kind === "image" ? asset : undefined;
577
+ const thumb = useImageThumb(image === undefined ? "" : assetUrl(image.path), image?.hash ?? "", 20);
578
+ const name = item.label.trim() || "Untitled card";
579
+ const must = referenceImportanceOf(item) === "must";
580
+ return (
581
+ <li
582
+ className="plan-pin"
583
+ data-testid={`plan-pin-${item.id}`}
584
+ draggable={!props.readOnly}
585
+ title={props.readOnly ? undefined : "Drag onto the sketch"}
586
+ onDragStart={(event) => writeDrag(event.dataTransfer, { kind: "reference", id: item.id })}
587
+ >
588
+ <span className="plan-pin-thumb" aria-hidden="true">
589
+ {image === undefined ? pinGlyph(item) : thumb === null ? null : <img src={thumb} alt="" draggable={false} />}
590
+ </span>
591
+ <span className="plan-pin-name">{name}</span>
592
+ {props.onSketch ? (
593
+ <span className="plan-pin-on" data-testid="plan-pin-on-sketch">
594
+ on sketch
595
+ </span>
596
+ ) : null}
597
+ {isRequirement(item) ? <span className={`badge${must ? " is-accent" : ""}`}>{must ? "Must" : "Optional"}</span> : null}
598
+ <span className="plan-pin-kind">{kindWord(item, doc.assets)}</span>
599
+ </li>
600
+ );
601
+ }
602
+
603
+ // ── STYLE ───────────────────────────────────────────────────────────────
604
+
605
+ /** How many Style cards every shot reads. The Style wall sits on the board's
606
+ * desk itself, so the row says the count and links nowhere. */
607
+ function StyleRow(props: { count: number }) {
608
+ const { count } = props;
609
+ return (
610
+ <div className="plan-style" data-testid="plan-style">
611
+ <h4 className="caps">Style</h4>
612
+ <p className="plan-style-text">
613
+ {count === 0 ? "No Style cards yet" : `${count} ${count === 1 ? "item applies" : "items apply"} to every shot`}
614
+ </p>
615
+ </div>
616
+ );
617
+ }