@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,1147 @@
1
+ // WHICH WORKSPACE THE WINDOW IS IN, and what that allows.
2
+ //
3
+ // The docked shell has one grid and four workspaces. The stage workspace is
4
+ // the editor; the other three are modes that show their own content in the
5
+ // same cells: the Storyboard, the material nodes and Components (a component
6
+ // alone on the stage, with the Component Studio or Code Studio as its Edit
7
+ // tab). The top bar, the columns and the bottom row stay on screen in every
8
+ // one of them, so the chrome that used to be unreachable behind a full-window
9
+ // mode (the app menu, the shot switcher, Export) has to know which workspace
10
+ // it is in and refuse what does not apply there, with the reason.
11
+ //
12
+ // Pure functions of state, beside `storyboardBlocker` in spirit: every reader
13
+ // (the shell's classes, the menu, the header, the cells) asks the same
14
+ // question and gets the same answer.
15
+ import {
16
+ COMPONENT_LISTING_ID_RE,
17
+ GENERATED_SHOTS_DIR,
18
+ INSTANCE_PROP_PREFIX,
19
+ WORKSPACE_COMPONENT_ID_RE,
20
+ clearReferenceProp,
21
+ componentFilesOf,
22
+ componentUsesOf,
23
+ exposedMasterValue,
24
+ exposedSpecOf,
25
+ homeOf,
26
+ instanceComponentIdsOf,
27
+ instanceExportOf,
28
+ instanceModuleOf,
29
+ layerSnapshotContract,
30
+ lockRefusal,
31
+ lockedLayerEdited,
32
+ patchHome,
33
+ setReferenceProp,
34
+ toTrackHex,
35
+ } from "@dailies-studio/shot/doc";
36
+ import type { ComponentDef3D, ComponentUse, Doc3D, Layer3D } from "@dailies-studio/shot/doc";
37
+ import { coerceValue, controlLabel, humanizeKey } from "@dailies-studio/contract";
38
+ import type { Contract } from "@dailies-studio/contract";
39
+ import { formatHex, parseHex, valueEquals } from "@dailies-studio/engine";
40
+ import type { Value } from "@dailies-studio/engine";
41
+ import { isProjectComponent } from "@dailies-studio/ui";
42
+ import type {
43
+ ComponentFileHome,
44
+ ComponentListingUpdate,
45
+ ComponentRow,
46
+ WorkspaceComponent,
47
+ WorkspaceComponentVersionInfo,
48
+ } from "../api.js";
49
+ import { isCutId, isGapId, isPartId, isReferenceId, shotNumberOf } from "./storyboardModel.js";
50
+ import type { ComponentsSelection, ShotEditorState } from "./types.js";
51
+
52
+ export type Workspace = "stage" | "storyboard" | "material" | "components";
53
+
54
+ type WorkspaceFields = Pick<
55
+ ShotEditorState,
56
+ "materialWorkspace" | "codeComponent" | "editingDef" | "storyboardOpen"
57
+ > &
58
+ Partial<Pick<ShotEditorState, "componentsOpen" | "componentsTab">>;
59
+
60
+ /**
61
+ * Every field `workspaceOf` reads. A write that names none of them cannot
62
+ * move the workspace: the store's mode guards skip their work for it (`set`).
63
+ */
64
+ export const WORKSPACE_KEYS = [
65
+ "materialWorkspace",
66
+ "codeComponent",
67
+ "editingDef",
68
+ "componentsOpen",
69
+ "storyboardOpen",
70
+ ] as const satisfies readonly (keyof WorkspaceFields)[];
71
+
72
+ // The other direction, as `SHOT_KINDS` has it: every field a workspace is read
73
+ // from is in the list, so a mode `workspaceOf` learns to read (a field added
74
+ // to `WorkspaceFields`) fails to compile until it is listed, and a field taken
75
+ // off the list does too. `componentsTab` is the one it never reads: the tab
76
+ // inside Components (`componentEditOpen`'s).
77
+ type _EveryWorkspaceFieldListed =
78
+ Exclude<keyof WorkspaceFields, "componentsTab"> extends (typeof WORKSPACE_KEYS)[number] ? true : never;
79
+ const _workspaceFieldsListed: _EveryWorkspaceFieldListed = true;
80
+ void _workspaceFieldsListed;
81
+
82
+ /**
83
+ * The workspace on screen. One precedence, spelled once: each mode's own
84
+ * entry clears the others, so two are never open at once in practice, and
85
+ * this order says which wins if they ever are. An open definition is always
86
+ * Components: every door into a component lands there, standalone, never in
87
+ * the Shot that holds it.
88
+ */
89
+ export function workspaceOf(s: WorkspaceFields): Workspace {
90
+ if (s.materialWorkspace !== null) return "material";
91
+ if (s.codeComponent !== null) return "components";
92
+ if (s.editingDef !== null) return "components";
93
+ if (s.componentsOpen === true) return "components";
94
+ if (s.storyboardOpen) return "storyboard";
95
+ return "stage";
96
+ }
97
+
98
+ /**
99
+ * Components' Edit tab on a Studio-made definition (mock C6): the layers
100
+ * outline, the tool footer and the def timeline are the editor here, so the
101
+ * stage's verbs apply to the definition's interior.
102
+ */
103
+ export function componentEditOpen(s: WorkspaceFields): boolean {
104
+ return workspaceOf(s) === "components" && s.codeComponent === null && s.editingDef !== null && s.componentsTab === "edit";
105
+ }
106
+
107
+ /** How to get out of each mode, said once for every refusal below. */
108
+ const LEAVE: Record<Exclude<Workspace, "stage" | "storyboard">, string> = {
109
+ material: "Close the material nodes",
110
+ components: "Close Components",
111
+ };
112
+
113
+ /**
114
+ * Why Export cannot run now, or null. The stage in these three modes is
115
+ * drawing one object or one component, not the film, and an export captures
116
+ * the stage. The board is fine: it draws over the film without replacing it.
117
+ */
118
+ export function exportBlocker(
119
+ s: WorkspaceFields & Partial<Pick<ShotEditorState, "doc" | "componentBuilds" | "library">>,
120
+ ): string | null {
121
+ const ws = workspaceOf(s);
122
+ if (ws !== "stage" && ws !== "storyboard") {
123
+ return `${LEAVE[ws]} before exporting: the stage is showing ${ws === "material" ? "one object" : "one component"}, not the film.`;
124
+ }
125
+ // A FAILING BUILD keeps its last good module playing (graph.ts `lastGood`),
126
+ // so the film on screen is not what the source says. Exporting it would
127
+ // ship a stale component silently.
128
+ if (s.doc == null || s.componentBuilds === undefined) return null;
129
+ for (const id of instanceComponentIdsOf(s.doc)) {
130
+ if (s.componentBuilds[id]?.status !== "failed") continue;
131
+ const name = s.library?.find((r) => r.id === id)?.name ?? instanceExportOf(id);
132
+ return `A component has a failing build: ${name}. Fix it before exporting.`;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * The app-menu rows each mode keeps, as exact ids or `prefix.` families.
139
+ * AN ALLOWLIST, on `MATERIAL_WORKSPACE_KEYS`' reasoning: a new row is refused
140
+ * in a mode until somebody decides it applies there, which is the safe
141
+ * direction. The rows kept everywhere are about the window, the account and
142
+ * the transport, never about the stage's selection.
143
+ */
144
+ const EVERYWHERE = [
145
+ "acct.",
146
+ "help",
147
+ "file.save",
148
+ "file.versions",
149
+ "view.panel.",
150
+ "view.badges.",
151
+ "view.appearance.",
152
+ "time.play",
153
+ "time.previewMuted",
154
+ // The Storyboard row is the top bar's segment, which hands over from every
155
+ // mode (`setStoryboardOpen`).
156
+ "ctx.storyboard",
157
+ ];
158
+
159
+ const ALLOWED: Record<"material" | "def" | "code", readonly string[]> = {
160
+ // The object's pane is a real working view and the timeline is on screen:
161
+ // the same verbs its keys keep (`MATERIAL_WORKSPACE_KEYS`).
162
+ material: [
163
+ ...EVERYWHERE,
164
+ "edit.undo",
165
+ "edit.redo",
166
+ "time.",
167
+ "view.fit",
168
+ "view.fitSelection",
169
+ "view.zoomIn",
170
+ "view.zoomOut",
171
+ "view.ortho",
172
+ "view.orbit.",
173
+ "view.preset.",
174
+ ],
175
+ // Components with a definition open on Preview: its own clock runs, and
176
+ // nothing edits the interior until the Edit tab.
177
+ def: [...EVERYWHERE, "edit.undo", "edit.redo", "time.step.", "time.fit", "time.mode."],
178
+ // Components with a code component open, or with nothing open yet. Undo and
179
+ // redo are face-dependent, so `commandBlocker` decides them below.
180
+ code: EVERYWHERE,
181
+ };
182
+
183
+ const allows = (list: readonly string[], id: string): boolean =>
184
+ list.some((rule) => (rule.endsWith(".") || rule.endsWith(":") ? id.startsWith(rule) : id === rule));
185
+
186
+ /**
187
+ * Why nothing can change for a viewer. Every refusal in a read-only Project
188
+ * says this one sentence: the store's `viewOnly`, the menu, the keys, the
189
+ * tool rail and the panels.
190
+ */
191
+ export const VIEW_ONLY = "You can view this Project but not edit it. Ask a Project admin for edit access.";
192
+
193
+ /** Why the comment tool and the Comments tab refuse a caller the server says
194
+ * may not comment (`LoadedShot.can.comment === false`). */
195
+ export const NO_COMMENT = "You cannot comment on this Project. Ask a Project admin for access.";
196
+
197
+ /** Why nothing can change while a saved version is on screen. */
198
+ export const PREVIEW_ONLY = "You are looking at a saved version. Go back to the current version, or restore this one, to edit.";
199
+
200
+ /** Why nothing can change while live sync holds the Project (R5). */
201
+ export const SYNC_HOLD_REASON = {
202
+ upgrade: "This Project was upgraded by a newer version of the editor. Reload to keep editing.",
203
+ "older-server": "This editor is newer than the server it reaches. Editing waits until the server catches up.",
204
+ quota: "Edits cannot be stored on this device. Free up space, then try again.",
205
+ } as const;
206
+
207
+ /** The sentence a read-only chrome says: a version preview's, else a sync
208
+ * hold's, else the viewer's. Every reader of `readOnly` that words a refusal
209
+ * asks this. */
210
+ export function readOnlyReason(s: { versionPreview?: unknown; syncStatus?: { hold: keyof typeof SYNC_HOLD_REASON | null } | null }): string {
211
+ if (s.versionPreview != null) return PREVIEW_ONLY;
212
+ const hold = s.syncStatus?.hold ?? null;
213
+ return hold === null ? VIEW_ONLY : SYNC_HOLD_REASON[hold];
214
+ }
215
+
216
+ /**
217
+ * The app-menu rows a viewer keeps, as exact ids or `prefix.` families. An
218
+ * allowlist for the reason `ALLOWED` is one: a new row is refused to a viewer
219
+ * until somebody decides it writes nothing. Each row here changes only the
220
+ * window, the view, the playhead or the selection, never the document.
221
+ */
222
+ const VIEW_ONLY_ALLOWED = [
223
+ "acct.",
224
+ "help",
225
+ "file.export",
226
+ "file.versions",
227
+ "view.panel.",
228
+ "view.badges.",
229
+ "view.appearance.",
230
+ "view.layout.",
231
+ "view.shading.",
232
+ "view.preset.",
233
+ "view.orbit.",
234
+ "view.video",
235
+ "view.ortho",
236
+ "view.grid",
237
+ "view.zoomIn",
238
+ "view.zoomOut",
239
+ "view.zoomReset",
240
+ "view.fit",
241
+ "view.fitSelection",
242
+ "view.selectMovesPlayhead",
243
+ // The board's Project assets panel: a viewer reads it, every row locked.
244
+ "board-assets",
245
+ // A shot's board: a view, which writes nothing for a planned shot.
246
+ "board-open-board",
247
+ // The Preferences page and its View rows write the viewer's own prefs,
248
+ // never the document.
249
+ "prefs.",
250
+ "view.rulers",
251
+ "view.smartGuides",
252
+ "view.motionPaths",
253
+ "time.play",
254
+ "time.previewMuted",
255
+ "time.step.",
256
+ "time.fit",
257
+ "time.mode.",
258
+ "time.workArea.",
259
+ "edit.copy",
260
+ // The motion clip's copies read the document and write nothing to it.
261
+ "edit.copyAnimation",
262
+ "edit.copyEffects",
263
+ "edit.selectAll",
264
+ "edit.deselect",
265
+ "tool.select",
266
+ "ctx.",
267
+ "comp.edit:",
268
+ "layer.isolate.exitAll",
269
+ "isolation",
270
+ "code.path",
271
+ "shot-open",
272
+ "shot-zoom",
273
+ "shot-transition",
274
+ "shot-copy-effects",
275
+ // The right-click rows that only open, select or change the view, and the
276
+ // copies, which write nothing to the document.
277
+ "copy",
278
+ "copy-animation",
279
+ "copy-effects",
280
+ "open-3d",
281
+ "material",
282
+ "graph-open",
283
+ "graph-copy",
284
+ "key-select-track",
285
+ "path-key-seek",
286
+ ];
287
+
288
+ /** Why a viewer cannot run this row (by its command id), or null. */
289
+ export function viewOnlyBlocker(id: string): string | null {
290
+ return allows(VIEW_ONLY_ALLOWED, id) ? null : VIEW_ONLY;
291
+ }
292
+
293
+ /**
294
+ * Why an app-menu row (by its command id) cannot run in this workspace, or
295
+ * null. The menu and WebMCP's `run_command` both read rows through
296
+ * `appMenuEntries`, which applies this, so the two cannot disagree.
297
+ */
298
+ export function commandBlocker(
299
+ s: WorkspaceFields &
300
+ Partial<Pick<ShotEditorState, "readOnly" | "versionPreview" | "syncStatus">>,
301
+ id: string,
302
+ ): string | null {
303
+ if (s.readOnly === true && viewOnlyBlocker(id) !== null) return readOnlyReason(s);
304
+ const ws = workspaceOf(s);
305
+ if (ws === "stage") return null;
306
+ if (ws === "storyboard") return boardCommandBlocker(id);
307
+ // The Edit tab edits the definition's layers with the stage's own verbs. An
308
+ // export still captures the stage, which shows one component.
309
+ if (componentEditOpen(s)) return id === "file.export" ? exportBlocker(s) : null;
310
+ const rows =
311
+ ws === "material" ? ALLOWED.material : s.codeComponent === null && s.editingDef !== null ? ALLOWED.def : ALLOWED.code;
312
+ if (allows(rows, id)) return null;
313
+ if (id === "file.export") return exportBlocker(s);
314
+ // In Code Studio the menu's undo walks the defaults history (`store.undo`);
315
+ // CodeMirror keeps its own for keys typed in the editor.
316
+ if (ws === "components" && s.codeComponent !== null && (id === "edit.undo" || id === "edit.redo")) return null;
317
+ return `${LEAVE[ws]} first: this acts on the scene, which is not on screen.`;
318
+ }
319
+
320
+ /** Why a component row cannot open in Components (D8): Dailies components
321
+ * are three only. The Components list, the board's library and the Assets
322
+ * tab say this one sentence on a dom row. */
323
+ export const DOM_NOT_SUPPORTED = "Not supported: Dailies components are React Three Fiber";
324
+
325
+ /** Why a built-in (`ComponentRow.builtin`: it ships inside a package) has no
326
+ * door into Components: there is no source file and no master to open. The
327
+ * doors that would refuse it on a press (Open in Components, Edit master)
328
+ * show disabled with this sentence instead. */
329
+ export const BUILTIN_NO_MASTER = "Built into Dailies: it has no source to open or edit.";
330
+
331
+ /** Whether a registry id names a built-in. */
332
+ export function isBuiltinComponent(library: readonly ComponentRow[], componentId: string | undefined): boolean {
333
+ return componentId !== undefined && library.some((row) => row.id === componentId && row.builtin === true);
334
+ }
335
+
336
+ /** One row of the Components list. A dom code component the Project uses is
337
+ * listed with `refusal` set and never opens. */
338
+ export interface ComponentsListRow {
339
+ sel: ComponentsSelection;
340
+ name: string;
341
+ refusal: string | null;
342
+ /** A Studio-made copy's `origin.source`: the workspace component (or the
343
+ * Community listing) it was copied from. */
344
+ copyOf?: string;
345
+ }
346
+
347
+ /** Whether a module path is a Project's generated film (`src/shots/<Name>.tsx`),
348
+ * a build product and never a code component of its own. */
349
+ const isGeneratedFilm = (rel: string): boolean => rel.startsWith(`${GENERATED_SHOTS_DIR}/`);
350
+
351
+ /**
352
+ * The Components list: this document's Studio-made definitions first, then
353
+ * every component of each code file made for it (`componentFiles`, in list
354
+ * order, registry order within a file), then the code components it uses
355
+ * (instance layers and board cards), in the order the document first uses
356
+ * them, each followed by the other components of its file, each once. A file
357
+ * made for it is listed even when nothing places it:
358
+ * membership is not a use. A built-in has no source file in this Project, a
359
+ * generated film is a build product, and a component the registry does not
360
+ * know is left out until the library answers. The open subject is not added:
361
+ * a component neither section lists has no row, and the crumb names it.
362
+ */
363
+ export function componentsListOf(doc: Doc3D, library: readonly ComponentRow[]): ComponentsListRow[] {
364
+ const rows: ComponentsListRow[] = Object.values(doc.components ?? {}).map((def) => ({
365
+ sel: { kind: "studio", def: def.id },
366
+ name: def.name,
367
+ refusal: null,
368
+ ...(def.origin === undefined ? {} : { copyOf: def.origin.source }),
369
+ }));
370
+ const seen = new Set<string>();
371
+ const add = (row: ComponentRow): void => {
372
+ if (seen.has(row.id) || row.builtin === true) return;
373
+ seen.add(row.id);
374
+ rows.push({
375
+ sel: { kind: "code", module: row.id },
376
+ name: row.name,
377
+ refusal: row.host === "three" ? null : DOM_NOT_SUPPORTED,
378
+ });
379
+ };
380
+ for (const path of componentFilesOf(doc)) {
381
+ if (isGeneratedFilm(path)) continue;
382
+ for (const row of library) if (row.rel === path) add(row);
383
+ }
384
+ for (const use of componentUsesOf(doc)) {
385
+ if (use.component === undefined) continue;
386
+ const row = library.find((r) => r.id === use.component);
387
+ if (row !== undefined) add(row);
388
+ // THE FILE IS THE UNIT: Your library counts a file this Project uses as
389
+ // its own (`ownFilesOf`, and `homePaths` for every other Project), so the
390
+ // file's other components list here too, or they would show in neither
391
+ // section. By the use's module, not its row: a renamed or deleted export
392
+ // leaves a stale id whose file still holds the others.
393
+ const path = instanceModuleOf(use.component);
394
+ for (const mate of library) if (mate.rel === path) add(mate);
395
+ }
396
+ return rows;
397
+ }
398
+
399
+ /** One entry of YOUR LIBRARY (`libraryComponentsOf`): a code component of
400
+ * the registry, or a Studio-made component another Project recorded and
401
+ * someone added to the library. `why` is the rule that lists a code one,
402
+ * the first that holds: a Dailies Library install, code made for no
403
+ * Project, or "In your library" ticked (`WorkspaceComponent.library`). */
404
+ export type LibraryEntry =
405
+ | {
406
+ kind: "code";
407
+ module: string;
408
+ name: string;
409
+ rel: string;
410
+ refusal: string | null;
411
+ why: "unassigned" | "installed" | "added";
412
+ /** An install's publisher, as the registry's provenance says it. */
413
+ publisher?: string;
414
+ /** The workspace store's row for it, once one was recorded. */
415
+ recorded?: WorkspaceComponent;
416
+ }
417
+ | { kind: "studio"; component: WorkspaceComponent };
418
+
419
+ /** Why a component is in Your library whatever its "In your library" box
420
+ * says. The Components panel shows the box ticked with this line. */
421
+ export const LIBRARY_RULE = {
422
+ installed: "Dailies Library installs are always in your library.",
423
+ unassigned: "Code made for no Project is always in your library.",
424
+ copied: "Copied from your library.",
425
+ } as const;
426
+
427
+ /** The module paths `doc` itself lists as made for it or uses: what `homes`
428
+ * says of every OTHER Project, read from the live document for this one, so
429
+ * an edit counts before the server has seen it. */
430
+ function ownFilesOf(doc: Doc3D): Set<string> {
431
+ const files = new Set(componentFilesOf(doc));
432
+ for (const use of componentUsesOf(doc)) if (use.component !== undefined) files.add(instanceModuleOf(use.component));
433
+ return files;
434
+ }
435
+
436
+ /** The files another Project lists or uses. Homes of this Project are left
437
+ * to the live document, and a home the caller may not name (`projectId`
438
+ * null) is still another Project's. */
439
+ function filesElsewhere(doc: Doc3D, homes: readonly ComponentFileHome[]): Set<string> {
440
+ return new Set(homes.filter((h) => h.projectId !== doc.id).map((h) => h.path));
441
+ }
442
+
443
+ /** CODE MADE FOR NO PROJECT: the user's own three code that no Project lists
444
+ * or uses, never a built-in and never a generated film. */
445
+ function madeForNoProject(row: ComponentRow, own: ReadonlySet<string>, elsewhere: ReadonlySet<string>): boolean {
446
+ if (!isProjectComponent(row) || row.host !== "three" || isGeneratedFilm(row.rel)) return false;
447
+ return !own.has(row.rel) && !elsewhere.has(row.rel);
448
+ }
449
+
450
+ /** Case-insensitive by name, then by module or id, so the order never
451
+ * depends on which list a row arrived in. */
452
+ function entryOrder(a: LibraryEntry, b: LibraryEntry): number {
453
+ const key = (e: LibraryEntry): [string, string] =>
454
+ e.kind === "code" ? [e.name.toLowerCase(), e.module] : [e.component.name.toLowerCase(), e.component.id];
455
+ const [an, ak] = key(a);
456
+ const [bn, bk] = key(b);
457
+ if (an !== bn) return an < bn ? -1 : 1;
458
+ return ak < bk ? -1 : ak > bk ? 1 : 0;
459
+ }
460
+
461
+ /**
462
+ * YOUR LIBRARY: what every Project of this workspace offers beside its own.
463
+ *
464
+ * (a) code made for no Project: the user's own three code whose file no
465
+ * Project lists or uses (`homes` for the others, the live document for
466
+ * this one). It waits while `homes` is null, so it never lists a file
467
+ * some Project holds before the index has answered.
468
+ * (b) Dailies Library installs (`library.scope` "shared"), even when a
469
+ * Project lists them.
470
+ * (c) what someone added with "In your library": a Studio-made component
471
+ * of another Project, or code made for one. A flagged code row the
472
+ * registry does not list has nothing to open, so it is left out.
473
+ *
474
+ * Less what This project already shows (`projectRows`): code by module, a
475
+ * definition by the copy that names it (`copyOf`) or by its own row.
476
+ * Sorted by name.
477
+ */
478
+ export function libraryComponentsOf(
479
+ doc: Doc3D,
480
+ library: readonly ComponentRow[],
481
+ workspace: readonly WorkspaceComponent[] | null,
482
+ homes: readonly ComponentFileHome[] | null,
483
+ projectRows: readonly ComponentsListRow[],
484
+ ): LibraryEntry[] {
485
+ const shown = new Set(projectRows.flatMap((r) => (r.sel.kind === "code" ? [r.sel.module] : [])));
486
+ const own = ownFilesOf(doc);
487
+ const elsewhere = homes === null ? null : filesElsewhere(doc, homes);
488
+ const entries: LibraryEntry[] = [];
489
+ for (const row of library) {
490
+ if (shown.has(row.id)) continue;
491
+ const recorded = workspaceRowOf(workspace, doc.id, { kind: "code", module: row.id });
492
+ const why =
493
+ row.library?.scope === "shared"
494
+ ? "installed"
495
+ : elsewhere !== null && madeForNoProject(row, own, elsewhere)
496
+ ? "unassigned"
497
+ : recorded?.library === true
498
+ ? "added"
499
+ : null;
500
+ if (why === null) continue;
501
+ const publisher = why === "installed" ? row.library?.publisher : undefined;
502
+ entries.push({
503
+ kind: "code",
504
+ module: row.id,
505
+ name: row.name,
506
+ rel: row.rel,
507
+ refusal: row.host === "three" ? null : DOM_NOT_SUPPORTED,
508
+ why,
509
+ ...(publisher === undefined ? {} : { publisher }),
510
+ ...(recorded === undefined ? {} : { recorded }),
511
+ });
512
+ }
513
+ for (const c of workspace ?? []) {
514
+ if (c.library !== true || !("def" in c.home)) continue;
515
+ // A COPY IS THIS PROJECT'S NOW: a definition whose origin names the row
516
+ // (Use here, or the shot board's picker, made it) lists in the Project.
517
+ if (projectRows.some((r) => r.copyOf === c.id || workspaceRowOf([c], doc.id, r.sel) !== undefined)) continue;
518
+ entries.push({ kind: "studio", component: c });
519
+ }
520
+ return entries.sort(entryOrder);
521
+ }
522
+
523
+ /**
524
+ * Why `sel` is in Your library whatever its "In your library" box says, or
525
+ * null when the box decides: an install, code no Project lists or uses
526
+ * (this document counted from the live copy), or a copy of a definition the
527
+ * library holds. The same rules `libraryComponentsOf` lists by, so the box
528
+ * and the list never disagree.
529
+ */
530
+ export function libraryRuleOf(
531
+ doc: Doc3D,
532
+ sel: ComponentsSelection,
533
+ library: readonly ComponentRow[],
534
+ workspace: readonly WorkspaceComponent[],
535
+ homes: readonly ComponentFileHome[],
536
+ ): string | null {
537
+ if (sel.kind === "studio") {
538
+ const source = doc.components?.[sel.def]?.origin?.source;
539
+ return source !== undefined && workspace.some((c) => c.id === source && c.library === true) ? LIBRARY_RULE.copied : null;
540
+ }
541
+ const row = library.find((r) => r.id === sel.module);
542
+ if (row === undefined) return null;
543
+ if (row.library?.scope === "shared") return LIBRARY_RULE.installed;
544
+ return madeForNoProject(row, ownFilesOf(doc), filesElsewhere(doc, homes)) ? LIBRARY_RULE.unassigned : null;
545
+ }
546
+
547
+ /** A Your library entry's name, whichever kind it is. */
548
+ export function libraryEntryName(entry: LibraryEntry): string {
549
+ return entry.kind === "code" ? entry.name : entry.component.name;
550
+ }
551
+
552
+ /** Where a Your library entry comes from: an install's publisher (else
553
+ * "Dailies Library"), other code's file name, a definition's home Project
554
+ * by name (`homeName`), or "another Project" when the caller may not see
555
+ * that Project. Here, beside the lists, so the shot board's picker reads it
556
+ * without the Components rail. */
557
+ export function libraryHomeLabel(entry: LibraryEntry): string {
558
+ if (entry.kind === "studio") return entry.component.homeName ?? "another Project";
559
+ if (entry.why === "installed") return entry.publisher ?? "Dailies Library";
560
+ return entry.rel.slice(entry.rel.lastIndexOf("/") + 1);
561
+ }
562
+
563
+ /** The workspace component a selection is, by its home. */
564
+ export function workspaceRowOf(
565
+ list: readonly WorkspaceComponent[] | null,
566
+ projectId: string | undefined,
567
+ sel: ComponentsSelection | null,
568
+ ): WorkspaceComponent | undefined {
569
+ if (list === null || sel === null) return undefined;
570
+ return list.find((c) =>
571
+ sel.kind === "studio"
572
+ ? "def" in c.home && c.home.def === sel.def && c.home.projectId === projectId
573
+ : "component" in c.home && c.home.component === sel.module,
574
+ );
575
+ }
576
+
577
+ /** The contract keys the previous version did not have (versions newest
578
+ * first, the first being the build on screen). None before a second one. */
579
+ export function newPropKeys(keys: readonly string[], versions: readonly WorkspaceComponentVersionInfo[] | undefined): string[] {
580
+ const previous = versions?.[1];
581
+ if (previous === undefined) return [];
582
+ return keys.filter((k) => !previous.propKeys.includes(k));
583
+ }
584
+
585
+ /** Whether two selections name the same component. */
586
+ export function sameComponent(a: ComponentsSelection | null, b: ComponentsSelection | null): boolean {
587
+ if (a === null || b === null) return a === b;
588
+ return a.kind === "studio" ? b.kind === "studio" && a.def === b.def : b.kind === "code" && a.module === b.module;
589
+ }
590
+
591
+ /** The component Components shows now, from the open subject. */
592
+ export function componentsSubject(s: Pick<ShotEditorState, "codeComponent" | "editingDef">): ComponentsSelection | null {
593
+ if (s.codeComponent !== null) return { kind: "code", module: s.codeComponent };
594
+ if (s.editingDef !== null) return { kind: "studio", def: s.editingDef };
595
+ return null;
596
+ }
597
+
598
+ /** The Every use view when it is up for the component Components shows now,
599
+ * else null: a view opened for another subject, or left behind by a door
600
+ * out of Components, shows nothing. */
601
+ export function usesViewOf(
602
+ s: WorkspaceFields & Pick<ShotEditorState, "codeComponent" | "editingDef" | "componentUses">,
603
+ ): { subject: ComponentsSelection; use: string | null } | null {
604
+ const view = s.componentUses;
605
+ if (view === null || workspaceOf(s) !== "components") return null;
606
+ return sameComponent(view.subject, componentsSubject(s)) ? view : null;
607
+ }
608
+
609
+ /** Components without the timeline row: nothing open, the Preview tab (its
610
+ * transport sits in the canvas cell), or a code component (its Edit tab has
611
+ * its own footer). Only a Studio-made component's Edit tab keeps it. */
612
+ export function componentsHideTimeline(
613
+ s: WorkspaceFields & Pick<ShotEditorState, "codeComponent" | "editingDef" | "componentsTab">,
614
+ ): boolean {
615
+ if (workspaceOf(s) !== "components") return false;
616
+ return s.codeComponent !== null || s.editingDef === null || s.componentsTab === "preview";
617
+ }
618
+
619
+ /** The board's half of `commandBlocker`. The board enters no Shot, so the
620
+ * Studio's context rows (All shots) act on a view that is not on screen:
621
+ * they are the Studio's. */
622
+ function boardCommandBlocker(id: string): string | null {
623
+ if (id === "ctx.scene") return "All shots is the Studio's view. Close the Storyboard to see the whole film.";
624
+ return null;
625
+ }
626
+
627
+ /** The planned shot whose shot board is on screen, or null: the board is
628
+ * closed, or it shows every shot. A closed board keeps `storyboardPlan` for
629
+ * the way back, so every reader asks this rather than the field. */
630
+ export function shotBoardOf(s: Pick<ShotEditorState, "storyboardOpen" | "storyboardPlan">): string | null {
631
+ return s.storyboardOpen ? s.storyboardPlan : null;
632
+ }
633
+
634
+ /** Which panel the board's right column shows: the agent, the comments, the
635
+ * component sheet (S3: exactly one component card), the
636
+ * inspector (any other card, a gap or a cut), the Plan panel (one shot
637
+ * card), or nothing (no selection: the canvas takes the whole width). On a
638
+ * shot board: the agent, the comments, the item inspector (`"sketch"`, any
639
+ * sketch item selected), Project assets, else the Plan panel. Off the board,
640
+ * the agent, the comments, then in Components the inspector while a
641
+ * component is open and nothing on its home; elsewhere the Brief (S4) or
642
+ * the inspector. */
643
+ export type BoardRightPanel = "agent" | "comments" | "brief" | "component" | "inspector" | "assets" | "plan" | "sketch" | null;
644
+
645
+ export function boardRightPanel(
646
+ s: Pick<
647
+ ShotEditorState,
648
+ | "storyboardOpen"
649
+ | "agentOpen"
650
+ | "commentsOpen"
651
+ | "briefOpen"
652
+ | "canComment"
653
+ | "storyboardSelection"
654
+ | "doc"
655
+ | "boardAssetsOpen"
656
+ | "materialWorkspace"
657
+ | "codeComponent"
658
+ | "editingDef"
659
+ > &
660
+ Partial<Pick<ShotEditorState, "storyboardPlan" | "sketchSelection" | "componentsOpen">>,
661
+ ): BoardRightPanel {
662
+ if (s.agentOpen) return "agent";
663
+ // The Comments tab, picked by the author, on the board or off it. Gated by
664
+ // `can.comment`: a caller who may not comment never sees the threads.
665
+ if (s.commentsOpen && s.canComment) return "comments";
666
+ if (!s.storyboardOpen) {
667
+ // COMPONENTS: the open component's inspector (its parts, This use, the
668
+ // Shot door's origin), never the Brief, which is a Shot's plan. The home
669
+ // has nothing to inspect, so its column steps aside like the board's.
670
+ if (workspaceOf(s) === "components") return componentsSubject(s) === null ? null : "inspector";
671
+ return s.briefOpen ? "brief" : "inspector";
672
+ }
673
+ // THE SHOT BOARD always has a panel: what is selected on the sketch, the
674
+ // assets it adds from, else the shot's plan.
675
+ if (s.storyboardPlan != null) {
676
+ if ((s.sketchSelection?.length ?? 0) > 0) return "sketch";
677
+ return s.boardAssetsOpen ? "assets" : "plan";
678
+ }
679
+ const board = s.storyboardSelection;
680
+ if (board.length === 1 && isReferenceId(board[0]!)) {
681
+ const item = s.doc?.resources?.references?.find((r) => r.id === board[0]);
682
+ if (item?.type === "component") return "component";
683
+ }
684
+ if (board.some((id) => isReferenceId(id) || isGapId(id) || isCutId(id))) return "inspector";
685
+ if (board.length === 1 && isPartId(board[0]!)) return "plan";
686
+ // Project assets, under any selected card's own panel: a press on a card
687
+ // shows that card, and a press on the ground brings the assets back.
688
+ if (s.boardAssetsOpen) return "assets";
689
+ return null;
690
+ }
691
+
692
+ // ── Every use (C4) ──────────────────────────────────────────────────────────
693
+ //
694
+ // A use is named by a KEY, so the list, the THIS USE panel and the store verbs
695
+ // agree on it without passing objects around: a board card by its id, a layer
696
+ // by its container (Shot or definition) and its id, since layer ids are only
697
+ // unique inside one container.
698
+
699
+ export const cardUseKey = (referenceId: string): string => `card:${referenceId}`;
700
+ export const layerUseKey = (container: string, layerId: string): string => `layer:${container}:${layerId}`;
701
+
702
+ export function useKeyOf(use: ComponentUse): string {
703
+ return use.referenceId !== undefined
704
+ ? cardUseKey(use.referenceId)
705
+ : layerUseKey(use.sceneId ?? use.inDef ?? "", use.layerId ?? "");
706
+ }
707
+
708
+ type UseTarget = { kind: "card"; id: string } | { kind: "layer"; container: string; id: string };
709
+
710
+ function targetOf(key: string): UseTarget | null {
711
+ if (key.startsWith("card:")) return { kind: "card", id: key.slice(5) };
712
+ if (!key.startsWith("layer:")) return null;
713
+ const rest = key.slice(6);
714
+ const cut = rest.lastIndexOf(":");
715
+ return cut <= 0 ? null : { kind: "layer", container: rest.slice(0, cut), id: rest.slice(cut + 1) };
716
+ }
717
+
718
+ /** The layer a key names, with its container. */
719
+ export function useLayerOf(key: string): { container: string; layerId: string } | null {
720
+ const target = targetOf(key);
721
+ return target?.kind === "layer" ? { container: target.container, layerId: target.id } : null;
722
+ }
723
+
724
+ /** The board card a key names. */
725
+ export function useCardOf(key: string): string | null {
726
+ const target = targetOf(key);
727
+ return target?.kind === "card" ? target.id : null;
728
+ }
729
+
730
+ /** A code component's defaults from its live contract, for `componentUsesOf`,
731
+ * as the engine values a use stores (`coerceValue`): a curve or a gradient
732
+ * default is its decoded object in the contract and its canonical JSON in a
733
+ * use, and comparing the two called every stored composite a change. */
734
+ export function contractDefaultsFrom(
735
+ contracts: Readonly<Record<string, Contract>>,
736
+ ): (componentId: string) => Record<string, Value> | null {
737
+ return (id) => {
738
+ const contract = contracts[id];
739
+ if (contract === undefined) return null;
740
+ const out: Record<string, Value> = {};
741
+ for (const spec of Object.values(contract.keys)) {
742
+ const value = "default" in spec ? spec.default : undefined;
743
+ if (value === null || value === undefined) continue;
744
+ try {
745
+ const coerced = coerceValue(spec, value);
746
+ if (coerced.ok) out[spec.key] = coerced.value;
747
+ } catch {
748
+ // A spec this build cannot read has no default a use could match.
749
+ }
750
+ }
751
+ return out;
752
+ };
753
+ }
754
+
755
+ /** Every use of one component in this document, in walk order. The live
756
+ * contracts go in twice: a code use's changes are measured against its
757
+ * contract defaults, and a definition's prop aimed at an interior instance's
758
+ * unset prop draws that prop's contract default (`exposedMasterValue`). */
759
+ export function usesOfSubject(
760
+ doc: Doc3D,
761
+ sel: ComponentsSelection,
762
+ contracts: Readonly<Record<string, Contract>> = {},
763
+ ): ComponentUse[] {
764
+ return componentUsesOf(doc, { contractDefaults: contractDefaultsFrom(contracts), contracts }).filter((u) =>
765
+ sel.kind === "studio" ? u.def === sel.def : u.component === sel.module,
766
+ );
767
+ }
768
+
769
+ /** The use a key names, or null when it is gone. */
770
+ export function useByKey(
771
+ doc: Doc3D,
772
+ key: string,
773
+ contracts: Readonly<Record<string, Contract>> = {},
774
+ ): ComponentUse | null {
775
+ return (
776
+ componentUsesOf(doc, { contractDefaults: contractDefaultsFrom(contracts), contracts }).find(
777
+ (u) => useKeyOf(u) === key,
778
+ ) ?? null
779
+ );
780
+ }
781
+
782
+ /** The list chip: "No changes", "1 change: Tilt", "2 changes". */
783
+ export function changeLabel(use: Pick<ComponentUse, "overrides">, labelOf: (key: string) => string = (k) => k): string {
784
+ const n = use.overrides.length;
785
+ if (n === 0) return "No changes";
786
+ return n === 1 ? `1 change: ${labelOf(use.overrides[0]!)}` : `${n} changes`;
787
+ }
788
+
789
+ /** "03 Phone reveal", by the board's shot numbering. */
790
+ export function shotLabel(doc: Doc3D, sceneId: string): string {
791
+ const name = doc.scenesById[sceneId]?.name ?? "";
792
+ const number = shotNumberOf(doc, sceneId);
793
+ return number === null ? name : `${number} ${name}`;
794
+ }
795
+
796
+ /** Where a board card is pinned, as the list says it. */
797
+ function cardPlace(doc: Doc3D, use: ComponentUse): string {
798
+ const part = use.partId === null ? undefined : doc.storyboard?.parts.find((p) => p.id === use.partId);
799
+ if (part === undefined) {
800
+ return use.sceneId === null ? "Reference on the Style" : `Reference in ${shotLabel(doc, use.sceneId)}`;
801
+ }
802
+ const scene = part.scene !== undefined && doc.scenesById[part.scene] !== undefined ? part.scene : null;
803
+ return `Reference, pinned to ${scene === null ? part.label : shotLabel(doc, scene)}`;
804
+ }
805
+
806
+ /** The list row's kind badge, title and location for a use in THIS document. */
807
+ export function useRowText(doc: Doc3D, use: ComponentUse): { kind: "BOARD" | "SHOT"; title: string; location: string } {
808
+ if (use.kind === "card") return { kind: "BOARD", title: cardPlace(doc, use), location: "This Project · Storyboard" };
809
+ const layer = `layer "${use.layerName ?? use.layerId ?? ""}"`;
810
+ if (use.sceneId !== null) {
811
+ return { kind: "SHOT", title: `${shotLabel(doc, use.sceneId)} · ${layer}`, location: "This Project · Studio" };
812
+ }
813
+ const def = use.inDef === undefined ? undefined : doc.components?.[use.inDef];
814
+ const owner = def?.name ?? "a component";
815
+ return { kind: "SHOT", title: `${def?.name ?? "Component"} · ${layer}`, location: `This Project · inside ${owner}` };
816
+ }
817
+
818
+ /** THIS USE's title: "Shot 03 · Phone hero", "Storyboard · Hero card". */
819
+ export function useTitle(doc: Doc3D, use: ComponentUse): string {
820
+ if (use.kind === "card") return `Storyboard · ${use.referenceName ?? "Reference"}`;
821
+ const name = use.layerName ?? use.layerId ?? "";
822
+ if (use.sceneId !== null) {
823
+ const number = shotNumberOf(doc, use.sceneId);
824
+ return `${number === null ? (use.sceneName ?? "Shot") : `Shot ${number}`} · ${name}`;
825
+ }
826
+ const def = use.inDef === undefined ? undefined : doc.components?.[use.inDef];
827
+ return `${def?.name ?? "Component"} · ${name}`;
828
+ }
829
+
830
+ /** Why a value cannot be written to this use from a panel, or null. An
831
+ * instance prop with keyframes is animated in its Shot: a base value written
832
+ * with no playhead would change nothing on screen. */
833
+ export function usePropRefusal(doc: Doc3D, key: string, prop: string, label = prop): string | null {
834
+ const target = targetOf(key);
835
+ if (target === null || target.kind === "card") return null;
836
+ const layer = homeOf(doc, target.container)?.layersById[target.id];
837
+ if (layer?.kind === "instance" && layer.tracks?.[`${INSTANCE_PROP_PREFIX}${prop}`] !== undefined) {
838
+ return `${label} is animated here. Change it in the Studio, on its keyframes.`;
839
+ }
840
+ return null;
841
+ }
842
+
843
+ /**
844
+ * One prop of one use set, or cleared with null so it follows the master
845
+ * again. The same document when the use is gone or the write is refused.
846
+ *
847
+ * `aliases` are the prop's `renamedFrom` keys. An override stored under one of
848
+ * them still reaches the renderer (`overrideFor`), so an explicit write takes
849
+ * them with it: a value set under the new key would leave the old one behind
850
+ * as dead weight, and a CLEAR that left it would keep showing the old value
851
+ * the author just reset.
852
+ */
853
+ export function withUseValue(
854
+ doc: Doc3D,
855
+ key: string,
856
+ prop: string,
857
+ value: Value | null,
858
+ aliases: readonly string[] = [],
859
+ ): Doc3D {
860
+ const target = targetOf(key);
861
+ if (target === null || usePropRefusal(doc, key, prop) !== null) return doc;
862
+ if (target.kind === "card") {
863
+ const base = aliases.reduce((next, alias) => clearReferenceProp(next, target.id, alias), doc);
864
+ return value === null ? clearReferenceProp(base, target.id, prop) : setReferenceProp(base, target.id, prop, value);
865
+ }
866
+ return patchUseLayer(doc, target.container, target.id, (values) => {
867
+ const stale = aliases.filter((alias) => alias in values);
868
+ const without = (drop: readonly string[]): Record<string, Value> =>
869
+ Object.fromEntries(Object.entries(values).filter(([k]) => !drop.includes(k)));
870
+ // The same object when nothing moves, so an unchanged write is no edit.
871
+ if (value === null) return stale.length === 0 && !(prop in values) ? values : without([prop, ...stale]);
872
+ if (stale.length > 0) return { ...without(stale), [prop]: value };
873
+ return valueEquals(values[prop], value) ? values : { ...values, [prop]: value };
874
+ });
875
+ }
876
+
877
+ /** The stored props of a use that carry keyframes: an instance layer's
878
+ * `instance.props.<key>` tracks. Reset all keeps these (`withUseReset`). */
879
+ function animatedPropsOf(doc: Doc3D, key: string): string[] {
880
+ const target = targetOf(key);
881
+ if (target === null || target.kind === "card") return [];
882
+ const layer = homeOf(doc, target.container)?.layersById[target.id];
883
+ if (layer?.kind !== "instance") return [];
884
+ return Object.keys(layer.instance.props).filter((prop) => layer.tracks?.[`${INSTANCE_PROP_PREFIX}${prop}`] !== undefined);
885
+ }
886
+
887
+ /**
888
+ * Reset all to master: the use stores no values, apart from an animated
889
+ * instance prop's. Its stored value is the base under its track, the value
890
+ * the track was minted from and the one that returns when the track goes:
891
+ * clearing it left the next edit of that prop unkeyed (the router keys only
892
+ * over a base) and brought the contract default back in place of the authored
893
+ * value when the keyframes were removed. So those are kept, and `resetUse`
894
+ * says which.
895
+ */
896
+ export function withUseReset(doc: Doc3D, key: string): Doc3D {
897
+ const target = targetOf(key);
898
+ if (target === null) return doc;
899
+ if (target.kind === "card") {
900
+ const stored = Object.keys(useByKey(doc, key)?.values ?? {});
901
+ return stored.reduce((next, prop) => clearReferenceProp(next, target.id, prop), doc);
902
+ }
903
+ const kept = new Set(animatedPropsOf(doc, key));
904
+ return patchUseLayer(doc, target.container, target.id, (values) => {
905
+ const stays = Object.keys(values).filter((prop) => kept.has(prop));
906
+ return stays.length === Object.keys(values).length ? values : Object.fromEntries(stays.map((prop) => [prop, values[prop]!]));
907
+ });
908
+ }
909
+
910
+ /** A component layer's overrides or an instance layer's props, rewritten. */
911
+ function patchUseLayer(
912
+ doc: Doc3D,
913
+ container: string,
914
+ layerId: string,
915
+ next: (values: Record<string, Value>) => Record<string, Value>,
916
+ ): Doc3D {
917
+ const home = homeOf(doc, container);
918
+ const layer = home?.layersById[layerId];
919
+ if (home === null || home === undefined || layer === undefined) return doc;
920
+ let patched: Layer3D;
921
+ if (layer.kind === "component") {
922
+ const overrides = next(layer.component.overrides);
923
+ if (overrides === layer.component.overrides) return doc;
924
+ patched = { ...layer, component: { ...layer.component, overrides } };
925
+ } else if (layer.kind === "instance") {
926
+ const props = next(layer.instance.props);
927
+ if (props === layer.instance.props) return doc;
928
+ patched = { ...layer, instance: { ...layer.instance, props } };
929
+ } else {
930
+ return doc;
931
+ }
932
+ return patchHome(doc, container, { layers: home.layers, layersById: { ...home.layersById, [layerId]: patched } });
933
+ }
934
+
935
+ /**
936
+ * The master's value of one prop: what the definition DRAWS for its exposed
937
+ * prop (`exposedMasterValue`: the interior layer's value at the prop's target,
938
+ * else the prop's published `default`), or the code component's contract
939
+ * default. Undefined when it is not known.
940
+ *
941
+ * The layer and not `prop.default`: an un-overridden use renders the layer
942
+ * as it stands, and the published default drifts from it once the field is
943
+ * edited anywhere but the props panel (the layer's own section, a stage drag,
944
+ * an agent's `update_layer`). Measured against the stale default, a use that
945
+ * matched the screen read as changed, and a value typed to match the screen
946
+ * was stored as an override.
947
+ */
948
+ export function masterValueOf(
949
+ doc: Doc3D,
950
+ contracts: Readonly<Record<string, Contract>>,
951
+ use: UseMaster,
952
+ prop: string,
953
+ ): Value | undefined {
954
+ if (use.def !== undefined) {
955
+ const def = doc.components?.[use.def];
956
+ const exposed = def?.exposed.find((p) => p.key === prop);
957
+ return def === undefined || exposed === undefined
958
+ ? undefined
959
+ : exposedMasterValue(def, exposed, { materials: doc.materials, contracts });
960
+ }
961
+ const contract = contractOfUse(doc, contracts, use);
962
+ return contract === null ? undefined : (contractDefaultsFrom({ master: contract })("master")?.[prop] ?? undefined);
963
+ }
964
+
965
+ /** What names a use's master: its definition or its module, and, for a layer,
966
+ * where the layer is (whose snapshot contract stands in for the live one). */
967
+ type UseMaster = Pick<ComponentUse, "def" | "component"> &
968
+ Partial<Pick<ComponentUse, "layerId" | "sceneId" | "inDef">>;
969
+
970
+ /**
971
+ * The contract a code use's props come from: the live registry's, else, for
972
+ * a placed layer, the snapshot it carries, the fallback the Studio's own rows
973
+ * read (InstanceSection). A component that has left the project keeps its
974
+ * props readable in every panel that shows the use, not only in the Studio.
975
+ */
976
+ export function contractOfUse(
977
+ doc: Doc3D,
978
+ contracts: Readonly<Record<string, Contract>>,
979
+ use: UseMaster,
980
+ ): Contract | null {
981
+ if (use.component === undefined) return null;
982
+ const live = contracts[use.component];
983
+ if (live !== undefined) return live;
984
+ const container = use.sceneId ?? use.inDef;
985
+ if (container === null || container === undefined || use.layerId === undefined) return null;
986
+ const layer = homeOf(doc, container)?.layersById[use.layerId];
987
+ return layer === undefined ? null : layerSnapshotContract(layer);
988
+ }
989
+
990
+ /** A prop's label as the master names it: the published label, or the
991
+ * contract's (`controlLabel`); else its key humanized, never the raw key. */
992
+ export function propLabelOf(
993
+ doc: Doc3D,
994
+ contracts: Readonly<Record<string, Contract>>,
995
+ use: UseMaster,
996
+ prop: string,
997
+ ): string {
998
+ if (use.def !== undefined) {
999
+ const label: unknown = doc.components?.[use.def]?.exposed.find((p) => p.key === prop)?.label;
1000
+ return typeof label === "string" && label.trim() !== "" ? label : humanizeKey(prop);
1001
+ }
1002
+ const spec = contractOfUse(doc, contracts, use)?.keys[prop];
1003
+ return spec === undefined ? humanizeKey(prop) : controlLabel(spec);
1004
+ }
1005
+
1006
+ /** A colour as one canonical hex, or null when it is no colour this lane
1007
+ * reads (`toTrackHex`'s spellings: hex of any length, `rgb()`, `rgba()`). */
1008
+ function canonicalHex(css: string): string | null {
1009
+ const hex = toTrackHex(css);
1010
+ if (hex === null) return null;
1011
+ try {
1012
+ return formatHex(parseHex(hex));
1013
+ } catch {
1014
+ return null;
1015
+ }
1016
+ }
1017
+
1018
+ /** The control kind a use's master draws for one prop: the row's own spec
1019
+ * (`exposedSpecOf` for a definition's prop, the contract for a code
1020
+ * component's), else the published record's kind. */
1021
+ function propKindOf(
1022
+ doc: Doc3D,
1023
+ contracts: Readonly<Record<string, Contract>>,
1024
+ use: UseMaster,
1025
+ prop: string,
1026
+ ): string | undefined {
1027
+ if (use.def !== undefined) {
1028
+ const def = doc.components?.[use.def];
1029
+ const exposed = def?.exposed.find((p) => p.key === prop);
1030
+ if (def === undefined || exposed === undefined) return undefined;
1031
+ return exposedSpecOf(exposed, def, { materials: doc.materials, contracts })?.spec.kind ?? exposed.kind;
1032
+ }
1033
+ return contractOfUse(doc, contracts, use)?.keys[prop]?.kind;
1034
+ }
1035
+
1036
+ /**
1037
+ * Whether a use's value is the master's own. A COLOUR prop's value is
1038
+ * compared as the colour it is: a published fill's default is captured in the
1039
+ * spelling its layer holds (`rgba(30,215,96,0.18)`, `#FFF`), while every
1040
+ * colour control writes `#rrggbb[aa]`, so the master's own colour typed back
1041
+ * must read as the master's and store nothing. Any other kind compares as it
1042
+ * is: a text that happens to spell a colour ("#FFF" over a master's
1043
+ * "#ffffff") is what the author typed, and is kept.
1044
+ */
1045
+ export function sameUseValue(a: Value, b: Value, kind: string | undefined): boolean {
1046
+ if (valueEquals(a, b)) return true;
1047
+ if (kind !== "color" || typeof a !== "string" || typeof b !== "string") return false;
1048
+ const hex = canonicalHex(a);
1049
+ return hex !== null && hex === canonicalHex(b);
1050
+ }
1051
+
1052
+ /**
1053
+ * THE ONE WRITE of a use's prop, for the panel's commits and its scrubs. A
1054
+ * value equal to the master is stored as nothing, so the use keeps following
1055
+ * the master (the rule `setComponentOverride` keeps). A locked layer and an
1056
+ * animated instance prop refuse, with the reason. A definition's prop is
1057
+ * written under its own key, taking any `renamedFrom` key it was stored under
1058
+ * with it (`withUseValue`).
1059
+ */
1060
+ export function writeUseValue(
1061
+ doc: Doc3D,
1062
+ contracts: Readonly<Record<string, Contract>>,
1063
+ key: string,
1064
+ prop: string,
1065
+ value: Value | null,
1066
+ ): { doc: Doc3D; refusal: string | null } {
1067
+ const use = useByKey(doc, key, contracts);
1068
+ if (use === null) return { doc, refusal: null };
1069
+ const label = propLabelOf(doc, contracts, use, prop);
1070
+ const animated = usePropRefusal(doc, key, prop, label);
1071
+ if (animated !== null) return { doc, refusal: animated };
1072
+ const master = masterValueOf(doc, contracts, use, prop);
1073
+ const stored =
1074
+ value !== null && master !== undefined && sameUseValue(value, master, propKindOf(doc, contracts, use, prop))
1075
+ ? null
1076
+ : value;
1077
+ const aliases =
1078
+ use.def === undefined ? [] : (doc.components?.[use.def]?.exposed.find((p) => p.key === prop)?.renamedFrom ?? []);
1079
+ return unlockedUse(doc, withUseValue(doc, key, prop, stored, aliases));
1080
+ }
1081
+
1082
+ /** Reset all to master, with the same lock rule. An animated instance prop
1083
+ * keeps its stored base (`withUseReset`), and the refusal line says which
1084
+ * were kept, so a reset that left values behind never reads as one that
1085
+ * did nothing. */
1086
+ export function resetUse(
1087
+ doc: Doc3D,
1088
+ key: string,
1089
+ contracts: Readonly<Record<string, Contract>> = {},
1090
+ ): { doc: Doc3D; refusal: string | null } {
1091
+ const out = unlockedUse(doc, withUseReset(doc, key));
1092
+ if (out.refusal !== null) return out;
1093
+ const use = useByKey(doc, key, contracts);
1094
+ const kept = use === null ? [] : animatedPropsOf(doc, key).map((prop) => propLabelOf(doc, contracts, use, prop));
1095
+ if (kept.length === 0) return out;
1096
+ const names = kept.length === 1 ? kept[0]! : `${kept.slice(0, -1).join(", ")} and ${kept.at(-1)!}`;
1097
+ const said = kept.length === 1 ? "it is animated, and only its keyframes change it" : "they are animated, and only their keyframes change them";
1098
+ return { doc: out.doc, refusal: `Kept ${names}: ${said}.` };
1099
+ }
1100
+
1101
+ /** Code components cannot be detached: their master is source code. */
1102
+ export const DETACH_CODE_REFUSAL = "Code components cannot be detached; fork it to change the code.";
1103
+ /** A board card always shows its master: it has no layers to detach into. */
1104
+ export const DETACH_CARD_REFUSAL = "A board card shows the master. Use it in a shot to detach a copy there.";
1105
+
1106
+ /** Why a use cannot be detached, or null: the store's refusal, which the
1107
+ * Detach button also wears as its reason before it is pressed. */
1108
+ export function detachRefusal(use: Pick<ComponentUse, "component">, key: string): string | null {
1109
+ if (use.component !== undefined) return DETACH_CODE_REFUSAL;
1110
+ return useLayerOf(key) === null ? DETACH_CARD_REFUSAL : null;
1111
+ }
1112
+
1113
+ function unlockedUse(before: Doc3D, after: Doc3D): { doc: Doc3D; refusal: string | null } {
1114
+ const locked = lockedLayerEdited(before, after);
1115
+ return locked === null ? { doc: after, refusal: null } : { doc: before, refusal: lockRefusal(locked) };
1116
+ }
1117
+
1118
+ /** A newer version of where a copied definition came from: the workspace
1119
+ * store's latest (`cpt_` origin) or the Community listing's (`cml-`). Code
1120
+ * components never have one: they are live across the workspace. */
1121
+ export interface CopyUpdate {
1122
+ source: string;
1123
+ from: "workspace" | "community";
1124
+ version: number;
1125
+ }
1126
+
1127
+ export function copyUpdateOf(
1128
+ def: ComponentDef3D | undefined,
1129
+ workspace: readonly WorkspaceComponent[] | null,
1130
+ listings: Readonly<Record<string, ComponentListingUpdate>>,
1131
+ ): CopyUpdate | null {
1132
+ const origin = def?.origin;
1133
+ if (origin === undefined) return null;
1134
+ if (WORKSPACE_COMPONENT_ID_RE.test(origin.source)) {
1135
+ const row = workspace?.find((c) => c.id === origin.source);
1136
+ return row !== undefined && row.version > origin.version
1137
+ ? { source: origin.source, from: "workspace", version: row.version }
1138
+ : null;
1139
+ }
1140
+ if (COMPONENT_LISTING_ID_RE.test(origin.source)) {
1141
+ const listing = listings[origin.source];
1142
+ return listing !== undefined && listing.version > origin.version
1143
+ ? { source: origin.source, from: "community", version: listing.version }
1144
+ : null;
1145
+ }
1146
+ return null;
1147
+ }