@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,656 @@
1
+ // THE BOARD'S PROJECT ASSETS: everything this Project holds, in the order an
2
+ // author reaches for it. A drop zone, the pictures, the other files, the
3
+ // components, the colors, and the brand's rules in a fold.
4
+ //
5
+ // The rows are the shared resource parts (`resourceParts`), and the verbs
6
+ // are the store's, so no panel can disagree on what a row is or what Remove
7
+ // takes out. The board's right column shows it (`boardRightPanel` "assets"),
8
+ // opened from the board's + menu.
9
+ //
10
+ // ── THE BOARD NEVER WRITES INTO A SHOT ───────────────────────────────────
11
+ //
12
+ // The board plans and the Studio makes, so on the board "Use in shot" is
13
+ // refused with `placeInShotRefusal`'s sentence. On a shot board a picture's
14
+ // button is "Add to sketch" instead (`addSketchItemFromAsset`).
15
+ //
16
+ // ── A FILE DRAGS ONLY WHERE A TARGET IS ON SCREEN ───────────────────────
17
+ //
18
+ // `assetDropTargetShown`: the board's canvas and its shot cards, which pin
19
+ // the file as a card. A font never drags: it is applied to type, never placed.
20
+ //
21
+ // ── A LINK IS A PIN, A MEDIA LINK IS REFUSED ────────────────────────────
22
+ //
23
+ // A page address dropped or pasted on the zone becomes a Whole film link card
24
+ // (`addLinkReference`, lane null). Links are never fetched, so an address
25
+ // that names a media file is refused by name: its bytes would have to come
26
+ // from somewhere, and the only honest source is the author's own download.
27
+ import { useEffect, useId, useRef, useState } from "react";
28
+ import type { ClipboardEvent, DragEvent, ReactNode } from "react";
29
+ import { effectiveResources, planAssetRemoval, soundtrackOwner } from "@dailies-studio/shot/doc";
30
+ import type { Doc3D, ScopedBrandItem3D, ShotAsset } from "@dailies-studio/shot/doc";
31
+ import { loadPeaks } from "@dailies-studio/timeline";
32
+ import { formatBytes } from "../../api.js";
33
+ import { SEARCH_ICON } from "../../icons.js";
34
+ import { useShotEditor, useStore } from "../../state/context.js";
35
+ import { useImageThumb } from "../../state/imageThumbs.js";
36
+ import { projectComponents } from "../../state/projectComponents.js";
37
+ import { assetPlaceTarget, placeInShotRefusal } from "../../state/store.js";
38
+ import { inShotContext } from "../../state/viewMode.js";
39
+ import { DOM_NOT_SUPPORTED, shotBoardOf } from "../../state/workspace.js";
40
+ import { formatDuration } from "../timeline/model.js";
41
+ import { STORYBOARD_MIME, urlFromTransfer } from "../storyboard/storyboardDrop.js";
42
+ import { ASSET_DROP_COPY, ASSET_MIME, assetDropTargetShown, writeAssetDrag } from "./assetDrag.js";
43
+ import {
44
+ BrandRow,
45
+ NameOf,
46
+ ResourceForm,
47
+ ResourceLane,
48
+ UPLOAD_ACCEPT,
49
+ actionForResource,
50
+ assetGlyph,
51
+ assetUsedInComposition,
52
+ fileNameOf,
53
+ selectedLayers,
54
+ } from "./resourceParts.js";
55
+
56
+ /**
57
+ * The board's Project assets panel in the right column: a head that names it
58
+ * and closes it, a search, and the tab under one scroller. A viewer reads
59
+ * every row locked.
60
+ */
61
+ export function BoardAssets() {
62
+ const store = useStore();
63
+ const readOnly = useShotEditor((s) => s.readOnly);
64
+ const [query, setQuery] = useState("");
65
+ return (
66
+ <section className="board-assets" data-testid="board-assets" aria-label="Project assets">
67
+ <header className="board-assets-head">
68
+ <h2 className="board-assets-title">Project assets</h2>
69
+ <button
70
+ type="button"
71
+ className="btn is-icon"
72
+ data-testid="board-assets-close"
73
+ aria-label="Close Project assets"
74
+ title="Close Project assets"
75
+ onClick={() => store.setBoardAssetsOpen(false)}
76
+ >
77
+ ×
78
+ </button>
79
+ </header>
80
+ <div className="board-assets-search">
81
+ <label className="search-field">
82
+ {SEARCH_ICON}
83
+ <input
84
+ type="search"
85
+ value={query}
86
+ placeholder="Search files"
87
+ aria-label="Search files"
88
+ onChange={(event) => setQuery(event.target.value)}
89
+ />
90
+ </label>
91
+ </div>
92
+ <div className="rail-scroll">
93
+ <AssetsTab query={query} readOnly={readOnly} />
94
+ </div>
95
+ </section>
96
+ );
97
+ }
98
+
99
+ export function AssetsTab({
100
+ readOnly = false,
101
+ query = "",
102
+ }: {
103
+ readOnly?: boolean;
104
+ /** The rail's search box. Filters every lane by the name the author sees. */
105
+ query?: string;
106
+ }) {
107
+ const store = useStore();
108
+ const doc = useShotEditor((state) => state.doc);
109
+ const name = useShotEditor((state) => state.name);
110
+ const usage = useShotEditor((state) => state.assetUsage);
111
+ const library = useShotEditor((state) => state.library);
112
+ const libraryLoaded = useShotEditor((state) => state.libraryLoaded);
113
+ const homes = useShotEditor((state) => state.componentHomes);
114
+ // The Shot this tab works on. The board enters none, so a Shot the Studio
115
+ // left open behind it is not one: no "This shot only" lane, and no Apply
116
+ // onto layers selected there.
117
+ const enteredScene = useShotEditor((state) => (state.storyboardOpen ? null : state.enteredScene));
118
+ const editingDef = useShotEditor((state) => state.editingDef);
119
+ const selection = useShotEditor((state) => state.selection);
120
+ const dropShown = useShotEditor(assetDropTargetShown);
121
+ // The target's NAME, a string or null, so the selector allocates nothing.
122
+ const targetName = useShotEditor((state) => {
123
+ const id = assetPlaceTarget(state);
124
+ return id === null ? null : (state.doc?.scenesById[id]?.name ?? null);
125
+ });
126
+ // Why Use would refuse. One hidden copy that every Use points at, since the
127
+ // reason is the tab's state, not a row's.
128
+ const refusal = useShotEditor(placeInShotRefusal);
129
+ // A sound joins the Shot's audio, so a leaf isolation does not refuse it.
130
+ // It is either `refusal` or null, so the one hidden copy still describes it.
131
+ const soundRefusal = useShotEditor((state) => placeInShotRefusal(state, "sound"));
132
+ const refusalId = useId();
133
+ // A shot board's pictures go on its sketch.
134
+ const sketching = useShotEditor((state) => shotBoardOf(state) !== null);
135
+ const [form, setForm] = useState<"color" | "guidance" | null>(null);
136
+
137
+ // The meter is server truth, refreshed on entry rather than trusted stale.
138
+ useEffect(() => {
139
+ if (name !== null) void store.refreshAssetUsage();
140
+ }, [name, store]);
141
+ // The registry rows the Components lane lists are the Project's own
142
+ // (`projectComponents`), so the registry has to be read once.
143
+ useEffect(() => {
144
+ if (!libraryLoaded) void store.refreshLibrary();
145
+ }, [libraryLoaded, store]);
146
+ // And which Project holds which file (`componentHomes`), or a folder-mate
147
+ // another Project lists or uses folds in here as this Project's.
148
+ useEffect(() => {
149
+ if (homes === null) void store.refreshWorkspaceComponents();
150
+ }, [homes, store]);
151
+
152
+ if (doc === null) return null;
153
+ const needle = query.trim().toLowerCase();
154
+ const matches = (text: string): boolean => needle === "" || text.toLowerCase().includes(needle);
155
+ const report = (error: unknown): void => store.reportError(error instanceof Error ? error.message : String(error));
156
+
157
+ const assets = Object.values(doc.assets);
158
+ const unused = new Set(planAssetRemoval(doc).remove);
159
+ const sizeByHash = new Map((usage?.assets ?? []).map((a) => [a.hash, a.size]));
160
+ // Gone from storage. The stage's notice offers Relink and Remove.
161
+ const missing = new Set((usage?.missing ?? []).map((row) => row.id));
162
+ const shownAssets = assets.filter((asset) => matches(fileNameOf(asset)));
163
+ const images = shownAssets.filter((asset) => asset.kind === "image");
164
+ const files = shownAssets.filter((asset) => asset.kind !== "image");
165
+
166
+ // The Project's rows, whatever Shot is entered, and the entered Shot's own
167
+ // in their own lane.
168
+ const project = effectiveResources(doc, null).branding;
169
+ const shot = enteredScene === null ? undefined : doc.scenesById[enteredScene];
170
+ const shotBranding =
171
+ shot === undefined ? [] : effectiveResources(doc, enteredScene).branding.filter((item) => item.scope === "shot");
172
+ const shadowedKeys = new Set(shotBranding.map((item) => item.key));
173
+ const brandAssets = new Set(
174
+ [...project, ...shotBranding].flatMap((item) => (item.type === "asset" ? [item.asset] : [])),
175
+ );
176
+ const colors = project.filter((item) => item.type === "color" && matches(item.label));
177
+ const rules = project.filter((item) => item.type !== "color" && matches(item.label));
178
+ const shotRows = shotBranding.filter((item) => matches(item.label));
179
+
180
+ const definitions = Object.values(doc.components ?? {}).filter((def) => matches(def.name));
181
+ const registry = projectComponents(doc, library, homes).project.filter((row) => matches(row.name));
182
+
183
+ const inShot = inShotContext({ enteredScene, editingDef });
184
+ const selected = selectedLayers(doc, editingDef ?? enteredScene, selection);
185
+
186
+ /** Not on a soundtrack whose video stays: that link would name a file that
187
+ * is gone. It leaves with its video. */
188
+ const removable = (asset: ShotAsset): boolean =>
189
+ !readOnly && unused.has(asset.id) && soundtrackOwner(doc, asset.id) === null;
190
+ const removeButton = (asset: ShotAsset): ReactNode => (
191
+ <button
192
+ type="button"
193
+ className="assets-tab-remove"
194
+ aria-label={`Remove ${asset.path} from this Project`}
195
+ title="Remove from this Project"
196
+ onClick={() => store.removeUnusedAssets([asset.id])}
197
+ >
198
+ ×
199
+ </button>
200
+ );
201
+ /** "Use in shot", or a refusal that says why: `aria-disabled` rather than
202
+ * `disabled`, so Tab still reaches it and the click reports the sentence. */
203
+ const useButton = (
204
+ label: string,
205
+ onUse: () => void,
206
+ reason: string | null = refusal,
207
+ reasonId: string = refusalId,
208
+ ): ReactNode =>
209
+ readOnly ? null : (
210
+ <button
211
+ type="button"
212
+ className="btn assets-tab-use"
213
+ aria-label={`Use in shot: ${label}${targetName === null ? "" : `, ${targetName}`}`}
214
+ aria-disabled={reason === null ? undefined : "true"}
215
+ aria-describedby={reason === null ? undefined : reasonId}
216
+ title={reason ?? `Use in ${targetName}`}
217
+ onClick={() => {
218
+ if (reason !== null && reason !== refusal) {
219
+ store.reportError(reason);
220
+ return;
221
+ }
222
+ onUse();
223
+ }}
224
+ >
225
+ Use in shot
226
+ </button>
227
+ );
228
+ /** "Add to sketch" on a shot board, for an image or a video; else Use. */
229
+ const useOrSketch = (asset: ShotAsset, reason: string | null = refusal): ReactNode =>
230
+ sketching && (asset.kind === "image" || asset.kind === "video") ? (
231
+ readOnly ? null : (
232
+ <button
233
+ type="button"
234
+ className="btn assets-tab-use"
235
+ aria-label={`Add to sketch: ${fileNameOf(asset)}`}
236
+ onClick={() => void store.addSketchItemFromAsset(asset.id)}
237
+ >
238
+ Add to sketch
239
+ </button>
240
+ )
241
+ ) : (
242
+ useButton(fileNameOf(asset), () => void store.useAssetInShot(asset.id), reason)
243
+ );
244
+ const dragProps = (asset: ShotAsset) => {
245
+ // READ-ONLY IS NOT DRAGGABLE, a font never is, and neither is a file
246
+ // with no drop target on screen.
247
+ const draggable = !readOnly && dropShown && asset.kind !== "font";
248
+ return {
249
+ draggable,
250
+ ...(draggable
251
+ ? {
252
+ title: ASSET_DROP_COPY,
253
+ onDragStart: (event: DragEvent<HTMLElement>) => {
254
+ if (event.dataTransfer !== null) writeAssetDrag(event.dataTransfer, { asset: asset.id });
255
+ },
256
+ }
257
+ : {}),
258
+ };
259
+ };
260
+
261
+ const brandRow = (item: ScopedBrandItem3D) => {
262
+ const asset = item.type === "asset" ? doc.assets[item.asset] : undefined;
263
+ return (
264
+ <BrandRow
265
+ key={item.id}
266
+ item={item}
267
+ {...(asset === undefined ? {} : { asset })}
268
+ used={asset === undefined ? false : assetUsedInComposition(doc, asset.id)}
269
+ readOnly={readOnly}
270
+ canOverride={item.scope === "project" && shot !== undefined && !shadowedKeys.has(item.key)}
271
+ shadowed={item.scope === "project" && shadowedKeys.has(item.key)}
272
+ action={actionForResource(item, asset, selected, inShot)}
273
+ onUse={() => void store.useBrandResource(item.id).catch(report)}
274
+ onOverride={() => store.overrideBrandResource(item.id)}
275
+ onRemove={() => store.removeResource("branding", item.id, item.scope)}
276
+ />
277
+ );
278
+ };
279
+
280
+ return (
281
+ <div className={`assets-tab resource-stack${readOnly ? " is-readonly" : ""}`} data-testid="assets-tab">
282
+ {/* `hidden` keeps it off screen, and `aria-describedby` still reads it. */}
283
+ {refusal === null ? null : (
284
+ <span id={refusalId} hidden>
285
+ {refusal}
286
+ </span>
287
+ )}
288
+ {readOnly ? null : <AssetsDropZone />}
289
+
290
+ <div className="assets-tab-head">
291
+ {usage === null ? null : (
292
+ <span className="assets-tab-meter" data-testid="assets-tab-meter">
293
+ {formatBytes(usage.limitBytes - usage.remainingBytes)} / {formatBytes(usage.limitBytes)}
294
+ </span>
295
+ )}
296
+ {readOnly || unused.size === 0 ? null : (
297
+ <button
298
+ type="button"
299
+ title="Take the files nothing in this Project uses out of it. Undo brings them back."
300
+ onClick={() => store.removeUnusedAssets()}
301
+ >
302
+ Remove unused ({unused.size})
303
+ </button>
304
+ )}
305
+ </div>
306
+
307
+ <ResourceLane title="Images" count={images.length} readOnly={false} actions={null}>
308
+ {images.length === 0 ? (
309
+ <div className="resource-empty">{needle === "" ? "No images yet." : "No image names match this search."}</div>
310
+ ) : (
311
+ <ul className="assets-tab-images" data-testid="assets-tab-images">
312
+ {images.map((asset) => (
313
+ <li key={asset.id} className="assets-tab-image" data-asset={asset.id} {...dragProps(asset)}>
314
+ <span className="assets-tab-thumb">
315
+ {/* Not draggable itself, so the drag is the TILE's and carries
316
+ the registry id rather than the image's URL. */}
317
+ <AssetThumb asset={asset} />
318
+ {brandAssets.has(asset.id) ? <span className="assets-tab-chip is-brand">Brand</span> : null}
319
+ {unused.has(asset.id) ? <span className="assets-tab-chip">Unused</span> : null}
320
+ {missing.has(asset.id) ? <span className="assets-tab-chip">File missing</span> : null}
321
+ {removable(asset) ? removeButton(asset) : null}
322
+ </span>
323
+ <NameOf asset={asset} bytes={sizeByHash.get(asset.hash)} />
324
+ {useOrSketch(asset)}
325
+ </li>
326
+ ))}
327
+ </ul>
328
+ )}
329
+ </ResourceLane>
330
+
331
+ <ResourceLane title="Files" count={files.length} readOnly={false} actions={null}>
332
+ {files.length === 0 ? (
333
+ <div className="resource-empty">{needle === "" ? "No other files yet." : "No file names match this search."}</div>
334
+ ) : (
335
+ <ul className="resource-list" data-testid="assets-tab-files">
336
+ {files.map((asset) => (
337
+ <li key={asset.id} className="resource-row assets-tab-file" data-asset={asset.id} {...dragProps(asset)}>
338
+ <div className="resource-asset-row">
339
+ <span className="resource-thumb is-kind" aria-hidden="true">
340
+ {assetGlyph(asset)}
341
+ </span>
342
+ <NameOf asset={asset} />
343
+ <FileMeta doc={doc} asset={asset} />
344
+ {brandAssets.has(asset.id) ? <span className="resource-badge">Brand</span> : null}
345
+ {missing.has(asset.id) ? <span className="resource-badge">File missing</span> : null}
346
+ {unused.has(asset.id) ? (
347
+ <span className="resource-badge">Unused</span>
348
+ ) : assetUsedInComposition(doc, asset.id) ? (
349
+ <span className="resource-used">Used</span>
350
+ ) : null}
351
+ {sizeByHash.has(asset.hash) ? (
352
+ <span className="asset-size">{formatBytes(sizeByHash.get(asset.hash)!)}</span>
353
+ ) : null}
354
+ {asset.kind === "font" ? null : useOrSketch(asset, asset.kind === "audio" ? soundRefusal : refusal)}
355
+ {removable(asset) ? removeButton(asset) : null}
356
+ </div>
357
+ </li>
358
+ ))}
359
+ </ul>
360
+ )}
361
+ </ResourceLane>
362
+
363
+ <ResourceLane
364
+ title="Components"
365
+ count={definitions.length + registry.length}
366
+ readOnly={false}
367
+ actions={
368
+ readOnly ? null : (
369
+ <div className="resource-add-actions">
370
+ <button type="button" onClick={() => store.setLibraryBrowserOpen(true, "component")}>
371
+ + From Library
372
+ </button>
373
+ </div>
374
+ )
375
+ }
376
+ >
377
+ {definitions.length + registry.length === 0 ? (
378
+ <div className="resource-empty">
379
+ {needle === "" ? "No components in this Project yet." : "No component names match this search."}
380
+ </div>
381
+ ) : (
382
+ <ul className="resource-list" data-testid="assets-tab-components">
383
+ {definitions.map((def) => (
384
+ <li key={def.id} className="resource-row" data-component={def.id}>
385
+ <div className="resource-asset-row">
386
+ <span className="resource-thumb is-kind" aria-hidden="true">
387
+ {"</>"}
388
+ </span>
389
+ <span className="resource-label">{def.name}</span>
390
+ <span className="resource-badge">Project</span>
391
+ {useButton(def.name, () => void store.useComponentInShot(def.id))}
392
+ </div>
393
+ </li>
394
+ ))}
395
+ {registry.map((row) => (
396
+ <li key={row.id} className="resource-row" data-component={row.id}>
397
+ <div className="resource-asset-row">
398
+ <span className="resource-thumb is-kind" aria-hidden="true">
399
+ {"</>"}
400
+ </span>
401
+ <span className="resource-label" title={row.rel}>
402
+ {row.name}
403
+ </span>
404
+ <span className="resource-badge">{row.host === "three" ? "3D" : "2D"}</span>
405
+ {row.host === "three" ? (
406
+ useButton(row.name, () => void store.useComponentInShot(row.id))
407
+ ) : (
408
+ <>
409
+ <span id={`${refusalId}-${row.id}`} hidden>
410
+ {DOM_NOT_SUPPORTED}
411
+ </span>
412
+ {useButton(
413
+ row.name,
414
+ () => void store.useComponentInShot(row.id),
415
+ DOM_NOT_SUPPORTED,
416
+ `${refusalId}-${row.id}`,
417
+ )}
418
+ </>
419
+ )}
420
+ </div>
421
+ </li>
422
+ ))}
423
+ </ul>
424
+ )}
425
+ </ResourceLane>
426
+
427
+ <ResourceLane title="Colors" count={colors.length} readOnly={false} actions={null}>
428
+ <ul className="assets-tab-swatches" data-testid="assets-tab-colors">
429
+ {colors.map((item) =>
430
+ item.type !== "color" ? null : (
431
+ <li key={item.id} className="assets-tab-swatch" data-resource={item.id} title={`${item.label} ${item.value}`}>
432
+ <span className="assets-tab-swatch-chip" style={{ backgroundColor: item.value }} aria-hidden="true" />
433
+ <span className="assets-tab-swatch-label">{item.label}</span>
434
+ {readOnly ? null : (
435
+ <button
436
+ type="button"
437
+ className="assets-tab-remove"
438
+ aria-label={`Remove ${item.label}`}
439
+ onClick={() => store.removeResource("branding", item.id, item.scope)}
440
+ >
441
+ ×
442
+ </button>
443
+ )}
444
+ </li>
445
+ ),
446
+ )}
447
+ {readOnly ? null : (
448
+ <li>
449
+ <button
450
+ type="button"
451
+ className="assets-tab-swatch-add"
452
+ aria-label="Add a color"
453
+ aria-expanded={form === "color"}
454
+ onClick={() => setForm(form === "color" ? null : "color")}
455
+ >
456
+ +
457
+ </button>
458
+ </li>
459
+ )}
460
+ </ul>
461
+ {form === "color" ? (
462
+ <ResourceForm
463
+ kind="color"
464
+ onCancel={() => setForm(null)}
465
+ onSubmit={(label, value) => {
466
+ store.addBrandColor(label, value, "project");
467
+ setForm(null);
468
+ }}
469
+ />
470
+ ) : null}
471
+ </ResourceLane>
472
+
473
+ <ResourceLane
474
+ title="Brand rules"
475
+ count={rules.length}
476
+ readOnly={readOnly}
477
+ foldable
478
+ actions={
479
+ readOnly ? null : (
480
+ <div className="resource-add-actions">
481
+ <button type="button" onClick={() => setForm(form === "guidance" ? null : "guidance")}>
482
+ + Rule
483
+ </button>
484
+ </div>
485
+ )
486
+ }
487
+ >
488
+ {form === "guidance" ? (
489
+ <ResourceForm
490
+ kind="guidance"
491
+ onCancel={() => setForm(null)}
492
+ onSubmit={(label, value) => {
493
+ store.addBrandGuidance(label, value, "project");
494
+ setForm(null);
495
+ }}
496
+ />
497
+ ) : null}
498
+ {rules.length === 0 ? (
499
+ <div className="resource-empty">No brand rules yet.</div>
500
+ ) : (
501
+ <ul className="resource-list" data-testid="assets-tab-rules">
502
+ {rules.map(brandRow)}
503
+ </ul>
504
+ )}
505
+ </ResourceLane>
506
+
507
+ {/* ONLY WHEN THERE IS SOMETHING IN IT. */}
508
+ {shot === undefined || shotRows.length === 0 ? null : (
509
+ <ResourceLane title="This shot only" count={shotRows.length} meta={shot.name} readOnly={readOnly} actions={null}>
510
+ <ul className="resource-list" data-testid="assets-tab-shot-only">
511
+ {shotRows.map(brandRow)}
512
+ </ul>
513
+ </ResourceLane>
514
+ )}
515
+ </div>
516
+ );
517
+ }
518
+
519
+ /**
520
+ * The dashed zone at the top of the tab: a picker for many files, a drop of
521
+ * many files, and a paste. Files upload one at a time, in order, each its own
522
+ * undo entry (`addAsset`). With no files, a page address is a Whole film link
523
+ * card. Our own drags (an asset, a board card) are not taken.
524
+ */
525
+ export function AssetsDropZone() {
526
+ const store = useStore();
527
+ const picker = useRef<HTMLInputElement | null>(null);
528
+ const [uploading, setUploading] = useState<string | null>(null);
529
+ const [over, setOver] = useState(false);
530
+
531
+ const upload = async (picked: readonly File[]): Promise<void> => {
532
+ for (const file of picked) {
533
+ setUploading(file.name);
534
+ try {
535
+ await store.addAsset(file);
536
+ } catch (error) {
537
+ store.reportError(error instanceof Error ? error.message : String(error));
538
+ }
539
+ }
540
+ setUploading(null);
541
+ };
542
+ const take = (transfer: DataTransfer): boolean => {
543
+ const types = Array.from(transfer.types ?? []);
544
+ if (types.includes(ASSET_MIME) || types.includes(STORYBOARD_MIME)) return false;
545
+ const picked = Array.from(transfer.files ?? []);
546
+ if (picked.length > 0) {
547
+ void upload(picked);
548
+ return true;
549
+ }
550
+ const url = urlFromTransfer(transfer);
551
+ if (url === null) return false;
552
+ store.addLinkReference(url, null, { select: false });
553
+ return true;
554
+ };
555
+ const accepts = (event: DragEvent<HTMLElement>): boolean => {
556
+ const types = Array.from(event.dataTransfer?.types ?? []);
557
+ if (types.includes(ASSET_MIME) || types.includes(STORYBOARD_MIME)) return false;
558
+ return types.includes("Files") || types.includes("text/uri-list") || types.includes("text/plain");
559
+ };
560
+
561
+ return (
562
+ <div
563
+ className={`assets-drop-zone${over ? " is-over" : ""}`}
564
+ data-testid="assets-drop-zone"
565
+ onDragOver={(event) => {
566
+ if (!accepts(event)) return;
567
+ event.preventDefault();
568
+ event.dataTransfer.dropEffect = "copy";
569
+ setOver(true);
570
+ }}
571
+ onDragLeave={() => setOver(false)}
572
+ onDrop={(event) => {
573
+ setOver(false);
574
+ if (!accepts(event)) return;
575
+ event.preventDefault();
576
+ // A drop is on purpose, so one that adds nothing says why. A paste
577
+ // that adds nothing stays quiet: it falls through to the field.
578
+ if (!take(event.dataTransfer)) store.reportError("Drop files here, or an https link.");
579
+ }}
580
+ onPaste={(event: ClipboardEvent<HTMLElement>) => {
581
+ if (take(event.clipboardData)) event.preventDefault();
582
+ }}
583
+ >
584
+ <input
585
+ ref={picker}
586
+ type="file"
587
+ hidden
588
+ multiple
589
+ accept={UPLOAD_ACCEPT}
590
+ data-testid="assets-drop-input"
591
+ onChange={(event) => {
592
+ const picked = Array.from(event.target.files ?? []);
593
+ event.target.value = "";
594
+ void upload(picked);
595
+ }}
596
+ />
597
+ <button type="button" onClick={() => picker.current?.click()} disabled={uploading !== null}>
598
+ Add files
599
+ </button>
600
+ <span className="assets-drop-zone-hint">or drop files, or paste a link</span>
601
+ {uploading === null ? null : <span className="resource-pending">Uploading {uploading}…</span>}
602
+ </div>
603
+ );
604
+ }
605
+
606
+ /** A 100px tile's picture from the shared thumbnail cache (`useImageThumb`),
607
+ * never the full-size original decoded for a tile. */
608
+ function AssetThumb({ asset }: { asset: ShotAsset }) {
609
+ const thumb = useImageThumb(`/studio/assets/${encodeURIComponent(asset.path)}`, asset.hash, 100);
610
+ return thumb === null ? null : <img src={thumb} alt="" loading="lazy" draggable={false} />;
611
+ }
612
+
613
+ /**
614
+ * What a file row says after its name: a video's or a sound's length
615
+ * (`formatDuration`), a font's family, a model's format.
616
+ *
617
+ * A length is the probe's (`media.durationSeconds`, written at import), else
618
+ * for a sound the decode the timeline shares (`loadPeaks`, one per path). Its
619
+ * 0 is "could not decode", which is not a length, so it prints nothing.
620
+ */
621
+ function FileMeta({ doc, asset }: { doc: Doc3D; asset: ShotAsset }) {
622
+ const probed = asset.media?.durationSeconds;
623
+ const [decoded, setDecoded] = useState<number | null>(null);
624
+ useEffect(() => {
625
+ if (asset.kind !== "audio" || probed !== undefined) return;
626
+ let alive = true;
627
+ loadPeaks(asset)
628
+ .then((data) => {
629
+ if (alive && data.sourceSeconds > 0) setDecoded(data.sourceSeconds);
630
+ })
631
+ .catch(() => {});
632
+ return () => {
633
+ alive = false;
634
+ };
635
+ }, [asset, probed]);
636
+
637
+ let text: string | null = null;
638
+ if (asset.kind === "video" || asset.kind === "audio") {
639
+ const seconds = probed ?? decoded;
640
+ text = seconds === null ? null : formatDuration(seconds);
641
+ } else if (asset.kind === "font") {
642
+ text = fontFamilyOf(doc, asset);
643
+ } else if (asset.kind === "model") {
644
+ text = /\.([a-z0-9]+)$/i.exec(asset.path)?.[1]?.toUpperCase() ?? null;
645
+ }
646
+ return text === null ? null : <span className="assets-tab-meta">{text}</span>;
647
+ }
648
+
649
+ /** A brand row's family for this font, else the file's stem. */
650
+ function fontFamilyOf(doc: Doc3D, asset: ShotAsset): string {
651
+ const named = effectiveResources(doc, null).branding.find(
652
+ (item) => item.type === "asset" && item.asset === asset.id && item.fontFamily !== undefined,
653
+ );
654
+ if (named?.type === "asset" && named.fontFamily !== undefined) return named.fontFamily;
655
+ return fileNameOf(asset).replace(/\.[^.]+$/, "");
656
+ }