@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
package/src/api.ts CHANGED
@@ -4,21 +4,40 @@
4
4
  // back labelled and this chrome can tell its own echo from someone else's write
5
5
  // (see state/clientId.ts). Every write also carries `baseRev`, so a lost race
6
6
  // is a 409 the author sees rather than a silent clobber.
7
- import type { Doc3D, ExportFormat, ExportQualityId, RenderStats } from "@dailies-studio/shot/doc";
7
+ import { PROJECT_MEDIA_LIMIT_BYTES } from "@dailies-studio/shot/doc";
8
+ import type { DefClosure, OpenverseSource3D } from "@dailies-studio/shot/doc";
9
+ import type {
10
+ ComponentDef3D,
11
+ ComponentUse,
12
+ Doc3D,
13
+ ExportFormat,
14
+ ExportQualityId,
15
+ LibraryReferenceKind3D,
16
+ MaterialGraph3D,
17
+ MediaProbe,
18
+ RenderStats,
19
+ ShotAsset,
20
+ ShotComponentHost,
21
+ SyncSnapshot,
22
+ } from "@dailies-studio/shot/doc";
8
23
  import type { Contract } from "@dailies-studio/contract";
24
+ import type { ComponentSource } from "@dailies-studio/ui";
9
25
  import { createAgentClient } from "@dailies-studio/agent-client";
10
26
  import type { AgentClient, AgentTransport } from "@dailies-studio/agent-client";
11
27
  import { CLIENT_ID, CLIENT_ID_HEADER } from "./state/clientId.js";
12
-
13
- export interface ShotListing {
14
- name: string;
15
- mtimeMs: number;
16
- }
28
+ import { dailiesHost } from "./hostApi.js";
29
+ import type { DailiesHost, HostRefusalError } from "./hostApi.js";
17
30
 
18
31
  export interface LoadedShot {
19
32
  name: string;
20
33
  doc: Doc3D;
21
34
  rev: string;
35
+ /** What the caller may do with this Project. Absent (an older server)
36
+ * means everything; hosted, a Viewer reads `edit: false`. */
37
+ can?: { edit: boolean; comment: boolean };
38
+ /** Live collaboration is on for this Project: edits go over the document's
39
+ * live session (state/sync.ts), not `saveShot`. */
40
+ sync?: true;
22
41
  }
23
42
 
24
43
  export interface SaveResult {
@@ -36,6 +55,9 @@ export class ApiError extends Error {
36
55
  readonly status: number,
37
56
  message: string,
38
57
  readonly reason?: string,
58
+ readonly details?: Readonly<Record<string, unknown>>,
59
+ /** The body's machine `code`, e.g. "forbidden" for a role refusal. */
60
+ readonly code?: string,
39
61
  ) {
40
62
  super(message);
41
63
  this.name = "ApiError";
@@ -45,72 +67,268 @@ export class ApiError extends Error {
45
67
  }
46
68
  }
47
69
 
48
- async function request<T>(path: string, init?: RequestInit): Promise<T> {
49
- const res = await fetch(path, {
50
- ...init,
51
- headers: {
52
- ...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
53
- [CLIENT_ID_HEADER]: CLIENT_ID,
54
- ...init?.headers,
55
- },
56
- });
57
- const text = await res.text();
58
- let body: unknown = null;
59
- if (text.length > 0) {
60
- try {
61
- body = JSON.parse(text);
62
- } catch {
63
- // A non-JSON body from a route that owes JSON is itself the error.
64
- throw new ApiError(res.status, text.slice(0, 400));
70
+ /**
71
+ * How long a request waits out a workspace that is starting, and how often it
72
+ * asks again: the dashboard's budget (packages/dashboard/src/api.ts). The
73
+ * hosted Worker answers JSON 503 `{starting: true}` while the workspace's
74
+ * store resets after a deploy; any other 503 is a real failure.
75
+ */
76
+ export const STARTING_BUDGET_MS = 20_000;
77
+ export const STARTING_RETRY_MS = 1_500;
78
+
79
+ /** `baseRevGuarded`: a write the store refuses as stale (409) once a first
80
+ * attempt landed, so sending it again after a 503 `{starting: true}` cannot
81
+ * do twice what the first did. A read is retried too; nothing else is.
82
+ * `onStatus` hears the final answer's status, for a route whose 200 and 201
83
+ * mean different things. */
84
+ async function request<T>(
85
+ path: string,
86
+ init?: RequestInit,
87
+ baseRevGuarded = false,
88
+ onStatus?: (status: number) => void,
89
+ ): Promise<T> {
90
+ const method = (init?.method ?? "GET").toUpperCase();
91
+ const retries = baseRevGuarded || method === "GET" || method === "HEAD";
92
+ const deadline = Date.now() + STARTING_BUDGET_MS;
93
+ let res: Response;
94
+ let body: unknown;
95
+ for (;;) {
96
+ res = await fetch(path, {
97
+ ...init,
98
+ headers: {
99
+ ...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
100
+ [CLIENT_ID_HEADER]: CLIENT_ID,
101
+ ...init?.headers,
102
+ },
103
+ });
104
+ const text = await res.text();
105
+ body = null;
106
+ if (text.length > 0) {
107
+ try {
108
+ body = JSON.parse(text);
109
+ } catch {
110
+ // A non-JSON body from a route that owes JSON is itself the error.
111
+ throw new ApiError(res.status, text.slice(0, 400));
112
+ }
65
113
  }
114
+ const starting = res.status === 503 && (body as { starting?: unknown } | null)?.starting === true;
115
+ if (!starting || !retries || Date.now() + STARTING_RETRY_MS > deadline) break;
116
+ await new Promise((resolve) => setTimeout(resolve, STARTING_RETRY_MS));
66
117
  }
67
118
  if (!res.ok) {
68
- const b = body as { error?: unknown; reason?: unknown } | null;
119
+ const b = body as { error?: unknown; reason?: unknown; code?: unknown } | null;
120
+ // The whole body rides along, so a caller can read a refusal's extras
121
+ // (`userIds` on a 422 mention_no_access, `retry` on a 429).
122
+ const details = typeof body === "object" && body !== null ? (body as Readonly<Record<string, unknown>>) : undefined;
69
123
  throw new ApiError(
70
124
  res.status,
71
- typeof b?.error === "string" ? b.error : `${init?.method ?? "GET"} ${path} failed (${res.status})`,
125
+ details !== undefined && b?.code === "project_quota_exceeded"
126
+ ? quotaMessage(details)
127
+ : typeof b?.error === "string"
128
+ ? b.error
129
+ : `${init?.method ?? "GET"} ${path} failed (${res.status})`,
72
130
  typeof b?.reason === "string" ? b.reason : undefined,
131
+ details,
132
+ typeof b?.code === "string" ? b.code : undefined,
73
133
  );
74
134
  }
135
+ onStatus?.(res.status);
75
136
  return body as T;
76
137
  }
77
138
 
78
- export function listShots(): Promise<{ shots: ShotListing[] }> {
79
- return request("/api/shots");
80
- }
81
-
82
139
  export function loadShot(name: string): Promise<LoadedShot> {
83
140
  return request(`/api/shots/${encodeURIComponent(name)}`);
84
141
  }
85
142
 
143
+ /** The live document with its place in the session's log: what a snapshot
144
+ * too large for the socket points at. */
145
+ export function loadShotSnapshot(name: string): Promise<SyncSnapshot> {
146
+ return request(`/api/shots/${encodeURIComponent(name)}?sync=1`);
147
+ }
148
+
86
149
  /** `baseRev` is REQUIRED at this seam even though the route allows it to be
87
150
  * absent: an editor that omits it is an editor that can silently overwrite
88
151
  * another surface's work, and `null` already means "I am claiming a create". */
89
152
  export function saveShot(name: string, doc: Doc3D, baseRev: string | null): Promise<SaveResult> {
90
- return request(`/api/shots/${encodeURIComponent(name)}`, {
91
- method: "PUT",
92
- body: JSON.stringify({ doc, baseRev }),
153
+ return request(
154
+ `/api/shots/${encodeURIComponent(name)}`,
155
+ { method: "PUT", body: JSON.stringify({ doc, baseRev }) },
156
+ true,
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Re-emit `src/shots/<Name>.tsx` from the document, discarding hand edits.
162
+ *
163
+ * ALWAYS DESTRUCTIVE at this seam, and the caller must confirm before reaching
164
+ * it. The route's GET is the dry run and its POST without the flag is a
165
+ * re-emit that still refuses a conflict — but a menu row named "Regenerate" that
166
+ * reported "still blocked" and did nothing would leave the author exactly where
167
+ * the emit warning already put them, which is the state this row exists to
168
+ * leave. `ok: false` with `blocked` is still a 200: the answer arrived.
169
+ */
170
+ export function regenerateShot(name: string): Promise<{
171
+ ok: boolean;
172
+ generated: string | null;
173
+ blocked?: string;
174
+ warning?: string;
175
+ }> {
176
+ return request(`/api/shots/${encodeURIComponent(name)}/regenerate`, {
177
+ method: "POST",
178
+ body: JSON.stringify({ discardHandEdits: true }),
93
179
  });
94
180
  }
95
181
 
96
- export function deleteShot(name: string): Promise<{ deleted: true }> {
97
- return request(`/api/shots/${encodeURIComponent(name)}`, { method: "DELETE" });
182
+ // ---- version history (packages/store/src/shotVersions.ts) --------------------
183
+
184
+ /** The open autosave group: the edits that led to the stored document. */
185
+ export interface ShotVersionHead {
186
+ /** User ids, first writer first. "local" is the CLI's one user. */
187
+ authors: string[];
188
+ /** Epoch ms of the group's first and last save. */
189
+ from: number;
190
+ at: number;
98
191
  }
99
192
 
100
- export function renameShot(name: string, to: string): Promise<{ renamed: true; generated: string | null }> {
101
- return request(`/api/shots/${encodeURIComponent(name)}/rename`, {
102
- method: "POST",
103
- body: JSON.stringify({ to }),
104
- });
193
+ export interface ShotVersion extends ShotVersionHead {
194
+ id: string;
195
+ rev: string;
196
+ /** Present on a named version. */
197
+ name?: string;
198
+ /** The row a restore brought back. */
199
+ restoredFrom?: string;
105
200
  }
106
201
 
107
- export function duplicateShot(name: string, to: string): Promise<{ duplicated: true }> {
108
- return request(`/api/shots/${encodeURIComponent(name)}/duplicate`, {
109
- method: "POST",
110
- body: JSON.stringify({ to }),
202
+ export interface ShotVersionList {
203
+ head: ShotVersionHead | null;
204
+ /** Newest first. */
205
+ versions: ShotVersion[];
206
+ /** User id to display name (hosted). Empty from the CLI. */
207
+ people: Record<string, string>;
208
+ }
209
+
210
+ const versionsPath = (name: string, rest = ""): string => `/api/shots/${encodeURIComponent(name)}/versions${rest}`;
211
+
212
+ // ---- an agent's question to the person (store agentQuestions.ts) ----------
213
+
214
+ /** What an export question opens the Export dialog with (store
215
+ * `AgentQuestionExport`); each absent field is the dialog's own default. */
216
+ export interface AgentQuestionExport {
217
+ format?: ExportFormat;
218
+ quality?: ExportQualityId;
219
+ scene?: string;
220
+ }
221
+
222
+ /** The saved video an export question's yes made (store
223
+ * `AgentQuestionVideo`): in Videos, written by the studio server's render, or
224
+ * only downloaded. */
225
+ export type AgentQuestionVideo =
226
+ | { where: "videos"; id: string; url: string }
227
+ | { where: "project"; path: string }
228
+ | { where: "downloads"; filename: string };
229
+
230
+ /** The question as the route answers it (store `AgentQuestion`, restated:
231
+ * the chrome does not depend on the store package). */
232
+ export interface AgentQuestion {
233
+ id: string;
234
+ projectId: string;
235
+ question: string;
236
+ summary: string;
237
+ boardRev: string;
238
+ askedBy: { client: string; member: string | null; sessionId: string };
239
+ askedAt: number;
240
+ status: "open" | "answered" | "withdrawn";
241
+ /** A later question's export: a yes opens the Export dialog with it. */
242
+ export?: AgentQuestionExport;
243
+ answer?: {
244
+ choice: "yes" | "changes";
245
+ note?: string;
246
+ by: { id: string; name: string };
247
+ at: number;
248
+ boardRev: string | null;
249
+ video?: AgentQuestionVideo;
250
+ };
251
+ }
252
+
253
+ const questionPath = (name: string): string => `/api/shots/${encodeURIComponent(name)}/question`;
254
+
255
+ /** The Project's open question, else its latest answered one for a day. */
256
+ export async function getAgentQuestion(name: string): Promise<AgentQuestion | null> {
257
+ return (await request<{ question: AgentQuestion | null }>(questionPath(name))).question;
258
+ }
259
+
260
+ /** Answer it: "yes" makes the video, "changes" sends the note. */
261
+ export async function answerAgentQuestion(
262
+ name: string,
263
+ id: string,
264
+ body: { choice: "yes" | "changes"; note?: string },
265
+ ): Promise<AgentQuestion> {
266
+ return (
267
+ await request<{ question: AgentQuestion }>(`${questionPath(name)}/${encodeURIComponent(id)}/answer`, {
268
+ method: "POST",
269
+ body: JSON.stringify(body),
270
+ })
271
+ ).question;
272
+ }
273
+
274
+ /** Tell an export question where its saved video is, which wakes the
275
+ * agent's waiting get_answer. */
276
+ export async function reportAgentQuestionVideo(name: string, id: string, video: AgentQuestionVideo): Promise<AgentQuestion> {
277
+ return (
278
+ await request<{ question: AgentQuestion }>(`${questionPath(name)}/${encodeURIComponent(id)}/video`, {
279
+ method: "POST",
280
+ body: JSON.stringify({ video }),
281
+ })
282
+ ).question;
283
+ }
284
+
285
+ export function listVersions(name: string): Promise<ShotVersionList> {
286
+ return request(versionsPath(name));
287
+ }
288
+
289
+ /** Close the open group as a version. `rev` refuses (409) when the stored
290
+ * document is no longer the one the author saw. */
291
+ export function saveVersion(name: string, body: { name?: string; rev?: string }): Promise<{ version: ShotVersion }> {
292
+ return request(versionsPath(name), { method: "POST", body: JSON.stringify(body) });
293
+ }
294
+
295
+ export function readVersion(name: string, id: string): Promise<{ version: ShotVersion; doc: Doc3D }> {
296
+ return request(versionsPath(name, `/${encodeURIComponent(id)}`));
297
+ }
298
+
299
+ export function renameVersion(name: string, id: string, label: string): Promise<{ version: ShotVersion }> {
300
+ return request(versionsPath(name, `/${encodeURIComponent(id)}`), {
301
+ method: "PATCH",
302
+ body: JSON.stringify({ name: label }),
111
303
  });
112
304
  }
113
305
 
306
+ /** Write the version's document as a new save. The live session adopts it
307
+ * like any other write, so the editor sees a change event. */
308
+ export function restoreVersion(name: string, id: string): Promise<{ version: ShotVersion; rev: string }> {
309
+ return request(versionsPath(name, `/${encodeURIComponent(id)}/restore`), { method: "POST" });
310
+ }
311
+
312
+ /** The Trash entries of the document `docId`. A hosted editor may list no
313
+ * more than that; the local server lists the whole Trash. `id` is the
314
+ * Project's id (hosted, where it is the document's id too); `name` the name
315
+ * it had (local). */
316
+ export function listTrash(docId: string): Promise<{ items: { trashId: string; id?: string; name?: string; deletedAt: number }[] }> {
317
+ return request(`/api/trash?doc=${encodeURIComponent(docId)}`);
318
+ }
319
+
320
+ /** Bring a Project back from the Trash (R10's "Restore"). `name` is where it
321
+ * landed: its old name, or a free one if that was taken meanwhile. */
322
+ export function restoreFromTrash(trashId: string): Promise<{ name: string }> {
323
+ return request(`/api/trash/${encodeURIComponent(trashId)}/restore`, { method: "POST" });
324
+ }
325
+
326
+ /** `doc` as a new Project, beside a trashed one whose media it may use (R10's
327
+ * "Save as copy"). */
328
+ export function copyFromTrash(trashId: string, doc: Doc3D): Promise<{ name: string; id: string; title: string }> {
329
+ return request(`/api/trash/${encodeURIComponent(trashId)}/copy`, { method: "POST", body: JSON.stringify({ doc }) });
330
+ }
331
+
114
332
  // ---- the component registry (instance layers) --------------------------------
115
333
 
116
334
  /**
@@ -122,7 +340,10 @@ export function duplicateShot(name: string, to: string): Promise<{ duplicated: t
122
340
  *
123
341
  * `host` is the instance gate (only "three" components can be instanced — a
124
342
  * "dom" one under the three reconciler is a hard crash, and the SERVER's
125
- * `classifyHost` is what decided the value). `contractHash` is the drift
343
+ * `classifyHost` is what decided the value). Its union is `ShotComponentHost`,
344
+ * imported rather than restated: the row is the wire shape of a value the
345
+ * schema already owns, and a chrome-local copy would keep typechecking after a
346
+ * host classification was added. `contractHash` is the drift
126
347
  * baseline recorded on the layer. `contractAvailability` is the server's own
127
348
  * verdict on whether generated code can STATICALLY IMPORT the contract or
128
349
  * whether the document must carry a snapshot — consumed verbatim, never
@@ -137,7 +358,14 @@ export interface ComponentRow {
137
358
  contractHash: string;
138
359
  contractSource: "declared" | "inferred";
139
360
  contractAvailability?: "sibling" | "colocated" | "inferred";
140
- host: "dom" | "three" | "dom-embedded-gl";
361
+ host: ShotComponentHost;
362
+ /**
363
+ * The SERVER's verdict on where the component came from: the user's own code
364
+ * or something the studio shipped. `builtin` is the older flag and both are
365
+ * on the wire, so read them through `componentSourceOf` from
366
+ * `@dailies-studio/ui` rather than testing either one directly.
367
+ */
368
+ source?: ComponentSource;
141
369
  builtin?: boolean;
142
370
  /**
143
371
  * Present only on catalog EFFECTS, which is what makes it the filter for the
@@ -155,10 +383,241 @@ export interface ComponentRow {
155
383
  thumb?: string;
156
384
  softwareRaster?: "unfinished";
157
385
  };
386
+ /**
387
+ * Where the SERVER says this component came from: the user's own code, a
388
+ * shipped built-in, or a library item this Project vendored under
389
+ * `src/shared/`. Derived from the install lockfile at read time, which is
390
+ * why the document never stores it: a field would go stale on uninstall.
391
+ */
392
+ library?: LibraryDescriptor;
393
+ }
394
+
395
+ // ---- the shipped library -----------------------------------------------------
396
+ //
397
+ // Chrome-local copies of the CLI's `libraryCatalog.ts` shapes, for the same
398
+ // reason `ComponentRow` is declared above rather than imported: only these
399
+ // fields are load-bearing here, and the shot chrome depends on no other package
400
+ // for a wire shape. Every field the server ADDS later keeps this typechecking.
401
+
402
+ export type LibraryScope = "builtin" | "shared" | "project";
403
+
404
+ export interface LibraryDescriptor {
405
+ scope: LibraryScope;
406
+ removable: boolean;
407
+ publisher?: string;
408
+ libraryId?: string;
409
+ libraryVersion?: number;
410
+ /** The installed files no longer match what was installed. */
411
+ modified?: boolean;
412
+ }
413
+
414
+ /** `component`, the four kinds a `library` reference may name, and `sounds`,
415
+ * which is none of them: a pack is imported and its files become sound
416
+ * cards, so a `library` card of kind "sounds" is a type error here. */
417
+ export type LibraryItemKind = "component" | LibraryReferenceKind3D | "sounds";
418
+
419
+ /**
420
+ * One row of `GET /api/library/catalog`: a built-in, an installed shared
421
+ * component, or a SHIPPED item. `action` is the server's verdict on what a
422
+ * drop does: `install` vendors a component first, `add` places one already in
423
+ * the registry (`installedAs` names it), `claim` copies a shot into a new
424
+ * Project, `import` lands a material, an image pack or a sound pack in this
425
+ * one, and `read` is a blueprint, which is copied by hand through its ops.
426
+ */
427
+ export interface LibraryItem {
428
+ id: string;
429
+ name: string;
430
+ /** The manifest's human name; print it through `libraryDisplayName`. */
431
+ displayName?: string;
432
+ source: "builtin" | "shared" | "shipped";
433
+ group: string;
434
+ kind?: LibraryItemKind;
435
+ category?: string;
436
+ tags: string[];
437
+ vibe?: string[];
438
+ useFor?: string;
439
+ avoidFor?: string;
440
+ lengthFrames?: number;
441
+ deps?: string[];
442
+ /** The `/api/library/items/<id>` URL. */
443
+ docs?: string;
444
+ /** The `/api/library/items/<id>/thumb` URL, only when the item has one. */
445
+ thumb?: string;
446
+ host: ShotComponentHost;
447
+ action: "add" | "install" | "claim" | "import" | "read";
448
+ installedAs?: string;
449
+ description?: string;
450
+ publisher?: string;
451
+ version?: number;
452
+ updateAvailable?: boolean;
453
+ }
454
+
455
+ export interface LibraryCatalog {
456
+ items: LibraryItem[];
457
+ /** Items the registry refused to read. Named so the rail can say WHICH. */
458
+ problems: Array<{ id: string; error: string }>;
459
+ }
460
+
461
+ /** `GET /api/library/items/<id>`: the row, its `library.json`, its README and
462
+ * the kind-specific extra. */
463
+ export interface LibraryItemDetail {
464
+ item: LibraryItem;
465
+ manifest: Readonly<Record<string, unknown>>;
466
+ docs: string;
467
+ component?: {
468
+ files: Array<{ path: string; bytes: number; hash: string }>;
469
+ peerDeps: string[];
470
+ entry: string;
471
+ exportName: string;
472
+ host: ShotComponentHost;
473
+ contractHash: string;
474
+ installedAs?: string;
475
+ contract?: Contract;
476
+ };
477
+ shot?: { summary: { durationFrames: number; sceneCount: number; layerCount: number; outline: string[] } };
478
+ material?: { material: MaterialGraph3D };
479
+ blueprint?: { ops: unknown; expects: Array<{ layer: string; kind: string }> };
480
+ images?: { files: Array<{ path: string; kind: string; bytes: number }> };
481
+ /** A sound pack's files, each measured off its WAV: its length and where
482
+ * it lands (`hitMs`, from its `library.json`). */
483
+ sounds?: {
484
+ files: Array<{
485
+ path: string;
486
+ kind: "audio";
487
+ bytes: number;
488
+ durationMs: number;
489
+ hitMs: number;
490
+ /** The author's name for the file (`library.json`); print it through
491
+ * `libraryFileLabel`, which falls back to the stem. */
492
+ label?: string;
493
+ }>;
494
+ };
495
+ }
496
+
497
+ /** One sound a pack import registered: the Project asset id `add_audio` and a
498
+ * sound card name, the pack's file name, and the two numbers it is placed by. */
499
+ export interface LibraryAddedSound {
500
+ id: string;
501
+ file: string;
502
+ hitMs: number;
503
+ durationMs: number;
504
+ }
505
+
506
+ /** `POST /api/library/add-sounds`: the whole pack, content-addressed, so a
507
+ * second import of the same pack uploads nothing and answers the same ids. */
508
+ export interface LibraryAddSoundsResult {
509
+ assets: LibraryAddedSound[];
510
+ rev: string;
511
+ usedBytes: number;
512
+ remainingBytes: number;
513
+ }
514
+
515
+ /** `POST /api/library/install`: the CLI's `InstallResult`, plus the component
516
+ * detail the route fetched AFTER awaiting its own registry rescan, so the
517
+ * caller can place the component without a second round trip. */
518
+ export interface LibraryInstallResult {
519
+ componentId: string;
520
+ dir: string;
521
+ written: string[];
522
+ removed: string[];
523
+ alreadyInstalled: boolean;
524
+ replaces: { version: number; contentHash: string } | null;
525
+ files: Array<{ path: string }>;
526
+ conflicts: string[];
527
+ componentDetail: ComponentDetail | null;
528
+ }
529
+
530
+ export function libraryCatalog(): Promise<LibraryCatalog> {
531
+ return request("/api/library/catalog");
532
+ }
533
+
534
+ export function libraryItem(id: string): Promise<LibraryItemDetail> {
535
+ return request(`/api/library/items/${encodeURIComponent(id)}`);
536
+ }
537
+
538
+ /** Vendor a shipped component into this Project. A 409 is a user file in the
539
+ * way, refused before any byte lands. */
540
+ export function libraryInstall(libraryId: string): Promise<LibraryInstallResult> {
541
+ return request("/api/library/install", {
542
+ method: "POST",
543
+ body: JSON.stringify({ libraryId }),
544
+ });
545
+ }
546
+
547
+ /** Copy a shipped shot into a NEW Project. 409 when `name` is taken. */
548
+ export function libraryClaim(
549
+ libraryId: string,
550
+ name?: string,
551
+ ): Promise<{ name: string; title: string; projectId: string; href: string }> {
552
+ return request("/api/library/claim", {
553
+ method: "POST",
554
+ body: JSON.stringify(name === undefined ? { libraryId } : { libraryId, name }),
555
+ });
556
+ }
557
+
558
+ /** `baseRev` is the rev from the last read, so a stale write is a 409 the
559
+ * author sees rather than a silent clobber: the same discipline as `saveShot`. */
560
+ export function libraryImportMaterial(input: {
561
+ libraryId: string;
562
+ name: string;
563
+ baseRev: string;
564
+ }): Promise<{ materialId: string; rev: string }> {
565
+ return request("/api/library/import-material", {
566
+ method: "POST",
567
+ body: JSON.stringify(input),
568
+ });
569
+ }
570
+
571
+ export function libraryAddImages(input: {
572
+ libraryId: string;
573
+ name: string;
574
+ baseRev: string;
575
+ }): Promise<{
576
+ assets: Array<{ id: string; path: string; hash: string; kind: string; size: number }>;
577
+ rev: string;
578
+ usedBytes: number;
579
+ remainingBytes: number;
580
+ }> {
581
+ return request("/api/library/add-images", {
582
+ method: "POST",
583
+ body: JSON.stringify(input),
584
+ });
585
+ }
586
+
587
+ /** Import a sound pack into a Project against a saved rev: the one sound
588
+ * `file` names (a bare file name), or every file of it when it names none. */
589
+ export function libraryAddSounds(input: {
590
+ libraryId: string;
591
+ name: string;
592
+ baseRev: string;
593
+ file?: string;
594
+ }): Promise<LibraryAddSoundsResult> {
595
+ return request("/api/library/add-sounds", {
596
+ method: "POST",
597
+ body: JSON.stringify(input),
598
+ });
599
+ }
600
+
601
+ /**
602
+ * Where a default edit would LAND and which keys can take one — the defaults
603
+ * rail renders read-only rows from this instead of learning editability from a
604
+ * refused write. Computed by the server against the current source bytes on
605
+ * every detail GET, never cached beside the contract (the answer can change
606
+ * without the contractHash moving). Absent on builtins.
607
+ */
608
+ export interface ComponentDefaultsMeta {
609
+ /** Project-relative posix path the persist write targets: the sibling
610
+ * `.controls.ts`, the component file (colocated / inferred). */
611
+ file: string;
612
+ /** False when the target sits in a generated lane — the whole rail is
613
+ * read-only then, with the shot named as the owner. */
614
+ writable: boolean;
615
+ keys: Readonly<Record<string, { editable: true } | { editable: false; reason: string }>>;
158
616
  }
159
617
 
160
618
  export interface ComponentDetail extends ComponentRow {
161
619
  contract: Contract;
620
+ defaults?: ComponentDefaultsMeta;
162
621
  }
163
622
 
164
623
  /** A bare ARRAY, not an envelope — the shape that route has always answered. */
@@ -170,37 +629,382 @@ export function componentDetail(id: string): Promise<ComponentDetail> {
170
629
  return request(`/api/components/${encodeURIComponent(id)}`);
171
630
  }
172
631
 
632
+ /** What persisting a default answers. */
633
+ export interface ComponentDefaultResult {
634
+ file: string;
635
+ wrote: boolean;
636
+ hash?: string;
637
+ results: Readonly<Record<string, { status: "written" } | { status: "refused"; reason: string }>>;
638
+ contractHash: string;
639
+ editableKeys: ComponentDefaultsMeta["keys"];
640
+ }
641
+
642
+ /**
643
+ * Persist ONE contract default into the component's source — the defaults
644
+ * rail's commit. The server locates the exact span (the `default:` inside
645
+ * `defineControls`, or the destructured render-fn default) and splices the
646
+ * printed literal; everything else in the file survives byte-for-byte.
647
+ *
648
+ * `contractHash` is the drift baseline: a 409 means the contract moved since
649
+ * the panel loaded, and the caller refetches the detail rather than writing
650
+ * blind. The per-key verdict comes back in `results` on a 200 — a refused key
651
+ * is an answer, not an error.
652
+ *
653
+ * Hosted, the code host computes the edit (`__dailiesHost.applyDefaults`,
654
+ * the same decisions over the files it compiled), this module writes it
655
+ * through the file API with its baseHash, and the host re-extracts to answer
656
+ * the contract after it: the dev server's route, split at the write.
657
+ */
658
+ export function setComponentDefault(
659
+ id: string,
660
+ contractHash: string,
661
+ key: string,
662
+ value: unknown,
663
+ ): Promise<ComponentDefaultResult> {
664
+ const host = dailiesHost();
665
+ if (host !== undefined) return hostedDefault(host, id, { contractHash, values: { [key]: value } });
666
+ return request(`/api/components/${encodeURIComponent(id)}/defaults`, {
667
+ method: "POST",
668
+ body: JSON.stringify({ contractHash, values: { [key]: value } }),
669
+ });
670
+ }
671
+
672
+ /** A host refusal as the error the dev server's route gives this module:
673
+ * its status and message, and its `reason` when it carries one. Any other
674
+ * failure passes through. */
675
+ function hostError(error: unknown): unknown {
676
+ const status = (error as HostRefusalError | null)?.status;
677
+ if (!(error instanceof Error) || typeof status !== "number") return error;
678
+ const reason = (error as HostRefusalError).body?.["reason"];
679
+ return new ApiError(status, error.message, typeof reason === "string" ? reason : undefined);
680
+ }
681
+
682
+ async function viaHost<T>(call: () => Promise<T>): Promise<T> {
683
+ try {
684
+ return await call();
685
+ } catch (e) {
686
+ throw hostError(e);
687
+ }
688
+ }
689
+
690
+ /** The dev server's defaults route, hosted: edit, write with the edit's
691
+ * baseHash, and on a 409 (the file moved under the edit) edit again and
692
+ * write ONCE more, so a second collision is the author's to see, as there. */
693
+ async function hostedDefault(
694
+ host: DailiesHost,
695
+ id: string,
696
+ body: { contractHash: string; values: Record<string, unknown> },
697
+ ): Promise<ComponentDefaultResult> {
698
+ let edit = await viaHost(() => host.applyDefaults(id, body));
699
+ let written: SourceFileSaved | null = null;
700
+ if (edit.edits > 0) {
701
+ try {
702
+ written = await writeFile(edit.file, edit.content, edit.baseHash);
703
+ } catch (e) {
704
+ if (!(e instanceof ApiError) || e.status !== 409) throw e;
705
+ edit = await viaHost(() => host.applyDefaults(id, body));
706
+ if (edit.edits > 0) written = await writeFile(edit.file, edit.content, edit.baseHash);
707
+ }
708
+ }
709
+ // The contract the host serves now, the write extracted.
710
+ const after = await viaHost(() => host.componentDefaults(id));
711
+ return {
712
+ file: edit.file,
713
+ wrote: written !== null,
714
+ ...(written !== null ? { hash: written.hash } : {}),
715
+ results: edit.results,
716
+ contractHash: after?.contractHash ?? edit.contractHash,
717
+ editableKeys: after?.editableKeys ?? {},
718
+ };
719
+ }
720
+
721
+ // ---- code component source ---------------------------------------------------
722
+ //
723
+ // `/api/files` is the studio's ONE write path into user code, and it is the
724
+ // same route the motion chrome's code drawer has always used. Nothing new is
725
+ // added server-side for the code studio: the containment guard, the generated
726
+ // guard, the sha256 conflict check and the atomic write are all already there,
727
+ // and reaching them through anything but this route would be a second set of
728
+ // rules over the same files.
729
+
730
+ /** Mirror of the server's FileDiagnostic. Positions are 0-based offsets into
731
+ * the text that was checked, which is the buffer when one was sent. */
732
+ export interface CodeDiagnostic {
733
+ category: "error" | "warning" | "suggestion" | "message";
734
+ code: number;
735
+ message: string;
736
+ start: number;
737
+ length: number;
738
+ line: number;
739
+ character: number;
740
+ endLine: number;
741
+ endCharacter: number;
742
+ }
743
+
744
+ export interface SourceFile {
745
+ path: string;
746
+ content: string;
747
+ /** sha256 of the bytes on disk. Sent back as `baseHash` to make a write
748
+ * conflict-checked, which is how a save can refuse rather than clobber. */
749
+ hash: string;
750
+ mtimeMs: number;
751
+ size: number;
752
+ /** What the file uses on disk, so a save can put it back. */
753
+ lineEnding: "lf" | "crlf";
754
+ /** Set when the file is a build product the studio itself emits. */
755
+ generated?: { lane: string; by: string; id: string; docName?: string };
756
+ }
757
+
758
+ export interface SourceFileSaved {
759
+ saved: true;
760
+ hash: string;
761
+ }
762
+
763
+ /** One editable file in the project, as the listing reports it. FLAT — the
764
+ * server walks the tree and answers posix paths, and the shape of the tree is
765
+ * the chrome's problem, which is right: nesting is a way of DRAWING this list,
766
+ * not a fact about the project. */
767
+ export interface ProjectFile {
768
+ path: string;
769
+ size: number;
770
+ /** Set when the file is a build product the studio itself emits. The listing
771
+ * decides this by directory alone, so a marker-only generated file reads as
772
+ * ordinary here and refuses on the way in — which is why the buffer trusts
773
+ * `SourceFile.generated` and not this. */
774
+ generated?: { lane: string; by: string; id: string; docName?: string };
775
+ }
776
+
777
+ /** `truncated` means the walk hit the server's cap and stopped. Said out loud
778
+ * in the tree rather than swallowed: a file tree that silently omits files is
779
+ * worse than no file tree. */
780
+ export interface ProjectFiles {
781
+ files: ProjectFile[];
782
+ truncated?: boolean;
783
+ }
784
+
785
+ /** Every editable file in the project. The route has always existed — the
786
+ * motion chrome's drawer used it — and this is the shot chrome finally asking.
787
+ * Editable extensions only, no dotfiles, no `node_modules`, no `.studio`. */
788
+ export function listFiles(): Promise<ProjectFiles> {
789
+ return request("/api/files");
790
+ }
791
+
792
+ export function readFile(rel: string): Promise<SourceFile> {
793
+ return request(`/api/files/${encodeURIComponent(rel)}`);
794
+ }
795
+
796
+ /** `baseHash` makes the write conflict-checked: a 409 lands in `ApiError.status`
797
+ * rather than overwriting whatever arrived under the buffer. */
798
+ export function writeFile(
799
+ rel: string,
800
+ content: string,
801
+ baseHash?: string,
802
+ ): Promise<SourceFileSaved> {
803
+ return request(`/api/files/${encodeURIComponent(rel)}`, {
804
+ method: "PUT",
805
+ body: JSON.stringify(baseHash !== undefined ? { content, baseHash } : { content }),
806
+ });
807
+ }
808
+
809
+ /** Create a new file. A path that exists answers 409 rather than overwriting. */
810
+ export function createFile(rel: string, content: string): Promise<SourceFileSaved> {
811
+ return request(`/api/files/${encodeURIComponent(rel)}`, { method: "POST", body: JSON.stringify({ content }) });
812
+ }
813
+
814
+ /** Typecheck a file. `content` checks an UNSAVED buffer instead of disk, which
815
+ * is what puts a red gutter under code the author has not committed yet.
816
+ * Hosted, the code host's extractor answers it, in the same shape. */
817
+ export function fileDiagnostics(
818
+ rel: string,
819
+ content?: string,
820
+ ): Promise<{ diagnostics: CodeDiagnostic[] }> {
821
+ const host = dailiesHost();
822
+ if (host !== undefined) return viaHost(() => host.diagnostics(rel, content));
823
+ return request("/api/diagnostics", {
824
+ method: "POST",
825
+ body: JSON.stringify(content !== undefined ? { path: rel, content } : { path: rel }),
826
+ });
827
+ }
828
+
173
829
  // ---- the asset store ---------------------------------------------------------
174
830
 
175
- /** What `POST /api/assets` answers with. `path` is the BARE content-addressed
176
- * filename; identical bytes always land on it, so a re-import is idempotent. */
831
+ /** What the Project-scoped asset POST answers with. `path` is the BARE
832
+ * content-addressed filename; identical bytes always land on it, so a
833
+ * re-import is idempotent. */
177
834
  export interface UploadedAsset {
178
835
  path: string;
179
836
  hash: string;
180
837
  /** Absent for an extension the store does not recognise. */
181
838
  kind?: string;
839
+ /** Canonical bytes after server-side conversion (not the upload's size). */
840
+ size?: number;
841
+ usedBytes?: number;
842
+ remainingBytes?: number;
843
+ /** A video's measurement, which the chrome writes onto the asset row
844
+ * (`mergeAssetMedia`) as `dailies media import` does, so one upload
845
+ * registers the same row on either host. Locally the route's own answer
846
+ * (its probe of the staged bytes, on a re-upload too); hosted, the
847
+ * browser's probe (`uploadAsset` below), since the Worker measures
848
+ * nothing. */
849
+ media?: MediaProbe;
850
+ }
851
+
852
+ export interface ProjectAssetUsage {
853
+ limitBytes: number;
854
+ usedBytes: number;
855
+ reservedBytes: number;
856
+ remainingBytes: number;
857
+ assets: ReadonlyArray<{
858
+ path: string;
859
+ hash: string;
860
+ kind?: string;
861
+ size: number;
862
+ }>;
863
+ /** Registry rows whose file is gone from local storage (the local server's
864
+ * `ProjectAssetQuota`). Absent hosted, where stored media cannot go missing. */
865
+ missing?: ReadonlyArray<{ id: string; path: string }>;
182
866
  }
183
867
 
184
868
  /**
185
- * Upload one file into the shared asset store (the F2-C route every chrome
186
- * uses; the shot lane is served by the same server, so it is the same route).
869
+ * Upload one file into the open Project's quota-accounted asset store.
187
870
  *
188
- * The filename rides a URI-ENCODED header because HTTP headers are latin-1 and
189
- * user filenames are not, and the BODY is the raw bytes — this is the one write
190
- * in this module that is not JSON, so it overrides `request`'s content-type
191
- * rather than letting it claim the bytes are JSON.
871
+ * HOSTED, the page does the local route's media work around the POST (browser
872
+ * host media/host.ts): audio is canonicalised first (the Worker stores bytes
873
+ * as they arrive and refuses any other audio), and after it a video is
874
+ * measured and every upload's sidecars are derived. Locally the route does
875
+ * all of that itself.
876
+ *
877
+ * `remainingBytes` is the meter's number (`assetUsage`), when the caller has
878
+ * one. Hosted, a file over it is refused before any byte is sent, as the
879
+ * Worker reserves the bytes before it dedupes; the server's check still
880
+ * stands behind this one.
881
+ */
882
+ export async function uploadAsset(project: string, file: File, remainingBytes?: number): Promise<UploadedAsset> {
883
+ const host = dailiesHost();
884
+ if (host === undefined) {
885
+ // Locally only the whole allowance: the route answers bytes the Project
886
+ // already holds before it checks the space left, and audio is judged as
887
+ // the WAV only the route makes.
888
+ if (!file.type.startsWith("audio/")) await refuseOverSpace(project, file, undefined);
889
+ return postAsset(project, file);
890
+ }
891
+ const media = host.media;
892
+ const prepared = await media.prepareUpload(file);
893
+ // Hosted, the browser made the WAV, so audio is judged as it will be stored.
894
+ await refuseOverSpace(project, prepared, remainingBytes, prepared === file ? undefined : "wav");
895
+ const uploaded = await postAsset(project, prepared, await sha256Hex(prepared));
896
+ const probe = await media.uploaded(uploaded.path, prepared);
897
+ return probe === null ? uploaded : { ...uploaded, media: probe };
898
+ }
899
+
900
+ /**
901
+ * The POST itself. The filename rides a URI-ENCODED header because HTTP
902
+ * headers are latin-1 and user filenames are not, and the BODY is the raw
903
+ * bytes — this is the one write in this module that is not JSON, so it
904
+ * overrides `request`'s content-type rather than letting it claim the bytes
905
+ * are JSON.
192
906
  */
193
- export function uploadAsset(file: File): Promise<UploadedAsset> {
194
- return request("/api/assets", {
907
+ function postAsset(project: string, file: File, sha256?: string): Promise<UploadedAsset> {
908
+ return request(`/api/shots/${encodeURIComponent(project)}/assets`, {
195
909
  method: "POST",
196
910
  body: file,
197
911
  headers: {
198
912
  "content-type": "application/octet-stream",
199
913
  "x-studio-filename": encodeURIComponent(file.name),
914
+ ...(sha256 === undefined ? {} : { [CONTENT_SHA256_HEADER]: sha256 }),
200
915
  },
201
916
  });
202
917
  }
203
918
 
919
+ /**
920
+ * Refuse a file the Project cannot hold before streaming a doomed body, in the
921
+ * server's own words (`quotaMessage`). Over the whole allowance needs no
922
+ * number from anyone. Over `hint` asks the server first: the meter can be
923
+ * stale (a save may have freed space since), and a refusal is only as good as
924
+ * the number it quotes. If that read fails, the server judges the upload.
925
+ */
926
+ async function refuseOverSpace(
927
+ project: string,
928
+ file: File,
929
+ hint: number | undefined,
930
+ storedAs?: "wav",
931
+ ): Promise<void> {
932
+ const wav = storedAs === undefined ? {} : { storedAs };
933
+ if (file.size > PROJECT_MEDIA_LIMIT_BYTES) {
934
+ const whole = `This file is ${formatBytes(file.size)}, over the ${formatBytes(PROJECT_MEDIA_LIMIT_BYTES)} media allowance of a whole Project.`;
935
+ throw new ApiError(
936
+ 413,
937
+ storedAs === undefined ? whole : `Audio is stored uncompressed (48 kHz WAV). ${whole}`,
938
+ undefined,
939
+ { code: "project_quota_exceeded", limitBytes: PROJECT_MEDIA_LIMIT_BYTES, requestedBytes: file.size, ...wav },
940
+ );
941
+ }
942
+ if (hint === undefined || file.size <= hint) return;
943
+ let usage: ProjectAssetUsage;
944
+ try {
945
+ usage = await projectAssetUsage(project);
946
+ } catch {
947
+ return;
948
+ }
949
+ if (typeof usage.remainingBytes !== "number" || file.size <= usage.remainingBytes) return;
950
+ const details = {
951
+ code: "project_quota_exceeded",
952
+ limitBytes: usage.limitBytes,
953
+ usedBytes: usage.usedBytes,
954
+ requestedBytes: file.size,
955
+ remainingBytes: usage.remainingBytes,
956
+ ...wav,
957
+ };
958
+ throw new ApiError(413, quotaMessage(details), undefined, details);
959
+ }
960
+
961
+ /**
962
+ * What a hosted upload says its bytes hash to (the Worker's
963
+ * `WORKER_HEADERS.contentSha256`): the Worker hashes the body anyway and
964
+ * refuses a mismatch, and skips storing bytes the workspace already holds
965
+ * (a re-upload, the same file in another Project). Hosted only; the local
966
+ * route stores every upload.
967
+ */
968
+ export const CONTENT_SHA256_HEADER = "x-studio-content-sha256";
969
+
970
+ async function sha256Hex(file: Blob): Promise<string> {
971
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()));
972
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
973
+ }
974
+
975
+ /** Server-authoritative Project usage, including bytes reserved by concurrent
976
+ * uploads. Kept beside upload because both are one quota surface. */
977
+ export function projectAssetUsage(project: string): Promise<ProjectAssetUsage> {
978
+ return request(`/api/shots/${encodeURIComponent(project)}/assets`);
979
+ }
980
+
981
+ /** The server sends the ceiling it enforced. Say THAT number back, rather than
982
+ * a literal that can drift away from the rule the refusal came from. */
983
+ function quotaMessage(details: Readonly<Record<string, unknown>>): string {
984
+ const remaining = typeof details["remainingBytes"] === "number" ? details["remainingBytes"] : 0;
985
+ const requested = typeof details["requestedBytes"] === "number" ? details["requestedBytes"] : null;
986
+ const limit = formatBytes(
987
+ typeof details["limitBytes"] === "number" ? details["limitBytes"] : PROJECT_MEDIA_LIMIT_BYTES,
988
+ );
989
+ const left = formatBytes(remaining);
990
+ const message =
991
+ requested === null
992
+ ? `This Project has ${left} of its ${limit} media allowance left.`
993
+ : `This file needs ${formatBytes(requested)}, but this Project has ${left} of its ${limit} media allowance left.`;
994
+ // The server judged the stored WAV, not the file the user picked, so say why
995
+ // the number is bigger than their MP3.
996
+ return details["storedAs"] === "wav" ? `Audio is stored uncompressed (48 kHz WAV). ${message}` : message;
997
+ }
998
+
999
+ /** A byte count as an author reads it. The one copy: the quota refusal, the
1000
+ * usage meter and every file row say the same number the same way. */
1001
+ export function formatBytes(bytes: number): string {
1002
+ const safe = Math.max(0, bytes);
1003
+ if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(safe >= 10_000_000 ? 0 : 1)} MB`;
1004
+ if (safe >= 1_000) return `${Math.round(safe / 1_000)} KB`;
1005
+ return `${safe} B`;
1006
+ }
1007
+
204
1008
  /**
205
1009
  * The stored bytes of an asset.
206
1010
  *
@@ -210,6 +1014,293 @@ export function uploadAsset(file: File): Promise<UploadedAsset> {
210
1014
  * and the stored one's can differ by a partial frame, and the clip has to be
211
1015
  * timed against what will actually be mixed.
212
1016
  */
1017
+ /** One workspace font (store `WorkspaceFont`): uploaded once to the
1018
+ * workspace, offered in every Project's font list. Its id is the stored path
1019
+ * a Project copy of the same bytes gets, so a copy is found by
1020
+ * `asset.path === id`. The optional keys are what the file's name table
1021
+ * says, and are absent when it could not be read (a file uploaded before
1022
+ * migration 0033 keeps its file-name family and nothing else). */
1023
+ export interface WorkspaceFont {
1024
+ id: string;
1025
+ /** The name table's family (name ID 16, then 1), else the file stem. */
1026
+ family: string;
1027
+ filename: string;
1028
+ format: string;
1029
+ size: number;
1030
+ /** OS/2 usWeightClass, 1 to 1000. */
1031
+ weight?: number;
1032
+ italic?: boolean;
1033
+ /** A variable font's `wght` axis: [min, default, max]. */
1034
+ wghtAxis?: [number, number, number];
1035
+ }
1036
+
1037
+ /** `GET /api/fonts` on both hosts. `canManage` is whether this person may
1038
+ * upload and remove (`fonts.manage`; always locally). */
1039
+ export interface WorkspaceFontList {
1040
+ fonts: WorkspaceFont[];
1041
+ canManage: boolean;
1042
+ maxFonts: number;
1043
+ limitBytes: number;
1044
+ }
1045
+
1046
+ export function listWorkspaceFonts(): Promise<WorkspaceFontList> {
1047
+ return request("/api/fonts");
1048
+ }
1049
+
1050
+ export const workspaceFontUrl = (id: string): string => `/studio/fonts/${encodeURIComponent(id)}`;
1051
+
1052
+ /**
1053
+ * Add a font file to the workspace's fonts (`POST /studio/fonts`, the font
1054
+ * picker's upload, on both hosts). `created` is false when the workspace
1055
+ * already held these bytes (a 200 answering that row). A refusal is an
1056
+ * `ApiError` carrying the server's message and `code` as they came
1057
+ * (`font_too_large`, `fonts_full`, `forbidden`, `rate_limited`...), with the
1058
+ * whole body in `details` (`limitBytes`, `maxFonts`, `retryAfter`).
1059
+ */
1060
+ export async function uploadWorkspaceFont(file: File): Promise<{ font: WorkspaceFont; created: boolean }> {
1061
+ let status = 0;
1062
+ const body = await request<{ font: WorkspaceFont | null }>(
1063
+ "/studio/fonts",
1064
+ {
1065
+ method: "POST",
1066
+ body: file,
1067
+ headers: { "content-type": "application/octet-stream", "x-studio-filename": encodeURIComponent(file.name) },
1068
+ },
1069
+ false,
1070
+ (s) => {
1071
+ status = s;
1072
+ },
1073
+ );
1074
+ if (body.font === null) throw new ApiError(status, `${file.name} was stored, but the workspace could not read it back.`);
1075
+ return { font: body.font, created: status === 201 };
1076
+ }
1077
+
1078
+ /** How a font download reports: bytes read so far, and the whole size when
1079
+ * the response says it (`Content-Length` on an unencoded body), else null. */
1080
+ export interface FontDownloadOptions {
1081
+ signal?: AbortSignal;
1082
+ onProgress?: (loaded: number, total: number | null) => void;
1083
+ }
1084
+
1085
+ /** A response body read whole, reporting as it streams. */
1086
+ async function readFontBody(res: Response, opts: FontDownloadOptions): Promise<Blob> {
1087
+ const reader = opts.onProgress === undefined ? undefined : res.body?.getReader();
1088
+ if (reader === undefined) return res.blob();
1089
+ const length = Number(res.headers.get("content-length"));
1090
+ const total = res.headers.has("content-encoding") || !(length > 0) ? null : length;
1091
+ const chunks: Uint8Array[] = [];
1092
+ let loaded = 0;
1093
+ opts.onProgress?.(0, total);
1094
+ for (;;) {
1095
+ const { done, value } = await reader.read();
1096
+ if (done) break;
1097
+ chunks.push(value);
1098
+ loaded += value.byteLength;
1099
+ opts.onProgress?.(loaded, total);
1100
+ }
1101
+ return new Blob(chunks);
1102
+ }
1103
+
1104
+ /** Who a saved video's page opens for (worker videos.ts). */
1105
+ export type VideoVisibility = "private" | "workspace" | "link";
1106
+
1107
+ export interface SaveVideoMeta {
1108
+ /** The Project's name (the store's `name`), whose export this is. */
1109
+ project: string;
1110
+ title: string;
1111
+ filename: string;
1112
+ visibility: VideoVisibility;
1113
+ /** The "Sound credits" block the export owes (v49), shown on its page. */
1114
+ credits?: string;
1115
+ durationMs: number;
1116
+ width: number;
1117
+ height: number;
1118
+ }
1119
+
1120
+ /**
1121
+ * Save an export to the workspace's Videos, hosted only, and answer its id
1122
+ * and page. In parts, at the size the server names: a video can be larger
1123
+ * than one request may carry.
1124
+ */
1125
+ export async function saveVideo(blob: Blob, meta: SaveVideoMeta): Promise<{ id: string; url: string }> {
1126
+ const started = await request<{ id: string; partBytes: number; parts: number }>("/studio/videos", {
1127
+ method: "POST",
1128
+ body: JSON.stringify({ ...meta, size: blob.size }),
1129
+ });
1130
+ const base = `/studio/videos/${encodeURIComponent(started.id)}`;
1131
+ const parts: Array<{ partNumber: number; etag: string }> = [];
1132
+ for (let n = 1; n <= started.parts; n++) {
1133
+ parts.push(
1134
+ await request(`${base}/parts/${n}`, {
1135
+ method: "PUT",
1136
+ body: blob.slice((n - 1) * started.partBytes, n * started.partBytes),
1137
+ headers: { "content-type": "application/octet-stream" },
1138
+ }),
1139
+ );
1140
+ }
1141
+ const done = await request<{ url: string }>(`${base}/complete`, { method: "POST", body: JSON.stringify({ parts }) });
1142
+ return { id: started.id, url: done.url };
1143
+ }
1144
+
1145
+ /** The font's bytes as a File named like the upload, ready for `uploadAsset`. */
1146
+ export async function fetchWorkspaceFont(font: WorkspaceFont, opts: FontDownloadOptions = {}): Promise<File> {
1147
+ const url = workspaceFontUrl(font.id);
1148
+ const res = await fetch(url, opts.signal === undefined ? undefined : { signal: opts.signal });
1149
+ if (!res.ok) throw new ApiError(res.status, `${font.family} is no longer in this workspace's fonts.`);
1150
+ return new File([await readFontBody(res, opts)], font.filename, { type: res.headers.get("content-type") ?? "" });
1151
+ }
1152
+
1153
+ /** A font file from a url (a Google font on jsDelivr, the chrome's bundled
1154
+ * Inter) as a File named `name`, ready for `uploadAsset`. */
1155
+ export async function fetchFontFile(url: string, name: string, opts: FontDownloadOptions = {}): Promise<File> {
1156
+ const res = await fetch(url, opts.signal === undefined ? undefined : { signal: opts.signal });
1157
+ if (!res.ok) throw new ApiError(res.status, `${name} could not be fetched (${res.status}).`);
1158
+ return new File([await readFontBody(res, opts)], name, { type: "font/woff2" });
1159
+ }
1160
+
1161
+ // ---- the generate drawer (Sound mode) ----------------------------------------
1162
+ //
1163
+ // The wire of `/studio/audio/generate`, on both hosts. The shapes are
1164
+ // `GenerationStatus` and `GenerationTake` in `@dailies-studio/store`
1165
+ // (audioGeneration.ts), restated because the chrome does not depend on the
1166
+ // store package.
1167
+
1168
+ export type GenerationKind = "sfx" | "music" | "voice";
1169
+
1170
+ export interface GenerationTake {
1171
+ id: string;
1172
+ kind: GenerationKind;
1173
+ prompt: string;
1174
+ seconds: number | null;
1175
+ voice: string | null;
1176
+ provider: string;
1177
+ model: string;
1178
+ createdAt: number;
1179
+ name: string;
1180
+ /** Where the bytes are served, same-origin. */
1181
+ url: string;
1182
+ /** Set when the take is already a stored asset (local). */
1183
+ path?: string;
1184
+ hash?: string;
1185
+ /** In the sound history past its waiting day. */
1186
+ kept?: boolean;
1187
+ /** Music composed to a Project's sections: it lines up only from frame 0. */
1188
+ scored?: boolean;
1189
+ }
1190
+
1191
+ /** One page of the sound history, newest first (store `GenerationHistory`).
1192
+ * A page can be short, even empty, and still have a `next`. */
1193
+ export interface GenerationHistory {
1194
+ takes: GenerationTake[];
1195
+ next: string | null;
1196
+ }
1197
+
1198
+ export interface GenerationStatus {
1199
+ host: "local" | "hosted";
1200
+ kinds: Record<GenerationKind, { ok: boolean; reason?: string }>;
1201
+ voices: ReadonlyArray<{ id: string; name: string; description: string }>;
1202
+ caps: {
1203
+ promptMaxChars: number;
1204
+ sfx: { minSeconds: number; maxSeconds: number; defaultSeconds: number };
1205
+ music: { minSeconds: number; maxSeconds: number; defaultSeconds: number };
1206
+ voice: { maxChars: number };
1207
+ };
1208
+ /** Hosted, with no ElevenLabs key the workspace can use: "add" when this
1209
+ * member may add one (Settings, AI keys), "ask" when an Owner or Admin must. */
1210
+ needsKey?: "add" | "ask";
1211
+ recent: GenerationTake[];
1212
+ }
1213
+
1214
+ export interface GenerationBody {
1215
+ project: string;
1216
+ kind: GenerationKind;
1217
+ prompt: string;
1218
+ seconds?: number;
1219
+ voice?: string;
1220
+ instrumental?: boolean;
1221
+ loop?: boolean;
1222
+ /** Music scored to the Project: its sections, in order (`scoreSectionsOf`). */
1223
+ sections?: Array<{ name: string; durationMs: number }>;
1224
+ }
1225
+
1226
+ export const AUDIO_GENERATE_ROUTE = "/studio/audio/generate";
1227
+
1228
+ export function generationStatus(project: string): Promise<GenerationStatus> {
1229
+ return request(`${AUDIO_GENERATE_ROUTE}?project=${encodeURIComponent(project)}`);
1230
+ }
1231
+
1232
+ export function generateAudioTake(body: GenerationBody): Promise<{ take: GenerationTake }> {
1233
+ return request(AUDIO_GENERATE_ROUTE, { method: "POST", body: JSON.stringify(body) });
1234
+ }
1235
+
1236
+ /** The sound history: every take this member may see that is kept or still
1237
+ * in its day, newest first; `before` asks for the page after a `next`. */
1238
+ export function generationHistory(before?: string): Promise<GenerationHistory> {
1239
+ return request(`${AUDIO_GENERATE_ROUTE}/history${before === undefined ? "" : `?before=${encodeURIComponent(before)}`}`);
1240
+ }
1241
+
1242
+ /** Keep a take in the history past its day: it went into `project` (by its
1243
+ * storage name), which lets a member keep another's take they added. */
1244
+ export function keepGeneration(id: string, project?: string): Promise<{ kept: boolean }> {
1245
+ return request(`${AUDIO_GENERATE_ROUTE}/${encodeURIComponent(id)}/keep`, {
1246
+ method: "POST",
1247
+ body: JSON.stringify(project === undefined ? {} : { project }),
1248
+ });
1249
+ }
1250
+
1251
+ /** Delete a take from the history. Locally its file stays, for any Project
1252
+ * that uses it; hosted, its waiting-room bytes go. */
1253
+ export function deleteGeneration(id: string): Promise<{ deleted: boolean }> {
1254
+ return request(`${AUDIO_GENERATE_ROUTE}/${encodeURIComponent(id)}`, { method: "DELETE" });
1255
+ }
1256
+
1257
+ /** A hosted take's bytes as a File, ready for `importAudio`. */
1258
+ export async function fetchGenerationFile(take: GenerationTake): Promise<File> {
1259
+ const res = await fetch(take.url);
1260
+ if (!res.ok) throw new ApiError(res.status, "That take has expired. Generate it again.");
1261
+ return new File([await res.blob()], `${take.name}.mp3`, { type: res.headers.get("content-type") ?? "audio/mpeg" });
1262
+ }
1263
+
1264
+ // ---- stock sounds (the Library's Stock sounds) -------------------------------
1265
+ //
1266
+ // The wire of `/api/openverse`, on both hosts. `OpenverseTake` is the store's
1267
+ // (openverse.ts), restated because the chrome does not depend on the store
1268
+ // package. The search itself never comes here: the Library searches Openverse
1269
+ // from this browser (`panels/library/stockSearch.ts`).
1270
+
1271
+ export const OPENVERSE_ROUTE = "/api/openverse";
1272
+
1273
+ export interface OpenverseTake {
1274
+ /** The provenance to record: the host's own read of the row. */
1275
+ source: OpenverseSource3D;
1276
+ /** Same-origin: where the file is read. */
1277
+ file: string;
1278
+ filename: string;
1279
+ durationMs: number;
1280
+ }
1281
+
1282
+ /** Whether this host can add a stock sound: locally always, hosted once the
1283
+ * deployment has a registered Openverse app. */
1284
+ export function openverseStatus(): Promise<{ imports: boolean }> {
1285
+ return request(OPENVERSE_ROUTE);
1286
+ }
1287
+
1288
+ /** The host reads the row itself and answers the provenance to record and
1289
+ * where to read the file. */
1290
+ export function openverseTake(input: { id: string; project: string }): Promise<OpenverseTake> {
1291
+ return request(`${OPENVERSE_ROUTE}/take`, { method: "POST", body: JSON.stringify(input) });
1292
+ }
1293
+
1294
+ /** A take's bytes as a File, ready for the audio upload lane. */
1295
+ export async function fetchOpenverseFile(take: OpenverseTake): Promise<File> {
1296
+ const res = await fetch(take.file);
1297
+ if (!res.ok) {
1298
+ const body = (await res.json().catch(() => null)) as { error?: unknown } | null;
1299
+ throw new ApiError(res.status, typeof body?.error === "string" ? body.error : `The sound could not be fetched (${res.status}).`);
1300
+ }
1301
+ return new File([await res.blob()], take.filename, { type: "audio/mpeg" });
1302
+ }
1303
+
213
1304
  export async function fetchAssetBytes(path: string): Promise<ArrayBuffer> {
214
1305
  const url = `/studio/assets/${encodeURIComponent(path)}`;
215
1306
  const res = await fetch(url);
@@ -227,6 +1318,16 @@ export interface RenderJob {
227
1318
  total: number;
228
1319
  out?: string;
229
1320
  error?: string;
1321
+ /** Every `warning:` line the render emitted. Optional on the wire: a server
1322
+ * older than this field simply omits it, and the dialog renders nothing. */
1323
+ warnings?: string[];
1324
+ /** A failed render's last output lines, `[page]` errors included. */
1325
+ log?: string[];
1326
+ /** A failed render's forensics folder, when one was written. */
1327
+ diagnostics?: string;
1328
+ /** What the render is doing before its first frame, such as downloading
1329
+ * the browser renderer. Cleared once frames start. */
1330
+ note?: string;
230
1331
  elapsedMs: number;
231
1332
  }
232
1333
 
@@ -235,6 +1336,9 @@ export interface RenderRequest {
235
1336
  quality: ExportQualityId;
236
1337
  /** Absent = the whole timeline. */
237
1338
  sceneId?: string;
1339
+ /** Master frames [start, end) of the whole timeline, for the work area.
1340
+ * Never sent with `sceneId`. */
1341
+ frameRange?: { start: number; end: number };
238
1342
  /** `false` renders a SILENT file even though the document has clips. Absent
239
1343
  * = the server's default, which is to mux whatever `doc.audio` carries. */
240
1344
  audio?: boolean;
@@ -270,6 +1374,414 @@ export function cancelRender(name: string): Promise<{ cancelled: boolean }> {
270
1374
  return request(`/api/shots/${encodeURIComponent(name)}/render`, { method: "DELETE" });
271
1375
  }
272
1376
 
1377
+ // ---- comments (packages/store/src/commentRoutes.ts) ------------------------
1378
+
1379
+ /** A pin on a storyboard Frame (a still). */
1380
+ export interface CommentFrameAnchor {
1381
+ kind: "frame";
1382
+ /** The Shot the Frame sat in when the thread was made. */
1383
+ sceneId: string;
1384
+ stillId: string;
1385
+ /** 0..1 across the picture, left to right. */
1386
+ u: number;
1387
+ /** 0..1 down the picture, top to bottom. */
1388
+ v: number;
1389
+ layerId: string | null;
1390
+ frame: number;
1391
+ frameLabel: string | null;
1392
+ }
1393
+
1394
+ /** A thread on a board card (migration 0029): a planned shot (`partId`) or
1395
+ * a reference (`referenceId`), exactly one of the two. `sceneId` is the Shot
1396
+ * the card pointed at when written, or null. `stillId` is never set: it is
1397
+ * here so "does this thread sit on still X" reads on any anchor. */
1398
+ export interface CommentCardAnchor {
1399
+ kind: "card";
1400
+ partId: string | null;
1401
+ referenceId: string | null;
1402
+ sceneId: string | null;
1403
+ stillId?: never;
1404
+ }
1405
+
1406
+ /** A thread on a spot in the Studio (migration 0029): a Shot at a Shot-local
1407
+ * `frame`, the layer under the click (or none), the point as 0..1 of the
1408
+ * camera frame. `stillId` as on a card. */
1409
+ export interface CommentStudioAnchor {
1410
+ kind: "studio";
1411
+ sceneId: string;
1412
+ frame: number;
1413
+ layerId: string | null;
1414
+ u: number;
1415
+ v: number;
1416
+ stillId?: never;
1417
+ }
1418
+
1419
+ /** Where a thread is pinned, always read back with its `kind`. */
1420
+ export type CommentAnchor = CommentFrameAnchor | CommentCardAnchor | CommentStudioAnchor;
1421
+ export type CommentAnchorKind = CommentAnchor["kind"];
1422
+
1423
+ /** One thread linked to another (0029): a board thread and a Studio thread,
1424
+ * seen from either side. `kind` is the other thread's anchor kind. */
1425
+ export interface CommentLink {
1426
+ threadId: string;
1427
+ kind: CommentAnchorKind;
1428
+ }
1429
+
1430
+ export interface CommentPerson {
1431
+ id: string;
1432
+ login: string;
1433
+ displayName: string | null;
1434
+ avatarKey: string | null;
1435
+ }
1436
+
1437
+ export interface CommentMessage {
1438
+ id: string;
1439
+ threadId: string;
1440
+ authorId: string;
1441
+ authorLabel: string;
1442
+ authorAvatarKey: string | null;
1443
+ /** Mentions are inline `<@usr-...>` tokens. Empty when deleted. */
1444
+ body: string;
1445
+ viaAgent: boolean;
1446
+ createdAt: number;
1447
+ editedAt: number | null;
1448
+ deleted: boolean;
1449
+ can: { edit: boolean; delete: boolean };
1450
+ }
1451
+
1452
+ export interface CommentThread {
1453
+ id: string;
1454
+ projectId: string;
1455
+ anchor: CommentAnchor;
1456
+ /** The threads linked to this one, in the other place. */
1457
+ links: CommentLink[];
1458
+ status: "open" | "resolved";
1459
+ createdBy: string;
1460
+ assignee: CommentPerson | null;
1461
+ resolvedBy: string | null;
1462
+ resolvedAt: number | null;
1463
+ messageCount: number;
1464
+ snapshotKey: string | null;
1465
+ createdAt: number;
1466
+ updatedAt: number;
1467
+ lastMessageAt: number;
1468
+ head: CommentMessage | null;
1469
+ viewer: { lastReadAt: number | null; following: boolean; unread: boolean };
1470
+ /** Whether the anchor is there, judged against the SAVED document. */
1471
+ orphan: CommentOrphan;
1472
+ can: { reply: boolean; resolve: boolean; assign: boolean; delete: boolean; snapshot: boolean };
1473
+ /** Oldest first; only on a single-thread read. */
1474
+ messages?: CommentMessage[];
1475
+ }
1476
+
1477
+ /** Null while the anchor is there: a Frame pin's Frame, a card's part or
1478
+ * reference, a Studio thread's Shot and layer. */
1479
+ export type CommentOrphan = "frame-removed" | "card-removed" | "layer-removed" | "shot-removed" | null;
1480
+
1481
+ export interface MentionablePerson extends CommentPerson {
1482
+ guest: boolean;
1483
+ }
1484
+
1485
+ export interface Mentionable {
1486
+ /** People who can open the Project, with their role on it. */
1487
+ people: (MentionablePerson & { role: string })[];
1488
+ /** Members who cannot open it yet: only for a caller who may share it. */
1489
+ others: MentionablePerson[];
1490
+ canShare: boolean;
1491
+ }
1492
+
1493
+ export interface InboxItem {
1494
+ id: number;
1495
+ kind: "mention" | "reply" | "assigned" | "resolved" | "reopened";
1496
+ projectId: string;
1497
+ /** The Project's store name, the editor's `?shot=`; null when unknown. */
1498
+ projectName: string | null;
1499
+ projectTitle: string | null;
1500
+ threadId: string;
1501
+ /** '' for a card written on no Shot. */
1502
+ sceneId: string;
1503
+ /** '' for a card or Studio thread. */
1504
+ stillId: string;
1505
+ frameLabel: string | null;
1506
+ /** Where the thread sits, so an item opens the right place. */
1507
+ anchorKind: CommentAnchorKind;
1508
+ messageId: string | null;
1509
+ excerpt: string;
1510
+ actorId: string;
1511
+ actorLabel: string;
1512
+ viaAgent: boolean;
1513
+ createdAt: number;
1514
+ readAt: number | null;
1515
+ }
1516
+
1517
+ export type ThreadFilter = {
1518
+ status?: "open" | "resolved";
1519
+ filter?: "mine" | "mentions";
1520
+ cursor?: string;
1521
+ kind?: CommentAnchorKind;
1522
+ partId?: string;
1523
+ referenceId?: string;
1524
+ sceneId?: string;
1525
+ };
1526
+
1527
+ const query = (params: Record<string, string | number | undefined>): string =>
1528
+ new URLSearchParams(
1529
+ Object.entries(params).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])),
1530
+ ).toString();
1531
+
1532
+ const threadPath = (id: string): string => `/api/comments/${encodeURIComponent(id)}`;
1533
+
1534
+ export function listThreads(
1535
+ projectId: string,
1536
+ filter: ThreadFilter = {},
1537
+ ): Promise<{ threads: CommentThread[]; nextCursor?: string }> {
1538
+ return request(`/api/comments?${query({ project: projectId, limit: 50, ...filter })}`);
1539
+ }
1540
+
1541
+ export async function getThread(id: string): Promise<CommentThread> {
1542
+ return (await request<{ thread: CommentThread }>(threadPath(id))).thread;
1543
+ }
1544
+
1545
+ export async function createThread(projectId: string, anchor: CommentAnchor, body: string): Promise<CommentThread> {
1546
+ const res = await request<{ thread: CommentThread }>("/api/comments", {
1547
+ method: "POST",
1548
+ body: JSON.stringify({ projectId, anchor, body }),
1549
+ });
1550
+ return res.thread;
1551
+ }
1552
+
1553
+ export function replyThread(id: string, body: string): Promise<{ message: CommentMessage; thread: CommentThread }> {
1554
+ return request(`${threadPath(id)}/messages`, { method: "POST", body: JSON.stringify({ body }) });
1555
+ }
1556
+
1557
+ export async function patchThread(
1558
+ id: string,
1559
+ patch: { status?: "open" | "resolved"; assigneeId?: string | null },
1560
+ ): Promise<CommentThread> {
1561
+ return (await request<{ thread: CommentThread }>(threadPath(id), { method: "PATCH", body: JSON.stringify(patch) })).thread;
1562
+ }
1563
+
1564
+ /** Link a board thread and a Studio thread of one Project. Idempotent.
1565
+ * Answers this thread with its links. */
1566
+ export async function linkThread(id: string, otherId: string): Promise<CommentThread> {
1567
+ const res = await request<{ thread: CommentThread }>(`${threadPath(id)}/links`, {
1568
+ method: "POST",
1569
+ body: JSON.stringify({ threadId: otherId }),
1570
+ });
1571
+ return res.thread;
1572
+ }
1573
+
1574
+ /** Undo `linkThread`. Idempotent. Answers this thread with its links. */
1575
+ export async function unlinkThread(id: string, otherId: string): Promise<CommentThread> {
1576
+ const res = await request<{ thread: CommentThread }>(`${threadPath(id)}/links/${encodeURIComponent(otherId)}`, {
1577
+ method: "DELETE",
1578
+ });
1579
+ return res.thread;
1580
+ }
1581
+
1582
+ export async function deleteThread(id: string): Promise<void> {
1583
+ await request(threadPath(id), { method: "DELETE" });
1584
+ }
1585
+
1586
+ export async function editMessage(threadId: string, messageId: string, body: string): Promise<CommentMessage | null> {
1587
+ const res = await request<{ message: CommentMessage | null }>(
1588
+ `${threadPath(threadId)}/messages/${encodeURIComponent(messageId)}`,
1589
+ { method: "PATCH", body: JSON.stringify({ body }) },
1590
+ );
1591
+ return res.message;
1592
+ }
1593
+
1594
+ export async function deleteMessage(threadId: string, messageId: string): Promise<void> {
1595
+ await request(`${threadPath(threadId)}/messages/${encodeURIComponent(messageId)}`, { method: "DELETE" });
1596
+ }
1597
+
1598
+ export async function markThreadRead(id: string): Promise<void> {
1599
+ await request(`${threadPath(id)}/read`, { method: "POST" });
1600
+ }
1601
+
1602
+ export function mentionable(projectId: string, q: string): Promise<Mentionable> {
1603
+ return request(`/api/comments/mentionable?${query({ project: projectId, q: q === "" ? undefined : q, limit: 8 })}`);
1604
+ }
1605
+
1606
+ /** One person who can open a Project (the workspace's people route). */
1607
+ export interface ProjectPerson {
1608
+ userId: string;
1609
+ login: string;
1610
+ name: string;
1611
+ avatarUrl: string | null;
1612
+ isYou: boolean;
1613
+ projectRole: string;
1614
+ guest: boolean;
1615
+ }
1616
+
1617
+ /** Who can open a Project: names and avatars for the presence faces. Hosted
1618
+ * only. */
1619
+ export function projectPeople(name: string): Promise<{ people: ProjectPerson[] }> {
1620
+ return request(`/api/projects/shot/${encodeURIComponent(name)}/people`);
1621
+ }
1622
+
1623
+ export function inbox(unreadOnly: boolean): Promise<{ items: InboxItem[]; nextCursor?: string; unread: number }> {
1624
+ return request(`/api/inbox?${query({ unread: unreadOnly ? 1 : undefined, limit: 30 })}`);
1625
+ }
1626
+
1627
+ export async function markInboxRead(ids?: readonly number[]): Promise<void> {
1628
+ await request("/api/inbox/read", { method: "POST", body: JSON.stringify(ids === undefined ? {} : { ids }) });
1629
+ }
1630
+
1631
+ /** Keep the picture the author commented on (webp, png or jpeg, at most
1632
+ * 256 KB): its author, once. A removed Frame's thread shows it. */
1633
+ export async function putSnapshot(id: string, picture: Blob): Promise<void> {
1634
+ await request(`${threadPath(id)}/snapshot`, {
1635
+ method: "PUT",
1636
+ body: picture,
1637
+ headers: { "content-type": picture.type },
1638
+ });
1639
+ }
1640
+
1641
+ /** The picture a thread keeps, for an `<img src>`. */
1642
+ export const snapshotUrl = (id: string): string => `${threadPath(id)}/snapshot`;
1643
+
1644
+ /** Ask the Project's admins for Editor. Always 202, so nothing leaks about
1645
+ * whether a request was filed (packages/worker/src/requests.ts). */
1646
+ export async function requestEditAccess(projectId: string): Promise<void> {
1647
+ await request("/api/workspace/requests", { method: "POST", body: JSON.stringify({ projectId, role: "editor" }) });
1648
+ }
1649
+
1650
+ // ---- the workspace component store (packages/store/src/workspaceComponents.ts) --
1651
+ //
1652
+ // Restated here, as the comment shapes above are: the chrome cannot import the
1653
+ // store package. Keep these in step with that file.
1654
+
1655
+ /** Where a component lives: a definition in a Project, or a code module. */
1656
+ export type WorkspaceComponentHome = { projectId: string; def: string } | { component: string };
1657
+
1658
+ export interface WorkspaceComponent {
1659
+ id: string;
1660
+ kind: "studio" | "code";
1661
+ home: WorkspaceComponentHome;
1662
+ name: string;
1663
+ slug: string;
1664
+ /** The latest version's number and contentHash. */
1665
+ version: number;
1666
+ contentHash: string;
1667
+ createdAt: number;
1668
+ updatedAt: number;
1669
+ createdBy: string;
1670
+ /** A Studio-made component's home Project name, as the list answers it. */
1671
+ homeName?: string | null;
1672
+ /** In the workspace's library (`setWorkspaceComponentLibrary`): every
1673
+ * Project lists it under Your library. */
1674
+ library?: true;
1675
+ }
1676
+
1677
+ /** A code file a live Project lists as made for it or uses, by module path
1678
+ * (`src/Gem.tsx`). `projectId` is null for a Project the caller may not
1679
+ * view: the file still belongs to some Project, just not one it may name. */
1680
+ export interface ComponentFileHome {
1681
+ path: string;
1682
+ projectId: string | null;
1683
+ }
1684
+
1685
+ /** One version as the History list shows it. */
1686
+ export interface WorkspaceComponentVersionInfo {
1687
+ version: number;
1688
+ contentHash: string;
1689
+ name: string;
1690
+ /** The exposed prop keys (studio) or the contract keys (code), in order. */
1691
+ propKeys: string[];
1692
+ mediaHashes: string[];
1693
+ createdAt: number;
1694
+ createdBy: string;
1695
+ }
1696
+
1697
+ export interface StudioComponentPayload {
1698
+ kind: "studio";
1699
+ def: ComponentDef3D;
1700
+ nested: Record<string, ComponentDef3D>;
1701
+ assets: Record<string, ShotAsset>;
1702
+ materials: Record<string, MaterialGraph3D>;
1703
+ }
1704
+
1705
+ export interface CodeComponentPayload {
1706
+ kind: "code";
1707
+ files: { path: string; content: string }[];
1708
+ contract: unknown;
1709
+ }
1710
+
1711
+ /** A version with its payload: the view and restore read. */
1712
+ export interface WorkspaceComponentVersion extends WorkspaceComponentVersionInfo {
1713
+ payload: StudioComponentPayload | CodeComponentPayload;
1714
+ }
1715
+
1716
+ /** One row of the usage index. */
1717
+ export type IndexedComponentUse = ComponentUse & { projectId: string; projectName: string };
1718
+
1719
+ export interface RecordedComponentVersion {
1720
+ component: WorkspaceComponent;
1721
+ version: WorkspaceComponentVersionInfo;
1722
+ created: boolean;
1723
+ }
1724
+
1725
+ const WORKSPACE_COMPONENTS = "/api/workspace-components";
1726
+ const workspaceComponentPath = (id: string): string => `${WORKSPACE_COMPONENTS}/${encodeURIComponent(id)}`;
1727
+
1728
+ /** Every workspace component the caller may see, and which Project each code
1729
+ * file belongs to (`homes`), the index Your library reads. */
1730
+ export function listWorkspaceComponents(): Promise<{ components: WorkspaceComponent[]; homes: ComponentFileHome[] }> {
1731
+ return request(WORKSPACE_COMPONENTS);
1732
+ }
1733
+
1734
+ /** Add a recorded component to the workspace's library, or take it out.
1735
+ * Answers the row as it now stands. */
1736
+ export async function setWorkspaceComponentLibrary(id: string, library: boolean): Promise<WorkspaceComponent> {
1737
+ return (
1738
+ await request<{ component: WorkspaceComponent }>(`${workspaceComponentPath(id)}/library`, {
1739
+ method: "PUT",
1740
+ body: JSON.stringify({ library }),
1741
+ })
1742
+ ).component;
1743
+ }
1744
+
1745
+ /** Record a version from the saved document (studio) or the registry (code).
1746
+ * The same content twice writes nothing (`created: false`). */
1747
+ export function recordWorkspaceComponent(
1748
+ body: { kind: "studio"; projectId: string; def: string } | { kind: "code"; component: string },
1749
+ ): Promise<RecordedComponentVersion> {
1750
+ return request(WORKSPACE_COMPONENTS, { method: "POST", body: JSON.stringify(body) });
1751
+ }
1752
+
1753
+ /** Newest first. */
1754
+ export async function workspaceComponentVersions(id: string): Promise<WorkspaceComponentVersionInfo[]> {
1755
+ return (await request<{ versions: WorkspaceComponentVersionInfo[] }>(`${workspaceComponentPath(id)}/versions`)).versions;
1756
+ }
1757
+
1758
+ export async function workspaceComponentVersion(id: string, version: number): Promise<WorkspaceComponentVersion> {
1759
+ return (await request<{ version: WorkspaceComponentVersion }>(`${workspaceComponentPath(id)}/versions/${version}`)).version;
1760
+ }
1761
+
1762
+ export async function workspaceComponentUses(id: string): Promise<IndexedComponentUse[]> {
1763
+ return (await request<{ uses: IndexedComponentUse[] }>(`${workspaceComponentPath(id)}/uses`)).uses;
1764
+ }
1765
+
1766
+ let buildProbe = 0;
1767
+
1768
+ /**
1769
+ * Why a code component's module does not build now, or null when it does.
1770
+ * Hosted, the code host's compile answers (after a sync). Locally, the dev
1771
+ * server's transform: asking for the module is what compiles it, and a module
1772
+ * that fails answers non-2xx with no usable body (Vite's middleware mode hands
1773
+ * the error on without one), hence the plain sentence.
1774
+ */
1775
+ export async function componentBuildError(rel: string): Promise<string | null> {
1776
+ const host = dailiesHost();
1777
+ if (host !== undefined) {
1778
+ if (host.buildErrors === undefined) return null;
1779
+ return (await host.buildErrors())[rel] ?? null;
1780
+ }
1781
+ const res = await fetch(`/${rel.split("/").map(encodeURIComponent).join("/")}?dailies-build=${++buildProbe}`);
1782
+ return res.ok ? null : `${rel} does not build.`;
1783
+ }
1784
+
273
1785
  // ---- the agent lane ----------------------------------------------------------
274
1786
 
275
1787
  /**
@@ -287,3 +1799,67 @@ const agentTransport: AgentTransport = {
287
1799
  };
288
1800
 
289
1801
  export const agent: AgentClient = createAgentClient(agentTransport);
1802
+
1803
+ // ---- Community component listings (worker componentListings.ts) ----------
1804
+ // The three Studio-side calls on this host. Listing a component, its link and
1805
+ // its preview are written on the main host (`/c/share`), not here.
1806
+
1807
+ const COMPONENT_LISTINGS = "/studio/component-listings";
1808
+
1809
+ /** The owner side: this workspace's listing of a component, if any, with how
1810
+ * many people added each version. */
1811
+ export interface OwnedComponentListing {
1812
+ id: string;
1813
+ status: "draft" | "unlisted" | "published";
1814
+ version: number | null;
1815
+ name: string;
1816
+ people: { version: number; people: number }[];
1817
+ }
1818
+
1819
+ export async function ownedComponentListing(componentId: string): Promise<OwnedComponentListing | null> {
1820
+ return (
1821
+ await request<{ listing: OwnedComponentListing | null }>(
1822
+ `${COMPONENT_LISTINGS}/owned?component=${encodeURIComponent(componentId)}`,
1823
+ )
1824
+ ).listing;
1825
+ }
1826
+
1827
+ /** The copy side: the latest version of each listing these copies came from
1828
+ * (`origin.source`). A copy whose `origin.version` is lower has an update. */
1829
+ export interface ComponentListingUpdate {
1830
+ source: string;
1831
+ version: number;
1832
+ contentHash: string;
1833
+ name: string;
1834
+ }
1835
+
1836
+ export async function componentListingUpdates(sources: readonly string[]): Promise<ComponentListingUpdate[]> {
1837
+ const ids = [...new Set(sources)].filter((s) => s.startsWith("cml-")).slice(0, 50);
1838
+ if (ids.length === 0) return [];
1839
+ return (
1840
+ await request<{ updates: ComponentListingUpdate[] }>(`${COMPONENT_LISTINGS}/updates?source=${ids.join(",")}`)
1841
+ ).updates;
1842
+ }
1843
+
1844
+ /** The latest version of a listing this workspace may see, as its frozen
1845
+ * closure, so an update can name what it keeps aside before the yes. */
1846
+ export function componentListingDefinition(
1847
+ listingId: string,
1848
+ ): Promise<{ version: number; contentHash: string; closure: DefClosure }> {
1849
+ return request(`${COMPONENT_LISTINGS}/${encodeURIComponent(listingId)}/definition`);
1850
+ }
1851
+
1852
+ /** Replace the copy `def` in `project` with the listing's latest version.
1853
+ * Overrides move by key (`carryOverrides`); `quarantined` counts the ones
1854
+ * kept under their old names. The store writes the document, so the open
1855
+ * editor receives it as a remote change. */
1856
+ export function applyComponentListingUpdate(
1857
+ listingId: string,
1858
+ project: string,
1859
+ def: string,
1860
+ ): Promise<{ defId: string; changed: boolean; quarantined: number; rev: string; version: number }> {
1861
+ return request(`${COMPONENT_LISTINGS}/${encodeURIComponent(listingId)}/update`, {
1862
+ method: "POST",
1863
+ body: JSON.stringify({ project, def, clientId: CLIENT_ID }),
1864
+ });
1865
+ }