@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,723 @@
1
+ // THE INSERT DRAWER: everything an author can add, in the left column.
2
+ //
3
+ // ── WHERE IT LIVES ──────────────────────────────────────────────────────
4
+ //
5
+ // Docked in the left cell, over the outline (or a mode's column), never a
6
+ // modal and never floating over the stage. LeftColumn mounts it directly and
7
+ // marks the rail `is-inserting`, which hides the Files panel and the mode slot
8
+ // without unmounting them, so the outline keeps its scroll and open rows for
9
+ // the trip back. The dock's + (InsertButton), the Scene section's "Shader
10
+ // background…" and the first open of an empty Project all open the same
11
+ // drawer (`openInsertDrawer`).
12
+ //
13
+ // ── NO TABS ─────────────────────────────────────────────────────────────
14
+ //
15
+ // The left column has no tabs (a founder ruling), so the drawer is one search
16
+ // field, a row of jump chips and ONE scrolling list with sticky section heads.
17
+ // A chip scrolls the list; scrolling the list moves the chip.
18
+ //
19
+ // ── WHAT EACH SECTION READS ─────────────────────────────────────────────
20
+ //
21
+ // - Basics: `useAddRows`, the same rows calling the same `run` the Insert
22
+ // button uses. Nothing here knows what a shape or a device is.
23
+ // - Icons: Lucide, imported the first time the section is asked for
24
+ // (`loadIconSet`), so first paint pays nothing for 1850 icons.
25
+ // - Shaders: the registry's TSL effects (`row.effect`), grouped by the
26
+ // catalog's own category, in the order the catalog lists them.
27
+ // - Backgrounds: scene paints written to the Shot's `background` through the
28
+ // inspector's own seam (`patchScene`), and the effects tagged "ground".
29
+ //
30
+ // ── VIEWERS ─────────────────────────────────────────────────────────────
31
+ //
32
+ // Nothing renders for a viewer: the + is hidden too, and the store's set()
33
+ // seam closes the drawer when `readOnly` turns on.
34
+ import { createElement, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
35
+ import type { CSSProperties, ReactElement, ReactNode } from "react";
36
+ import { createPortal } from "react-dom";
37
+ import { paintCss } from "@dailies-studio/shot/doc";
38
+ import type { IconNode, Paint } from "@dailies-studio/shot/doc";
39
+ import type { ComponentRow } from "../../api.js";
40
+ import { useShotEditor, useStore } from "../../state/context.js";
41
+ import { INSERT_SECTIONS } from "../../state/types.js";
42
+ import type { InsertSection } from "../../state/types.js";
43
+ import { isSceneContext } from "../../state/viewMode.js";
44
+ import { sceneContextRefusal } from "../../state/keymap.js";
45
+ import { isolationInsertRefusal } from "../../state/isolationTrail.js";
46
+ import { ADD_FAMILIES, promotedBuiltinLabel } from "../../addFamilies.js";
47
+ import {
48
+ ADD_ICON,
49
+ CLOSE_ICON,
50
+ IMAGE_UP_ICON,
51
+ LIBRARY_ICON,
52
+ MORE_ICON,
53
+ SEARCH_ICON,
54
+ addRowIcon,
55
+ effectCategoryIcon,
56
+ } from "../../icons.js";
57
+ import { useAddRows } from "../toolbar/useAddRows.js";
58
+ import type { AddRow } from "../toolbar/useAddRows.js";
59
+ import { useDismiss } from "../toolbar/useDismiss.js";
60
+ import { useFixedVirtualWindow } from "../virtualWindow.js";
61
+ import { patchScene } from "../inspector/scene/SceneSection.js";
62
+ import { loadIconSet, searchIcons } from "./iconSet.js";
63
+ import type { IconEntry } from "./iconSet.js";
64
+
65
+ /** One icon cell (32) and the grid's gap (2). */
66
+ const ICON_PITCH = 34;
67
+ /** The icon grid's own scroller shows this many rows. */
68
+ const ICON_ROWS_SHOWN = 7;
69
+ /** Columns before the grid has been measured (jsdom, the first frame). */
70
+ const ICON_COLS_FALLBACK = 6;
71
+
72
+ /** The effects a Backgrounds list offers: the catalog tags them "ground". */
73
+ const GROUND_TAG = "ground";
74
+
75
+ /** The scene paint presets. Solids first, then the gradients, as drawn. */
76
+ export const SCENE_PAINTS: readonly Paint[] = [
77
+ { type: "solid", color: "#000000" },
78
+ { type: "solid", color: "#0a0a0a" },
79
+ { type: "solid", color: "#0e1a2b" },
80
+ { type: "solid", color: "#f5f5f5" },
81
+ { type: "solid", color: "#ff4d1f" },
82
+ { type: "solid", color: "#0099ff" },
83
+ { type: "linear-gradient", angleDeg: 135, stops: [{ at: 0, color: "#1b2a4a" }, { at: 1, color: "#0a0a0a" }] },
84
+ { type: "linear-gradient", angleDeg: 135, stops: [{ at: 0, color: "#ff8a3d" }, { at: 1, color: "#7a2bff" }] },
85
+ { type: "linear-gradient", angleDeg: 180, stops: [{ at: 0, color: "#f5f5f5" }, { at: 1, color: "#bfc6d1" }] },
86
+ { type: "radial-gradient", stops: [{ at: 0, color: "#2b3a67" }, { at: 1, color: "#05070d" }] },
87
+ { type: "radial-gradient", stops: [{ at: 0, color: "#ffe1c2" }, { at: 1, color: "#ff5c69" }] },
88
+ { type: "linear-gradient", angleDeg: 135, stops: [{ at: 0, color: "#2fd4bf" }, { at: 1, color: "#0e1a2b" }] },
89
+ ];
90
+
91
+ /** The swatch side the stylesheet gives a swatch (insert-drawer.css). */
92
+ const SWATCH_PX = 32;
93
+ /** The frame before a document is open. */
94
+ const SQUARE_FRAME = { width: 1, height: 1 } as const;
95
+
96
+ /** A scene paint swatch: the Project frame in small, so it shows what the
97
+ * stage draws over that frame. The box is the frame's aspect inside the
98
+ * stylesheet's square, and the paint is `paintCss` over the frame, the
99
+ * stage's own geometry (`paintCss` reads only the aspect). A raw
100
+ * `${angleDeg}deg` read the angle as CSS's, so a 135 preset ran top-left to
101
+ * bottom-right here and the other way on stage, and a square box showed a
102
+ * diagonal at the wrong angle on any frame that is not square. */
103
+ function swatchStyle(paint: Paint, frame: { width: number; height: number }): CSSProperties {
104
+ const { width, height } = frame;
105
+ const wide = width >= height;
106
+ return {
107
+ width: wide ? SWATCH_PX : (SWATCH_PX * width) / height,
108
+ height: wide ? (SWATCH_PX * height) / width : SWATCH_PX,
109
+ background: paintCss(paint, width, height),
110
+ };
111
+ }
112
+
113
+ function paintName(paint: Paint): string {
114
+ if (paint.type === "solid") return paint.color;
115
+ const stops = paint.stops.map((s) => s.color).join(" to ");
116
+ return `${paint.type === "linear-gradient" ? "Linear" : "Radial"} ${stops}`;
117
+ }
118
+
119
+ const samePaint = (a: Paint | undefined, b: Paint): boolean => JSON.stringify(a) === JSON.stringify(b);
120
+
121
+ /** Every word of the query appears in the text. */
122
+ function matches(text: string, query: string): boolean {
123
+ const q = query.trim().toLowerCase();
124
+ if (q === "") return true;
125
+ const hay = text.toLowerCase();
126
+ return q.split(/\s+/).every((word) => hay.includes(word));
127
+ }
128
+
129
+ /** A Basics tile's caption: the drawer's own short words for two rows. */
130
+ function captionOf(row: AddRow): string {
131
+ if (row.key === "media") return "Media…";
132
+ if (row.key === "html") return "Html";
133
+ return row.label;
134
+ }
135
+
136
+ const capitalise = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
137
+
138
+ /** Effect rows grouped by catalog category, in first-appearance order. */
139
+ function byCategory(rows: readonly ComponentRow[]): Array<{ category: string; rows: ComponentRow[] }> {
140
+ const groups = new Map<string, ComponentRow[]>();
141
+ for (const row of rows) {
142
+ const category = row.effect?.category ?? "other";
143
+ const list = groups.get(category);
144
+ if (list === undefined) groups.set(category, [row]);
145
+ else list.push(row);
146
+ }
147
+ return [...groups].map(([category, list]) => ({ category, rows: list }));
148
+ }
149
+
150
+ export function InsertDrawer() {
151
+ const open = useShotEditor((s) => s.insertDrawer !== null && !s.readOnly);
152
+ return open ? <Drawer /> : null;
153
+ }
154
+
155
+ function Drawer() {
156
+ const store = useStore();
157
+ const drawer = useShotEditor((s) => s.insertDrawer);
158
+ const above = useShotEditor((s) => s.doc === null || isSceneContext(s));
159
+ const library = useShotEditor((s) => s.library);
160
+ const searchFocus = useShotEditor((s) => s.searchFocus);
161
+ const { rows, busy, fileInput } = useAddRows({ wantLibrary: true, claimImagePick: false });
162
+ const [query, setQuery] = useState("");
163
+ const [active, setActive] = useState<InsertSection>(drawer?.section ?? "Basics");
164
+ const [menu, setMenu] = useState<{ id: string; left: number; top: number } | null>(null);
165
+ const listRef = useRef<HTMLDivElement | null>(null);
166
+ const inputRef = useRef<HTMLInputElement | null>(null);
167
+ const sectionRefs = useRef(new Map<InsertSection, HTMLElement>());
168
+
169
+ const contextRefusal = above ? sceneContextRefusal("Insert", "adds") : null;
170
+ // Every tile here adds a layer, which a leaf isolation cannot hold. Scene
171
+ // paint is the exception: it sets the Shot's background, not a layer.
172
+ const leafRefusal = useShotEditor(isolationInsertRefusal);
173
+ const refusal = contextRefusal ?? leafRefusal;
174
+ const searching = query.trim() !== "";
175
+
176
+ // `/` (focusSearch) puts the caret in the drawer's field while it is open.
177
+ // The outline's own field is hidden under it, so its focus is a no-op.
178
+ const firstFocus = useRef(searchFocus);
179
+ useEffect(() => {
180
+ if (searchFocus !== firstFocus.current) inputRef.current?.focus();
181
+ }, [searchFocus]);
182
+
183
+ // A chip, the inspector's button or an open at a section: scroll to it. Keyed
184
+ // on the drawer OBJECT, which every open makes anew, so asking for the
185
+ // section already shown still scrolls back to its head.
186
+ useLayoutEffect(() => {
187
+ if (drawer === null) return;
188
+ setActive(drawer.section);
189
+ const list = listRef.current;
190
+ const head = sectionRefs.current.get(drawer.section);
191
+ if (list === null || head === undefined) return;
192
+ list.scrollTop = head.offsetTop;
193
+ }, [drawer]);
194
+
195
+ const onScroll = (): void => {
196
+ const list = listRef.current;
197
+ if (list === null) return;
198
+ const atEnd = list.scrollTop + list.clientHeight >= list.scrollHeight - 1;
199
+ let next: InsertSection = "Basics";
200
+ for (const section of INSERT_SECTIONS) {
201
+ const head = sectionRefs.current.get(section);
202
+ if (head !== undefined && (atEnd || head.offsetTop <= list.scrollTop + 1)) next = section;
203
+ }
204
+ setActive(next);
205
+ };
206
+
207
+ const sectionRef = (section: InsertSection) => (el: HTMLElement | null) => {
208
+ if (el === null) sectionRefs.current.delete(section);
209
+ else sectionRefs.current.set(section, el);
210
+ };
211
+
212
+ // ── the rows ───────────────────────────────────────────────────────────
213
+ const basics = rows.filter((r) => r.section !== "Effects" && matches(r.label, query));
214
+ const effects = useMemo(
215
+ () => library.filter((r) => r.host === "three" && r.effect !== undefined && promotedBuiltinLabel(r.id) === null),
216
+ [library],
217
+ );
218
+ const effectText = (r: ComponentRow): string => `${r.name} ${r.effect?.category ?? ""} ${(r.effect?.tags ?? []).join(" ")}`;
219
+ const shaders = effects.filter((r) => !(r.effect?.tags.includes(GROUND_TAG) ?? false) && matches(effectText(r), query));
220
+ const grounds = effects.filter((r) => (r.effect?.tags.includes(GROUND_TAG) ?? false) && matches(effectText(r), query));
221
+ const paints = searching ? [] : SCENE_PAINTS;
222
+
223
+ const report = (e: unknown): void => store.reportError(e instanceof Error ? e.message : String(e));
224
+ const addEffect = (id: string): void => void store.addInstanceLayer(id).catch(report);
225
+ const useAsBackground = (id: string): void => void store.useAsBackground(id).catch(report);
226
+
227
+ // The "…" closes the menu on its own press (useDismiss sees a press outside
228
+ // the menu), so the click that follows must not open it straight back.
229
+ const closed = useRef<{ id: string; at: number } | null>(null);
230
+ const openMenu = (id: string, tile: HTMLElement): void => {
231
+ const last = closed.current;
232
+ if (last !== null && last.id === id && performance.now() - last.at < 400) return;
233
+ const box = tile.getBoundingClientRect();
234
+ setMenu({ id, left: box.right + 4, top: Math.max(8, Math.min(box.top, window.innerHeight - 160)) });
235
+ };
236
+ const closeMenu = (): void => {
237
+ if (menu !== null) closed.current = { id: menu.id, at: performance.now() };
238
+ setMenu(null);
239
+ };
240
+
241
+ const effectTile = (row: ComponentRow) => (
242
+ <ShaderTile
243
+ key={row.id}
244
+ row={row}
245
+ refusal={refusal}
246
+ menuOpen={menu?.id === row.id}
247
+ onAdd={() => addEffect(row.id)}
248
+ onMenu={(tile) => openMenu(row.id, tile)}
249
+ />
250
+ );
251
+
252
+ return (
253
+ <section className="insert-drawer" aria-label="Insert">
254
+ <header className="insert-head">
255
+ <span className="insert-title">Insert</span>
256
+ <kbd className="insert-hint">Esc</kbd>
257
+ <button
258
+ type="button"
259
+ className="insert-close"
260
+ aria-label="Close Insert"
261
+ title="Close (Esc)"
262
+ onClick={() => store.closeInsertDrawer()}
263
+ >
264
+ {CLOSE_ICON}
265
+ </button>
266
+ </header>
267
+ <div className="insert-search-row">
268
+ <label className={`insert-search${searching ? " has-query" : ""}`}>
269
+ <span className="insert-search-icon" aria-hidden="true">
270
+ {SEARCH_ICON}
271
+ </span>
272
+ <input
273
+ ref={inputRef}
274
+ type="search"
275
+ className="insert-search-input"
276
+ placeholder="Search"
277
+ aria-label="Search everything you can insert"
278
+ value={query}
279
+ onChange={(e) => setQuery(e.currentTarget.value)}
280
+ onKeyDown={(e) => {
281
+ if (e.key !== "Escape") return;
282
+ // The field's Escape: a query clears first, then the drawer
283
+ // closes. Stopped here so no stage lane sees it.
284
+ e.preventDefault();
285
+ e.stopPropagation();
286
+ if (query !== "") setQuery("");
287
+ else store.closeInsertDrawer();
288
+ }}
289
+ />
290
+ {searching ? (
291
+ <button type="button" className="insert-search-clear" aria-label="Clear search" onClick={() => setQuery("")}>
292
+ {CLOSE_ICON}
293
+ </button>
294
+ ) : (
295
+ <kbd className="insert-hint">/</kbd>
296
+ )}
297
+ </label>
298
+ </div>
299
+ <nav className="insert-chips" aria-label="Jump to a section">
300
+ {INSERT_SECTIONS.map((section) => (
301
+ <button
302
+ key={section}
303
+ type="button"
304
+ className={`insert-chip${active === section ? " is-active" : ""}`}
305
+ aria-current={active === section ? "true" : undefined}
306
+ onClick={() => store.openInsertDrawer(section)}
307
+ >
308
+ {section}
309
+ </button>
310
+ ))}
311
+ </nav>
312
+ <div ref={listRef} className="insert-list" onScroll={onScroll}>
313
+ <Section name="Basics" count={basics.length} sectionRef={sectionRef("Basics")}>
314
+ {ADD_FAMILIES.map((family) => {
315
+ const members = basics.filter((r) => r.family === family);
316
+ if (members.length === 0) return null;
317
+ return (
318
+ <div key={family} className="insert-family">
319
+ <div className="insert-sub">{family}</div>
320
+ <div className="insert-tiles">
321
+ {members.map((row) => {
322
+ const why =
323
+ refusal ?? (row.disabled === true ? (row.title ?? row.label) : null) ??
324
+ (busy && (row.key === "media" || row.key === "mesh") ? "Waiting for the last file to finish importing" : null);
325
+ return (
326
+ <button
327
+ key={row.key}
328
+ type="button"
329
+ className="insert-tile"
330
+ disabled={why !== null}
331
+ title={why ?? row.title ?? row.label}
332
+ onClick={row.run}
333
+ >
334
+ <span className="insert-tile-icon" aria-hidden="true">
335
+ {addRowIcon(row.key)}
336
+ </span>
337
+ <span className="insert-tile-label">{captionOf(row)}</span>
338
+ </button>
339
+ );
340
+ })}
341
+ </div>
342
+ </div>
343
+ );
344
+ })}
345
+ </Section>
346
+ <IconsSection
347
+ query={query}
348
+ asked={drawer?.section === "Icons"}
349
+ refusal={refusal}
350
+ listRef={listRef}
351
+ sectionRef={sectionRef("Icons")}
352
+ />
353
+ {/* NOT VIRTUALISED, unlike Icons: the catalog's three-host effects
354
+ are a few dozen tiles, and each thumb is `loading="lazy"`, so the
355
+ browser fetches only the ones scrolled near. A windowed grid is
356
+ for the 1850 icons. Revisit if the registry grows into hundreds. */}
357
+ <Section name="Shaders" count={shaders.length} sectionRef={sectionRef("Shaders")}>
358
+ {byCategory(shaders).map((group) => (
359
+ <div key={group.category} className="insert-family">
360
+ <div className="insert-sub">
361
+ {capitalise(group.category)}
362
+ <span className="insert-count">{group.rows.length}</span>
363
+ </div>
364
+ <div className="insert-tiles">{group.rows.map(effectTile)}</div>
365
+ </div>
366
+ ))}
367
+ {searching ? null : (
368
+ <button type="button" className="insert-link" onClick={() => store.setLibraryBrowserOpen(true)}>
369
+ <span className="insert-link-icon" aria-hidden="true">
370
+ {LIBRARY_ICON}
371
+ </span>
372
+ Materials and blueprints are in the Library
373
+ </button>
374
+ )}
375
+ </Section>
376
+ <Section name="Backgrounds" count={paints.length + grounds.length} sectionRef={sectionRef("Backgrounds")}>
377
+ {paints.length === 0 ? null : <ScenePaints refusal={contextRefusal} />}
378
+ {grounds.length === 0 ? null : (
379
+ <div className="insert-family">
380
+ <div className="insert-sub">
381
+ Shader grounds
382
+ <span className="insert-count">{grounds.length}</span>
383
+ </div>
384
+ <div className="insert-tiles">{grounds.map(effectTile)}</div>
385
+ </div>
386
+ )}
387
+ </Section>
388
+ {searching ? (
389
+ <p className="insert-footnote">Search looks in every section. A section with no match stays as one line.</p>
390
+ ) : null}
391
+ </div>
392
+ {menu === null ? null : (
393
+ <TileMenu
394
+ left={menu.left}
395
+ top={menu.top}
396
+ onClose={closeMenu}
397
+ onAdd={() => {
398
+ closeMenu();
399
+ addEffect(menu.id);
400
+ }}
401
+ onBackground={() => {
402
+ closeMenu();
403
+ useAsBackground(menu.id);
404
+ }}
405
+ />
406
+ )}
407
+ {fileInput}
408
+ </section>
409
+ );
410
+ }
411
+
412
+ function Section(props: {
413
+ name: InsertSection;
414
+ count: number | null;
415
+ sectionRef: (el: HTMLElement | null) => void;
416
+ children?: ReactNode;
417
+ }) {
418
+ return (
419
+ <section ref={props.sectionRef} className="insert-section" aria-label={props.name} data-section={props.name}>
420
+ <div className="insert-section-head">
421
+ <span className="insert-section-label">{props.name}</span>
422
+ {props.count === null ? null : <span className="insert-count">{props.count}</span>}
423
+ </div>
424
+ {props.count === 0 ? null : props.children}
425
+ </section>
426
+ );
427
+ }
428
+
429
+ /** A shader tile: its thumb (or its category's glyph), its name, and a "…"
430
+ * that offers "Use as background". A click adds it to the shot. */
431
+ function ShaderTile(props: {
432
+ row: ComponentRow;
433
+ refusal: string | null;
434
+ menuOpen: boolean;
435
+ onAdd: () => void;
436
+ onMenu: (tile: HTMLElement) => void;
437
+ }) {
438
+ const { row } = props;
439
+ const [broken, setBroken] = useState(false);
440
+ const thumb = row.effect?.thumb;
441
+ return (
442
+ <div className={`insert-shader${props.menuOpen ? " is-menu-open" : ""}`}>
443
+ <button
444
+ type="button"
445
+ className="insert-shader-add"
446
+ disabled={props.refusal !== null}
447
+ title={props.refusal ?? row.name}
448
+ onClick={props.onAdd}
449
+ >
450
+ <span className="insert-thumb">
451
+ {thumb === undefined || broken ? (
452
+ <span className="insert-thumb-glyph" aria-hidden="true">
453
+ {effectCategoryIcon(row.effect?.category ?? "")}
454
+ </span>
455
+ ) : (
456
+ <img src={thumb} alt="" loading="lazy" decoding="async" onError={() => setBroken(true)} />
457
+ )}
458
+ </span>
459
+ <span className="insert-shader-label">{row.name}</span>
460
+ </button>
461
+ <button
462
+ type="button"
463
+ className="insert-shader-more"
464
+ aria-label={`More for ${row.name}`}
465
+ aria-haspopup="menu"
466
+ aria-expanded={props.menuOpen}
467
+ disabled={props.refusal !== null}
468
+ onClick={(e) => props.onMenu(e.currentTarget.parentElement ?? e.currentTarget)}
469
+ >
470
+ {MORE_ICON}
471
+ </button>
472
+ </div>
473
+ );
474
+ }
475
+
476
+ /** The shader tile's menu. Portalled to the body, at the tile's right edge,
477
+ * so the left rail's stacking context cannot paint it under the canvas. */
478
+ function TileMenu(props: {
479
+ left: number;
480
+ top: number;
481
+ onClose: () => void;
482
+ onAdd: () => void;
483
+ onBackground: () => void;
484
+ }) {
485
+ const ref = useRef<HTMLDivElement | null>(null);
486
+ useDismiss(true, ref, props.onClose);
487
+ // The first row takes focus, so Enter adds (the row's own hint says so).
488
+ useLayoutEffect(() => ref.current?.querySelector("button")?.focus(), []);
489
+ return createPortal(
490
+ <div ref={ref} className="insert-menu" role="menu" style={{ left: props.left, top: props.top }}>
491
+ <button type="button" role="menuitem" className="insert-menu-row" onClick={props.onAdd}>
492
+ <span className="insert-menu-icon" aria-hidden="true">
493
+ {ADD_ICON}
494
+ </span>
495
+ <span className="insert-menu-label">Add to shot</span>
496
+ <kbd className="insert-hint">Enter</kbd>
497
+ </button>
498
+ <button type="button" role="menuitem" className="insert-menu-row is-background" onClick={props.onBackground}>
499
+ <span className="insert-menu-icon" aria-hidden="true">
500
+ {IMAGE_UP_ICON}
501
+ </span>
502
+ <span className="insert-menu-label">Use as background</span>
503
+ </button>
504
+ <div className="insert-menu-divider" role="separator" />
505
+ <p className="insert-menu-note">Places it at the back, sized to the frame, locked and named Background. One undo.</p>
506
+ </div>,
507
+ document.body,
508
+ );
509
+ }
510
+
511
+ /** The Shot's background as presets: a click writes `background` through the
512
+ * inspector's seam, so an animated background takes a key at the playhead. */
513
+ function ScenePaints(props: { refusal: string | null }) {
514
+ const store = useStore();
515
+ const sceneId = useShotEditor((s) => s.enteredScene);
516
+ const current = useShotEditor((s) =>
517
+ s.enteredScene === null ? undefined : s.doc?.scenesById[s.enteredScene]?.background,
518
+ );
519
+ const frame = useShotEditor((s) => s.doc?.size) ?? SQUARE_FRAME;
520
+ const why = props.refusal ?? (sceneId === null ? sceneContextRefusal("Scene paint", "sets") : null);
521
+ return (
522
+ <div className="insert-family">
523
+ <div className="insert-sub">
524
+ Scene paint
525
+ <span className="insert-count">solid · gradient</span>
526
+ </div>
527
+ <div className="insert-swatches">
528
+ {SCENE_PAINTS.map((paint) => {
529
+ const on = samePaint(current, paint);
530
+ return (
531
+ <button
532
+ key={paintName(paint)}
533
+ type="button"
534
+ className={`insert-swatch${on ? " is-active" : ""}`}
535
+ style={swatchStyle(paint, frame)}
536
+ aria-pressed={on}
537
+ aria-label={paintName(paint)}
538
+ title={why ?? paintName(paint)}
539
+ disabled={why !== null}
540
+ onClick={() => {
541
+ if (sceneId === null) return;
542
+ store.edit((d) => patchScene(d, sceneId, { background: paint }, store.getState().frame));
543
+ }}
544
+ />
545
+ );
546
+ })}
547
+ </div>
548
+ </div>
549
+ );
550
+ }
551
+
552
+ /** One Lucide icon, drawn from its nodes with `createElement`: no innerHTML. */
553
+ function IconGlyph(props: { node: readonly IconNode[] }): ReactElement {
554
+ return createElement(
555
+ "svg",
556
+ {
557
+ viewBox: "0 0 24 24",
558
+ fill: "none",
559
+ stroke: "currentColor",
560
+ strokeWidth: 2,
561
+ strokeLinecap: "round",
562
+ strokeLinejoin: "round",
563
+ "aria-hidden": true,
564
+ },
565
+ ...props.node.map(([tag, attrs], index) => createElement(tag, { key: index, ...attrs })),
566
+ );
567
+ }
568
+
569
+ /**
570
+ * The Icons section. Lucide loads the first time the section is ASKED FOR:
571
+ * scrolled into view, jumped to, or searched. Its grid is its own scroller,
572
+ * seven rows tall, and only the rows near its viewport are mounted.
573
+ */
574
+ function IconsSection(props: {
575
+ query: string;
576
+ asked: boolean;
577
+ refusal: string | null;
578
+ listRef: React.RefObject<HTMLDivElement | null>;
579
+ sectionRef: (el: HTMLElement | null) => void;
580
+ }) {
581
+ const store = useStore();
582
+ const [set, setSet] = useState<readonly IconEntry[] | null>(null);
583
+ const [failed, setFailed] = useState(false);
584
+ const [wanted, setWanted] = useState(false);
585
+ const own = useRef<HTMLElement | null>(null);
586
+
587
+ const want = props.asked || props.query.trim() !== "" || wanted;
588
+
589
+ // Scrolled into view: the list is the root, so a drawer that opens at
590
+ // Basics does not load the set until the author scrolls down to it.
591
+ useEffect(() => {
592
+ const el = own.current;
593
+ const root = props.listRef.current;
594
+ if (wanted || el === null || root === null || typeof IntersectionObserver === "undefined") return;
595
+ const observer = new IntersectionObserver(
596
+ (entries) => {
597
+ if (entries.some((e) => e.isIntersecting)) setWanted(true);
598
+ },
599
+ { root },
600
+ );
601
+ observer.observe(el);
602
+ return () => observer.disconnect();
603
+ }, [wanted, props.listRef]);
604
+
605
+ const [attempt, setAttempt] = useState(0);
606
+ useEffect(() => {
607
+ if (!want || set !== null) return;
608
+ let live = true;
609
+ setFailed(false);
610
+ loadIconSet().then(
611
+ (loaded) => {
612
+ if (live) setSet(loaded);
613
+ },
614
+ () => {
615
+ if (live) setFailed(true);
616
+ },
617
+ );
618
+ return () => {
619
+ live = false;
620
+ };
621
+ }, [want, set, attempt]);
622
+
623
+ const found = useMemo(() => (set === null ? null : searchIcons(set, props.query)), [set, props.query]);
624
+
625
+ return (
626
+ <section
627
+ ref={(el) => {
628
+ own.current = el;
629
+ props.sectionRef(el);
630
+ }}
631
+ className="insert-section"
632
+ aria-label="Icons"
633
+ data-section="Icons"
634
+ >
635
+ <div className="insert-section-head">
636
+ <span className="insert-section-label">Icons</span>
637
+ {found === null ? null : <span className="insert-count">{found.length}</span>}
638
+ </div>
639
+ {found !== null ? (
640
+ found.length === 0 ? null : (
641
+ <IconGrid icons={found} refusal={props.refusal} onPick={(e) => store.addIconLayer(e.name, e.node)} />
642
+ )
643
+ ) : failed ? (
644
+ <div className="insert-status">
645
+ Icons did not load.
646
+ <button type="button" className="insert-retry" onClick={() => setAttempt((n) => n + 1)}>
647
+ Retry
648
+ </button>
649
+ </div>
650
+ ) : want ? (
651
+ <div className="insert-status">Loading icons…</div>
652
+ ) : null}
653
+ </section>
654
+ );
655
+ }
656
+
657
+ function IconGrid(props: { icons: readonly IconEntry[]; refusal: string | null; onPick: (entry: IconEntry) => void }) {
658
+ const scrollRef = useRef<HTMLDivElement | null>(null);
659
+ const gridRef = useRef<HTMLDivElement | null>(null);
660
+ const [cols, setCols] = useState(ICON_COLS_FALLBACK);
661
+ const [scrollTop, setScrollTop] = useState(0);
662
+
663
+ // Columns from the scroller's width: a wider rail fits more icons a row.
664
+ useLayoutEffect(() => {
665
+ const el = scrollRef.current;
666
+ if (el === null) return;
667
+ const measure = (): void => {
668
+ const width = el.clientWidth;
669
+ setCols(width > 0 ? Math.max(1, Math.floor((width + 2) / ICON_PITCH)) : ICON_COLS_FALLBACK);
670
+ };
671
+ measure();
672
+ if (typeof ResizeObserver === "undefined") return;
673
+ const observer = new ResizeObserver(measure);
674
+ observer.observe(el);
675
+ return () => observer.disconnect();
676
+ }, []);
677
+
678
+ const rowCount = Math.ceil(props.icons.length / cols);
679
+ const range = useFixedVirtualWindow({
680
+ listRef: gridRef,
681
+ scrollSelector: ".insert-icons",
682
+ count: rowCount,
683
+ rowHeight: ICON_PITCH,
684
+ enabled: true,
685
+ });
686
+ const seenRows = Math.ceil((scrollTop + ICON_ROWS_SHOWN * ICON_PITCH) / ICON_PITCH);
687
+ const below = Math.max(0, props.icons.length - seenRows * cols);
688
+
689
+ const rows: ReactElement[] = [];
690
+ for (let r = range.start; r < range.end; r += 1) {
691
+ rows.push(
692
+ <div key={r} className="insert-icon-row">
693
+ {props.icons.slice(r * cols, r * cols + cols).map((entry) => (
694
+ <button
695
+ key={entry.name}
696
+ type="button"
697
+ className="insert-icon"
698
+ data-name={entry.name}
699
+ aria-label={entry.name}
700
+ title={props.refusal ?? entry.name}
701
+ disabled={props.refusal !== null}
702
+ onClick={() => props.onPick(entry)}
703
+ >
704
+ <IconGlyph node={entry.node} />
705
+ </button>
706
+ ))}
707
+ </div>,
708
+ );
709
+ }
710
+
711
+ return (
712
+ <>
713
+ <div ref={scrollRef} className="insert-icons" onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}>
714
+ <div ref={gridRef} className="insert-icon-grid">
715
+ <div style={{ height: range.before }} />
716
+ {rows}
717
+ <div style={{ height: range.after }} />
718
+ </div>
719
+ </div>
720
+ {below === 0 ? null : <div className="insert-more">{below} more below</div>}
721
+ </>
722
+ );
723
+ }