@dailies-studio/shot-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -0,0 +1,373 @@
1
+ // THE CODE STUDIO (C2): one code component's source, beside a live render of
2
+ // what that source produces. It is the Components workspace's Edit tab for a
3
+ // code component; Preview is ComponentsPreview.tsx.
4
+ //
5
+ // It is `ComponentStudio`'s shape and for that file's reasons: the mounted
6
+ // `StagePanel` stays exactly where it is, and one element in the canvas cell is
7
+ // a HOLE the stage renders into. So the picture beside the editor is the real
8
+ // component under the real renderer, at its contract's own defaults, which is
9
+ // the only way "what I am editing" and "what will ship" can be the same thing.
10
+ //
11
+ // ── IN THE DOCKED CELLS ─────────────────────────────────────────────────
12
+ //
13
+ // LEFT the components list (ComponentLibraryRail), as on every tab
14
+ // CENTRE the CODE PANE (file bar, the open buffer, the 48 status footer),
15
+ // then the LIVE PANE (a 40 bar, then the hole), 420 wide
16
+ // RIGHT the component's actions and props (InspectorPanel's code branch)
17
+ // BAR the way back, the breadcrumb, the tab switch, Save and the zoom
18
+ //
19
+ // No transport under the centre and no timeline: the code pane carries its own
20
+ // footer, and the live pane loops on the component clock.
21
+ //
22
+ // ONE SUBTREE, PORTALLED. The buffer feeds both the editor in the canvas cell
23
+ // and the Save in the bar, and the back chevron and the Escape rung share one
24
+ // dirty confirm. The mode portals only while it IS the workspace
25
+ // (`workspaceOf`) on its Edit tab (`componentsTab`), so no two modes can ever
26
+ // fill one cell.
27
+ //
28
+ // ── THE SUBJECT AND THE BUFFER ──────────────────────────────────────────
29
+ //
30
+ // `codeComponent` is what the stage draws; `codeActive` is what the editor
31
+ // shows. The Files toggle in the file bar opens the project's files beside
32
+ // the editor, docked inside the code pane, and a file opened from it moves the
33
+ // buffer and never the subject: reading a helper is not a request to stop
34
+ // looking at the component it feeds.
35
+ //
36
+ import { useCallback, useEffect, useRef, useState } from "react";
37
+ import { useConfirm } from "@dailies-studio/ui";
38
+ import { framesToSeconds } from "@dailies-studio/engine";
39
+ import { CHEVRON_LEFT_ICON } from "../../icons.js";
40
+ import { instanceExportOf } from "@dailies-studio/shot/doc";
41
+ import { useShotEditor, useStore } from "../../state/context.js";
42
+ import { isEditable } from "../../state/keymap.js";
43
+ import { shortcutHint } from "../../state/shortcuts.js";
44
+ import { activeCodeFile } from "../../state/types.js";
45
+ import type { ComponentBuild } from "../../state/types.js";
46
+ import { workspaceOf } from "../../state/workspace.js";
47
+ import { CODE_LIVE_WIDTH, CODE_PANE_MIN } from "../../state/panelSize.js";
48
+ import { ResizeHandle } from "../ResizeHandle.js";
49
+ import { InCell } from "../stage/cellSlots.js";
50
+ import { formatDuration } from "../timeline/model.js";
51
+ import { dirtyCodeFileCount, discardBuffersMessage } from "./codeBufferGuard.js";
52
+ import { CodeFileStrips, CodePane, CodeTabs, TabWriter, useCodeDiagnostics } from "./codeParts.js";
53
+ import { FileTree } from "./FileTree.js";
54
+ import { ComponentLibraryRail } from "../studio/ComponentLibraryRail.js";
55
+ import { ComponentsTabSwitch, StudioZoom } from "../studio/ComponentStudio.js";
56
+ import { ComponentCrumb, ComponentsBack, useLoopLength, useNewPropKeys } from "../studio/componentParts.js";
57
+
58
+ export function CodeStudio() {
59
+ const store = useStore();
60
+ // THIS MODE'S CELLS, and only while it is the workspace on screen, on the
61
+ // Edit tab of Components.
62
+ const open = useShotEditor(
63
+ (s) => workspaceOf(s) === "components" && s.codeComponent !== null && s.componentsTab === "edit",
64
+ );
65
+ const componentId = useShotEditor((s) => s.codeComponent);
66
+ const doc = useShotEditor((s) => s.doc);
67
+ const file = useShotEditor(activeCodeFile);
68
+ const library = useShotEditor((s) => s.library);
69
+ const readOnly = useShotEditor((s) => s.readOnly);
70
+ // ACROSS EVERY BUFFER, not the one on screen. Leaving drops them all, so the
71
+ // question has to be asked about all of them.
72
+ const dirtyCount = useShotEditor((s) => dirtyCodeFileCount(s.codeFiles));
73
+ const build = useShotEditor((s) => (s.codeComponent === null ? undefined : s.componentBuilds[s.codeComponent]));
74
+ const propKeys = useShotEditor((s) => (s.codeComponent === null ? null : s.contracts[s.codeComponent]?.keys));
75
+ const fresh = useNewPropKeys(propKeys === null || propKeys === undefined ? [] : Object.keys(propKeys));
76
+ const { frames, fps } = useLoopLength();
77
+ // The seam between the code and the live pane measures both at the press.
78
+ const studioRef = useRef<HTMLElement | null>(null);
79
+ const liveRef = useRef<HTMLDivElement | null>(null);
80
+ const liveCeiling = (): number | undefined => {
81
+ const width = studioRef.current?.getBoundingClientRect().width ?? 0;
82
+ return width > 0 ? width - CODE_PANE_MIN : undefined;
83
+ };
84
+ const bufferCount = useShotEditor((s) => s.codeOrder.length);
85
+ const [filesOpen, setFilesOpen] = useState(false);
86
+ const { confirm, dialog } = useConfirm();
87
+
88
+ const dirty = file !== null && file.content !== file.savedContent;
89
+
90
+ // LEAVING IS THE ONE PLACE THAT CAN LOSE WORK, so it is the one place that
91
+ // asks. The store's verb never blocks (a store cannot open a dialog), which
92
+ // is why the confirm lives here and every door (Back to Shot, the chevron
93
+ // to All components, Escape) comes through it.
94
+ const guard = useCallback(
95
+ (then: () => void) => {
96
+ if (dirtyCount === 0) {
97
+ then();
98
+ return;
99
+ }
100
+ void confirm({
101
+ title: "Unsaved changes",
102
+ message: discardBuffersMessage(dirtyCount),
103
+ confirmLabel: "Discard",
104
+ danger: true,
105
+ }).then((ok) => {
106
+ if (ok) then();
107
+ });
108
+ },
109
+ [confirm, dirtyCount],
110
+ );
111
+ // ESCAPE: back to the Shot a Shot door opened this from, else out.
112
+ const leave = useCallback(
113
+ () =>
114
+ guard(() =>
115
+ store.getState().componentsOrigin !== null ? void store.backToShot() : store.closeCodeComponent(),
116
+ ),
117
+ [guard, store],
118
+ );
119
+ const fromShot = useShotEditor((s) => s.componentsOrigin !== null);
120
+
121
+ // AND AN ESCAPE THE PANEL DID NOT HEAR: one pressed with the stage iframe
122
+ // focused reaches the store's keymap, which asks here rather than closing.
123
+ // Answered once per request, because `leave` changes with the dirty count.
124
+ // A request made while this tab is not up is ComponentsPreview's, and is
125
+ // marked seen here, so swapping to Edit never answers it a second time.
126
+ const backRequest = useShotEditor((s) => s.codeBackRequest);
127
+ const answered = useRef<object | null>(null);
128
+ useEffect(() => {
129
+ if (backRequest === answered.current) return;
130
+ answered.current = backRequest;
131
+ if (!open || backRequest === null) return;
132
+ leave();
133
+ }, [open, backRequest, leave]);
134
+
135
+ // ESCAPE COMES HERE while this mode is open, so a dirty buffer gets the same
136
+ // question the back chevron asks.
137
+ //
138
+ // ON THE DOCUMENT, IN THE BUBBLE PHASE: after everything on screen that owns
139
+ // an Escape of its own, and before the keymap.
140
+ // - A menu, a popover and a live drag take Escape at window capture and
141
+ // stop it, so it never arrives here.
142
+ // - A Dialog stops it in React. CodeMirror marks one it used (closing its
143
+ // search panel, collapsing a selection) with preventDefault.
144
+ // - A field outside the editor (the Project title, a default, the Agent
145
+ // draft) keeps its Escape to revert or cancel.
146
+ // - The keymap listens on the window, bubble phase, so it runs after this,
147
+ // and every other Escape stops here.
148
+ useEffect(() => {
149
+ if (!open) return;
150
+ const onKey = (e: KeyboardEvent): void => {
151
+ if (e.key !== "Escape") return;
152
+ const target = e.target instanceof Element ? e.target : null;
153
+ if (target !== null && isEditable(target) && target.closest(".code-studio-pane") === null) return;
154
+ e.stopPropagation();
155
+ if (e.defaultPrevented || target?.closest(".dialog") != null) return;
156
+ e.preventDefault();
157
+ leave();
158
+ };
159
+ document.addEventListener("keydown", onKey);
160
+ return () => document.removeEventListener("keydown", onKey);
161
+ }, [open, leave]);
162
+
163
+ // THE TYPECHECK of the active buffer (`useCodeDiagnostics`), while this mode
164
+ // is on screen.
165
+ useCodeDiagnostics(open ? (file?.rel ?? null) : null, file?.content ?? null);
166
+
167
+ if (!open || componentId === null) return null;
168
+
169
+ const row = library.find((r) => r.id === componentId);
170
+ const name = row?.name ?? instanceExportOf(componentId);
171
+ const errors = file === null ? 0 : file.diagnostics.filter((d) => d.category === "error").length;
172
+ const tabRel = file?.rel ?? row?.rel ?? null;
173
+
174
+ return (
175
+ <>
176
+ {/* The confirm portals to <body> itself (house Dialog), so it lands in
177
+ no cell and paints over all of them. */}
178
+ {dialog}
179
+
180
+ <InCell name="bar">
181
+ {fromShot ? (
182
+ <ComponentsBack testId="code-back" guard={guard} />
183
+ ) : (
184
+ <button
185
+ type="button"
186
+ className="code-back"
187
+ data-testid="code-back"
188
+ title="All components"
189
+ aria-label="All components"
190
+ onClick={() => guard(() => void store.openComponents())}
191
+ >
192
+ {CHEVRON_LEFT_ICON}
193
+ </button>
194
+ )}
195
+ {/* THE SUBJECT, which is what the stage is drawing, NOT the buffer.
196
+ The file tab in the code pane says the other. */}
197
+ <ComponentCrumb
198
+ sel={{ kind: "code", module: componentId }}
199
+ name={
200
+ <span className="code-studio-name" title={name}>
201
+ {name}
202
+ </span>
203
+ }
204
+ />
205
+ {dirtyCount > 0 ? (
206
+ <span
207
+ className="code-studio-dirty"
208
+ data-testid="code-dirty"
209
+ title={dirtyCount === 1 ? "One file has unsaved changes" : `${dirtyCount} files have unsaved changes`}
210
+ />
211
+ ) : null}
212
+ <div className="component-studio-tools">
213
+ <ComponentsTabSwitch />
214
+ {/* SAVING IS EXPLICIT. Every save rebuilds, re-extracts the contract
215
+ and records a version, so a keystroke-triggered one would show the
216
+ author a stream of builds of half-typed code. */}
217
+ <button
218
+ type="button"
219
+ className="btn is-primary"
220
+ data-testid="code-save"
221
+ disabled={file === null || !dirty || file.status === "saving" || file.generated !== null}
222
+ title={file !== null && file.generated !== null ? "Generated files are read-only" : `Save${shortcutHint("file.save")}`}
223
+ onClick={() => void store.saveCodeFile()}
224
+ >
225
+ {file?.status === "saving" ? "Saving…" : "Save"}
226
+ </button>
227
+ <StudioZoom />
228
+ </div>
229
+ </InCell>
230
+
231
+ {doc === null ? null : (
232
+ <InCell name="left">
233
+ <ComponentLibraryRail doc={doc} current={{ kind: "code", module: componentId }} />
234
+ </InCell>
235
+ )}
236
+
237
+ <InCell name="centre">
238
+ <section className="code-studio" data-testid="code-studio" aria-label={`Code ${name}`} ref={studioRef}>
239
+ <div className="code-studio-column">
240
+ <div className="code-file-bar" data-testid="code-file-bar">
241
+ <button
242
+ type="button"
243
+ className={`code-files-toggle${filesOpen ? " is-on" : ""}`}
244
+ data-testid="code-files-toggle"
245
+ aria-pressed={filesOpen}
246
+ title={filesOpen ? "Hide the project's files" : "Show the project's files"}
247
+ onClick={() => setFilesOpen((v) => !v)}
248
+ >
249
+ Files
250
+ </button>
251
+ <CodeTabs />
252
+ {/* One buffer: the file tab alone. Two or more: the tab strip. */}
253
+ {tabRel !== null && bufferCount < 2 ? (
254
+ <span className="code-file-tab" data-testid="code-path" title={tabRel}>
255
+ {tabRel.slice(tabRel.lastIndexOf("/") + 1)}
256
+ <TabWriter rel={tabRel} />
257
+ </span>
258
+ ) : null}
259
+ <span className="code-file-spacer" />
260
+ <BuildStatus build={build} />
261
+ </div>
262
+ {/* The strips are FILE facts, so they head the column with the
263
+ file in it. */}
264
+ <CodeFileStrips file={file} />
265
+ <div className="code-studio-body">
266
+ {filesOpen ? <FileTree /> : null}
267
+ {file === null ? (
268
+ <p className="code-studio-empty" data-testid="code-no-buffer">
269
+ Pick a file to read it here.
270
+ </p>
271
+ ) : (
272
+ <CodePane rel={file.rel} />
273
+ )}
274
+ </div>
275
+ <div className="code-status" data-testid="code-status">
276
+ {errors > 0 ? (
277
+ <span className="code-status-errors" data-testid="code-errors">
278
+ {errors} {errors === 1 ? "error" : "errors"}
279
+ </span>
280
+ ) : (
281
+ <span className="code-status-ok" data-testid="code-problems">
282
+ No problems
283
+ </span>
284
+ )}
285
+ {fresh.length > 0 ? (
286
+ <span className="code-status-note" data-testid="code-new-prop">
287
+ · New prop found: {fresh.join(", ")}
288
+ </span>
289
+ ) : null}
290
+ <span className="code-file-spacer" />
291
+ <span className="code-status-verbs">
292
+ <button
293
+ type="button"
294
+ className="code-status-verb"
295
+ data-testid="code-remix"
296
+ disabled={readOnly || row === undefined || row.builtin === true}
297
+ title="Make your own copy of this component. The original stays as it was."
298
+ onClick={() => void store.remixComponent()}
299
+ >
300
+ Remix
301
+ </button>
302
+ <span aria-hidden="true">·</span>
303
+ <button
304
+ type="button"
305
+ className="code-status-verb"
306
+ data-testid="code-history"
307
+ title="Every built version of this component"
308
+ onClick={() => void store.openComponentHistory()}
309
+ >
310
+ History
311
+ </button>
312
+ </span>
313
+ </div>
314
+ </div>
315
+ <div className="code-live" data-testid="code-live" ref={liveRef}>
316
+ {/* ON THE SEAM, inside the pane it sizes. The live pane is on the
317
+ RIGHT, so moving the pointer along +x shrinks it. */}
318
+ <ResizeHandle
319
+ axis="x"
320
+ getValue={() => liveRef.current?.getBoundingClientRect().width || store.getState().codeLiveWidth}
321
+ sign={-1}
322
+ onChange={(px) => store.setCodeLiveWidth(px, liveCeiling())}
323
+ onReset={() => store.setCodeLiveWidth(CODE_LIVE_WIDTH.default)}
324
+ className="code-live-resize"
325
+ label="Resize the live preview"
326
+ />
327
+ <div className="code-live-bar">
328
+ <span className="code-live-title">Live</span>
329
+ <span className="code-live-loop" data-testid="code-live-loop">
330
+ loop {formatDuration(framesToSeconds(frames, fps))}
331
+ </span>
332
+ </div>
333
+ {/* THE COMPONENT ITSELF, and it is a HOLE rather than a component.
334
+ The stage behind this mode renders the registry component
335
+ standalone through a synthetic definition, and `measureInset`
336
+ hands it this element's rect. `aria-hidden` because a screen
337
+ reader has nothing to read in a GL surface this document does
338
+ not own. */}
339
+ <div className="stage-window code-studio-hole" aria-hidden="true" data-testid="code-preview-hole" />
340
+ </div>
341
+ </section>
342
+ </InCell>
343
+ </>
344
+ );
345
+ }
346
+
347
+ /** The file bar's build line: a passing build names its version, a failing
348
+ * one says the last good build is still the one playing. */
349
+ function BuildStatus(props: { build: ComponentBuild | undefined }) {
350
+ const b = props.build;
351
+ if (b === undefined) return null;
352
+ if (b.status === "building") {
353
+ return (
354
+ <span className="code-build is-building" data-testid="code-build-status">
355
+ Building…
356
+ </span>
357
+ );
358
+ }
359
+ if (b.status === "failed") {
360
+ return (
361
+ <span className="code-build is-failed" data-testid="code-build-status" title={b.message}>
362
+ <span className="code-build-dot" aria-hidden="true" />
363
+ {b.version === undefined ? "Build failed" : `Build failed · v${b.version} still plays`}
364
+ </span>
365
+ );
366
+ }
367
+ return (
368
+ <span className="code-build is-built" data-testid="code-build-status">
369
+ <span className="code-build-dot" aria-hidden="true" />
370
+ {b.version === undefined ? "Built" : `Built v${b.version}`}
371
+ </span>
372
+ );
373
+ }
@@ -0,0 +1,265 @@
1
+ // THE PROJECT'S FILES, as a tree, docked beside the editor in Code Studio's
2
+ // code pane while the file bar's Files toggle is on (CodeStudio.tsx). A head
3
+ // at the bar height, then the list, which is its one scroller: it wears
4
+ // `.rail-scroll`, the left column's scroller recipe, so it is bounded and
5
+ // themed like the outline.
6
+ //
7
+ // ── WHY THE TREE IS DERIVED AND NEVER STORED ────────────────────────────────
8
+ //
9
+ // `GET /api/files` answers a FLAT list of posix paths, and that is the honest
10
+ // shape: the project's directories are an artifact of where the author put
11
+ // things, not a structure the studio owns. Nesting is a way of DRAWING that
12
+ // list. Deriving it per render means a listing that arrives, changes, or
13
+ // truncates can never disagree with the tree on screen, and there is no second
14
+ // copy to invalidate when `files-changed` lands.
15
+ //
16
+ // The cost is one O(n) build per listing change, on at most `LIST_CAP` = 2000
17
+ // entries. That is nothing next to the class of bug it removes.
18
+ //
19
+ // ponytail: every expanded row is a real DOM node, no virtualisation. Fine to
20
+ // the server's own 2000-file cap; if that cap ever rises, this is the thing to
21
+ // window before anything else here.
22
+ //
23
+ // ── WHAT A ROW KNOWS ────────────────────────────────────────────────────────
24
+ //
25
+ // A file row carries three marks and no more: OPEN (it has a buffer), DIRTY
26
+ // (that buffer is unsaved) and GENERATED (the server will refuse to write it).
27
+ // Everything else an author might want here — which component is in it, whether
28
+ // it drifted — already has a home in the library section of the file panel, and
29
+ // duplicating it would make two places to disagree about one fact.
30
+ import { useMemo, useState } from "react";
31
+ import type { CSSProperties } from "react";
32
+ import type { ProjectFile } from "../../api.js";
33
+ import { useShotEditor, useStore } from "../../state/context.js";
34
+
35
+ /** A directory that starts collapsed. `node_modules` and dotfiles never reach
36
+ * the chrome — the server drops them — so this is only about the folders that
37
+ * are genuinely output rather than source. */
38
+ const COLLAPSED_BY_DEFAULT = new Set(["out", "dist", "build", "tmp", "coverage"]);
39
+
40
+ interface DirNode {
41
+ kind: "dir";
42
+ name: string;
43
+ path: string;
44
+ children: TreeNode[];
45
+ }
46
+
47
+ interface FileNode {
48
+ kind: "file";
49
+ name: string;
50
+ path: string;
51
+ generated: boolean;
52
+ }
53
+
54
+ type TreeNode = DirNode | FileNode;
55
+
56
+ /**
57
+ * Fold the flat listing into directories.
58
+ *
59
+ * Directories sort before files and both sort by name, which is the order every
60
+ * file browser has and therefore the only order that does not have to be
61
+ * learned. The server already sorts its walk, but it interleaves — a flat list
62
+ * cannot express "folders first" — so the comparison is redone here.
63
+ */
64
+ function buildTree(files: readonly ProjectFile[]): TreeNode[] {
65
+ const root: DirNode = { kind: "dir", name: "", path: "", children: [] };
66
+ const dirs = new Map<string, DirNode>([["", root]]);
67
+
68
+ const dirAt = (path: string): DirNode => {
69
+ const found = dirs.get(path);
70
+ if (found !== undefined) return found;
71
+ const cut = path.lastIndexOf("/");
72
+ const parent = dirAt(cut === -1 ? "" : path.slice(0, cut));
73
+ const node: DirNode = {
74
+ kind: "dir",
75
+ name: path.slice(cut + 1),
76
+ path,
77
+ children: [],
78
+ };
79
+ dirs.set(path, node);
80
+ parent.children.push(node);
81
+ return node;
82
+ };
83
+
84
+ for (const file of files) {
85
+ const cut = file.path.lastIndexOf("/");
86
+ const parent = dirAt(cut === -1 ? "" : file.path.slice(0, cut));
87
+ parent.children.push({
88
+ kind: "file",
89
+ name: file.path.slice(cut + 1),
90
+ path: file.path,
91
+ generated: file.generated !== undefined,
92
+ });
93
+ }
94
+
95
+ const sort = (node: DirNode): void => {
96
+ node.children.sort((a, b) => {
97
+ if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
98
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
99
+ });
100
+ for (const child of node.children) if (child.kind === "dir") sort(child);
101
+ };
102
+ sort(root);
103
+ return root.children;
104
+ }
105
+
106
+ export function FileTree() {
107
+ const store = useStore();
108
+ const files = useShotEditor((s) => s.projectFiles);
109
+ const loaded = useShotEditor((s) => s.projectFilesLoaded);
110
+ const truncated = useShotEditor((s) => s.projectFilesTruncated);
111
+ const error = useShotEditor((s) => s.projectFilesError);
112
+ const tree = useMemo(() => buildTree(files), [files]);
113
+
114
+ // COLLAPSE STATE IS THIS PANEL'S, not the store's: it is a posture, it means
115
+ // nothing to any other surface, and it dies with the mode the way a scroll
116
+ // position does.
117
+ const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(new Set());
118
+ const toggle = (path: string): void => {
119
+ setCollapsed((was) => {
120
+ const next = new Set(was);
121
+ if (!next.delete(path)) next.add(path);
122
+ return next;
123
+ });
124
+ };
125
+
126
+ return (
127
+ <div className="code-tree" data-testid="code-tree">
128
+ <div className="code-tree-head">
129
+ <span className="code-tree-title">Files</span>
130
+ <button
131
+ type="button"
132
+ className="btn is-icon"
133
+ data-testid="code-tree-refresh"
134
+ title="Re-read the project's files"
135
+ aria-label="Refresh the file list"
136
+ onClick={() => void store.refreshProjectFiles()}
137
+ >
138
+ ⟳
139
+ </button>
140
+ </div>
141
+ {error !== null ? (
142
+ <p className="code-tree-empty" role="alert" data-testid="code-tree-error">
143
+ {error}
144
+ </p>
145
+ ) : !loaded ? (
146
+ <p className="code-tree-empty">Reading the project…</p>
147
+ ) : tree.length === 0 ? (
148
+ <p className="code-tree-empty">No editable files in this project.</p>
149
+ ) : (
150
+ <ul className="code-tree-list rail-scroll" role="tree" aria-label="Project files">
151
+ {tree.map((node) => (
152
+ <TreeRow
153
+ key={node.path}
154
+ node={node}
155
+ depth={0}
156
+ collapsed={collapsed}
157
+ onToggle={toggle}
158
+ />
159
+ ))}
160
+ </ul>
161
+ )}
162
+ {/* SAID, NEVER SWALLOWED. A file browser that quietly omits files teaches
163
+ the author their file does not exist. */}
164
+ {truncated ? (
165
+ <p className="code-tree-truncated" data-testid="code-tree-truncated">
166
+ This project has more files than the listing returns. Some are not shown.
167
+ </p>
168
+ ) : null}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ function TreeRow({
174
+ node,
175
+ depth,
176
+ collapsed,
177
+ onToggle,
178
+ }: {
179
+ node: TreeNode;
180
+ depth: number;
181
+ collapsed: ReadonlySet<string>;
182
+ onToggle: (path: string) => void;
183
+ }) {
184
+ if (node.kind === "dir") {
185
+ // XOR AGAINST THE DEFAULT, rather than seeding the set: the set is built
186
+ // before any listing has arrived, so there is nothing to seed it from. The
187
+ // set therefore holds "the author has toggled this", and a name in the
188
+ // collapsed-by-default list is open exactly when it has been toggled.
189
+ // Keyed by full path, so two directories that share a name are independent.
190
+ const defaultCollapsed = COLLAPSED_BY_DEFAULT.has(node.name);
191
+ const isCollapsed = collapsed.has(node.path) !== defaultCollapsed;
192
+ return (
193
+ <li role="none">
194
+ <button
195
+ type="button"
196
+ className="code-tree-row is-dir"
197
+ role="treeitem"
198
+ aria-expanded={!isCollapsed}
199
+ style={{ "--depth": depth } as CSSProperties}
200
+ onClick={() => onToggle(node.path)}
201
+ >
202
+ <span className="code-tree-caret" aria-hidden="true">
203
+ {isCollapsed ? "▸" : "▾"}
204
+ </span>
205
+ <span className="code-tree-name">{node.name}</span>
206
+ </button>
207
+ {isCollapsed ? null : (
208
+ <ul className="code-tree-list" role="group">
209
+ {node.children.map((child) => (
210
+ <TreeRow
211
+ key={child.path}
212
+ node={child}
213
+ depth={depth + 1}
214
+ collapsed={collapsed}
215
+ onToggle={onToggle}
216
+ />
217
+ ))}
218
+ </ul>
219
+ )}
220
+ </li>
221
+ );
222
+ }
223
+ return <FileRow node={node} depth={depth} />;
224
+ }
225
+
226
+ function FileRow({ node, depth }: { node: FileNode; depth: number }) {
227
+ const store = useStore();
228
+ const active = useShotEditor((s) => s.codeActive === node.path);
229
+ const buffer = useShotEditor((s) => s.codeFiles[node.path]);
230
+ const isSubject = useShotEditor(
231
+ (s) => s.library.find((r) => r.id === s.codeComponent)?.rel === node.path,
232
+ );
233
+ const dirty = buffer !== undefined && buffer.content !== buffer.savedContent;
234
+ return (
235
+ <li role="none">
236
+ <button
237
+ type="button"
238
+ className="code-tree-row is-file"
239
+ role="treeitem"
240
+ aria-selected={active}
241
+ data-active={active ? "" : undefined}
242
+ data-open={buffer !== undefined ? "" : undefined}
243
+ data-testid={`code-tree-file-${node.path}`}
244
+ title={node.generated ? `${node.path}: generated, read-only` : node.path}
245
+ style={{ "--depth": depth } as CSSProperties}
246
+ onClick={() => void store.openCodeFile(node.path)}
247
+ >
248
+ <span className="code-tree-name">{node.name}</span>
249
+ {/* THE SUBJECT MARK, so the one file the stage is drawing is findable
250
+ in a tree of two hundred. */}
251
+ {isSubject ? (
252
+ <span className="code-tree-mark is-subject" title="The component in the preview">
253
+ ●
254
+ </span>
255
+ ) : null}
256
+ {node.generated ? (
257
+ <span className="code-tree-mark is-generated" title="Generated, read-only">
258
+ ⌁
259
+ </span>
260
+ ) : null}
261
+ {dirty ? <span className="code-tree-mark is-dirty" title="Unsaved changes" /> : null}
262
+ </button>
263
+ </li>
264
+ );
265
+ }