@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,52 @@
1
+ // The question asked before Code Studio's open buffers are dropped.
2
+ //
3
+ // ITS OWN FILE, WITH NO EDITOR IN IT. The header, the Components rail, home
4
+ // and preview all ask it, and they load with the shell; `codeParts.tsx`
5
+ // carries the CodeMirror editor, which loads only when code is opened. Kept
6
+ // together, every visit paid for CodeMirror to ask one question.
7
+ import { useCallback, type ReactNode } from "react";
8
+ import { useConfirm } from "@dailies-studio/ui";
9
+ import { useStore } from "../../state/context.js";
10
+ import type { CodeFileState } from "../../state/types.js";
11
+
12
+ /** How many open buffers hold text that is not on disk. */
13
+ export function dirtyCodeFileCount(files: Readonly<Record<string, CodeFileState>>): number {
14
+ return Object.values(files).filter((f) => f.content !== f.savedContent).length;
15
+ }
16
+
17
+ /** The one question asked before open buffers are dropped. */
18
+ export function discardBuffersMessage(count: number): string {
19
+ return count === 1
20
+ ? "One open file has edits that have not been written to disk. Close it anyway?"
21
+ : `${count} open files have edits that have not been written to disk. Close them anyway?`;
22
+ }
23
+
24
+ /**
25
+ * Leaving the code component in Components, which drops the buffers. With
26
+ * nothing dirty `guard` just runs `then`; otherwise it asks first. `then` is
27
+ * the verb that drops them (a leave, or a definition picked in its place).
28
+ */
29
+ export function useCodeBufferGuard(): { guard: (then: () => void) => void; dialog: ReactNode } {
30
+ const store = useStore();
31
+ const { confirm, dialog } = useConfirm();
32
+ const guard = useCallback(
33
+ (then: () => void): void => {
34
+ const s = store.getState();
35
+ const count = s.codeComponent === null ? 0 : dirtyCodeFileCount(s.codeFiles);
36
+ if (count === 0) {
37
+ then();
38
+ return;
39
+ }
40
+ void confirm({
41
+ title: "Unsaved changes",
42
+ message: discardBuffersMessage(count),
43
+ confirmLabel: "Discard",
44
+ danger: true,
45
+ }).then((ok) => {
46
+ if (ok) then();
47
+ });
48
+ },
49
+ [confirm, store],
50
+ );
51
+ return { guard, dialog };
52
+ }
@@ -0,0 +1,321 @@
1
+ // The parts of Code Studio's editor: the editor itself, the tab strip, the
2
+ // file strips above it and the typecheck under it.
3
+ //
4
+ // ONE BUFFER PER FILE, ONE 409 PATH. Every file edits the store buffers
5
+ // (`codeFiles`), saves through `saveCodeFile` and meets a write made elsewhere
6
+ // (a second tab, an agent through MCP, an editor outside the studio) through
7
+ // the same conflict strip.
8
+ import { useCallback, useEffect, useRef } from "react";
9
+ import { useConfirm } from "@dailies-studio/ui";
10
+ import { CodeEditor } from "@dailies-studio/ui/code";
11
+ import * as api from "../../api.js";
12
+ import { useStore, useShotEditor } from "../../state/context.js";
13
+ import type { CodeFileState } from "../../state/types.js";
14
+ import { GeneratedStrip } from "./CodeDefaultsPanel.js";
15
+ import { agentWriting } from "../../state/agentActivity.js";
16
+ import { AgentDots, useAgentActivity } from "../agent/AgentDots.js";
17
+
18
+ /** The MCP agent writing `rel` now, or null (its dots go on the file's tab). */
19
+ export function useAgentWriter(rel: string | null): string | null {
20
+ const activity = useAgentActivity();
21
+ return rel === null ? null : (agentWriting(activity, rel, null)?.name ?? null);
22
+ }
23
+
24
+ /** Follow mode is on for `rel` and an agent is still writing it: the buffer
25
+ * is the agent's until the author takes over. */
26
+ export function useFollowing(rel: string | null): string | null {
27
+ const writer = useAgentWriter(rel);
28
+ const follow = useShotEditor((s) => s.codeFollow);
29
+ return rel !== null && follow === rel ? writer : null;
30
+ }
31
+
32
+ /** The dots an agent's file tab shows. */
33
+ export function TabWriter({ rel }: { rel: string }) {
34
+ const writer = useAgentWriter(rel);
35
+ return writer === null ? null : <AgentDots label={`${writer} is writing this file`} />;
36
+ }
37
+
38
+ /**
39
+ * FOLLOW MODE'S STRIP: the agent is writing this file, so the buffer shows
40
+ * each write as it lands and takes no typing. "Take over" makes it the
41
+ * author's again; a later agent write is then an ordinary conflict.
42
+ */
43
+ export function FollowStrip({ rel }: { rel: string }) {
44
+ const store = useStore();
45
+ const writer = useFollowing(rel);
46
+ if (writer === null) return null;
47
+ return (
48
+ <div className="code-studio-strip is-follow" role="status" data-testid="code-follow">
49
+ <AgentDots label={`${writer} is writing this file`} />
50
+ <span>{writer} is writing this file. It updates as each write lands.</span>
51
+ <button
52
+ type="button"
53
+ className="btn"
54
+ data-testid="code-take-over"
55
+ title="Stop following and edit the file yourself"
56
+ onClick={() => store.takeOverCodeFile()}
57
+ >
58
+ Take over
59
+ </button>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** How long the buffer sits still before it is typechecked. Long enough that
65
+ * ordinary typing costs no round trips, short enough that a pause gets an
66
+ * answer: the motion chrome's drawer has run this number for as long as it
67
+ * has had a lint gutter. */
68
+ const DIAG_DEBOUNCE_MS = 700;
69
+
70
+ /**
71
+ * The typecheck, debounced, against the BUFFER rather than against disk,
72
+ * which is the whole point of `/api/diagnostics` taking content: the red
73
+ * gutter has to describe the code the author is looking at, not the code they
74
+ * last saved. Responses for text that has since changed are dropped.
75
+ *
76
+ * THE ACTIVE BUFFER ONLY. Typechecking every open tab on every keystroke
77
+ * would cost a round trip per tab for gutters nobody is looking at; a tab
78
+ * re-checks when it becomes active, because `rel` is a dependency here.
79
+ */
80
+ export function useCodeDiagnostics(rel: string | null, content: string | null): void {
81
+ const store = useStore();
82
+ useEffect(() => {
83
+ if (rel === null || content === null) return;
84
+ const timer = setTimeout(() => {
85
+ void api
86
+ .fileDiagnostics(rel, content)
87
+ .then((res) => {
88
+ // The buffer this describes, addressed by rel: the author can have
89
+ // switched tabs while this was in flight, and painting these into
90
+ // whatever is on screen now would mark the wrong file's lines.
91
+ const now = store.getState().codeFiles[rel];
92
+ if (now === undefined || now.content !== content) return;
93
+ store.setCodeDiagnostics(rel, res.diagnostics);
94
+ })
95
+ // Silent: a typecheck that will not answer is not a reason to interrupt
96
+ // someone who is typing, and the next keystroke asks again.
97
+ .catch(() => {});
98
+ }, DIAG_DEBOUNCE_MS);
99
+ return () => clearTimeout(timer);
100
+ }, [rel, content, store]);
101
+ }
102
+
103
+ /**
104
+ * The file strips: what is true of the active buffer that the editor alone
105
+ * cannot say. Generated (writes refused), changed on disk (the 409, asked
106
+ * about), and a save that failed.
107
+ */
108
+ export function CodeFileStrips({ file }: { file: CodeFileState | null }) {
109
+ if (file === null) return null;
110
+ return (
111
+ <>
112
+ <FollowStrip rel={file.rel} />
113
+ {file.generated !== null ? (
114
+ <GeneratedStrip
115
+ docName={file.generated.docName}
116
+ className="code-studio-strip is-generated"
117
+ testId="code-generated"
118
+ />
119
+ ) : null}
120
+ {file.status === "conflict" ? <ConflictStrip rel={file.rel} /> : null}
121
+ {file.status === "error" ? (
122
+ <div className="code-studio-strip is-error" role="alert" data-testid="code-error">
123
+ {file.error}
124
+ </div>
125
+ ) : null}
126
+ </>
127
+ );
128
+ }
129
+
130
+ /**
131
+ * The editor.
132
+ *
133
+ * SPLIT OUT so the CodeMirror host does not re-mount on every unrelated state
134
+ * change in the surface above it, and so the Ctrl+S wall has one owner. The
135
+ * keystroke is stopped here rather than filtered in the keymap: the chrome's
136
+ * own ⌘S saves the DOCUMENT, and a save inside a text editor means the text.
137
+ */
138
+ export function CodePane({ rel }: { rel: string }) {
139
+ const store = useStore();
140
+ // BOTH READ FROM THE SAME `rel`, so `path` and `content` always describe the
141
+ // same buffer in the same commit. `CodeEditor` builds a fresh state from
142
+ // whatever content it holds when a new path arrives, so a path that led its
143
+ // text by one render would seed the new tab with the old tab's file and then
144
+ // replace the whole document as one undoable edit.
145
+ const content = useShotEditor((s) => s.codeFiles[rel]?.content ?? "");
146
+ const diagnostics = useShotEditor((s) => s.codeFiles[rel]?.diagnostics);
147
+ const openPaths = useShotEditor((s) => s.codeOrder);
148
+ // THE VIEW REFUSES THE EDIT, not just the Save button. The strip above says
149
+ // this file's writes are refused; letting it accept a page of typing and then
150
+ // fail would tell the author that only afterwards. A viewer's Project
151
+ // refuses every file the same way.
152
+ const generatedOrViewer = useShotEditor((s) => s.readOnly || (s.codeFiles[rel]?.generated ?? null) !== null);
153
+ // FOLLOWING AN AGENT: its writes land in this buffer, so typing here would
154
+ // be thrown away by the next one. Take over (the strip) gives it back.
155
+ const following = useFollowing(rel);
156
+ const readOnly = generatedOrViewer || following !== null;
157
+ return (
158
+ <div
159
+ className="code-studio-pane"
160
+ onKeyDown={(e) => {
161
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ void store.saveCodeFile();
165
+ }
166
+ }}
167
+ >
168
+ <CodeEditor
169
+ path={rel}
170
+ content={content}
171
+ {...(diagnostics === undefined ? {} : { diagnostics })}
172
+ // THE WHOLE TAB SET, which is what makes the widget's per-path cache
173
+ // keep an undo history per tab and drop the ones the author closed.
174
+ openPaths={openPaths}
175
+ readOnly={readOnly}
176
+ onChange={(text) => store.updateCodeBuffer(text)}
177
+ />
178
+ </div>
179
+ );
180
+ }
181
+
182
+ /**
183
+ * The open buffers.
184
+ *
185
+ * ORDERED BY `codeOrder` AND NOT BY THE MAP, because an object's key order is a
186
+ * language detail and a tab strip's order is a promise: tabs that reshuffle when
187
+ * a buffer is replaced make the author re-find the one they were reading.
188
+ *
189
+ * Hidden at one tab, which is the state the studio opens in. A tab strip
190
+ * offering to close the only thing on screen is a control with nothing to do,
191
+ * and it costs a row of the editor's height to say so.
192
+ */
193
+ export function CodeTabs() {
194
+ const store = useStore();
195
+ const order = useShotEditor((s) => s.codeOrder);
196
+ const active = useShotEditor((s) => s.codeActive);
197
+ const { confirm, dialog } = useConfirm();
198
+
199
+ // CLOSING A DIRTY BUFFER IS THE OTHER PLACE THAT CAN LOSE WORK, so it is the
200
+ // other place that asks. `closeCodeFile` deletes the buffer outright and the
201
+ // text was never on disk: there is no undo behind it and no strip left to
202
+ // show it, which is exactly the case the whole file already refuses on save
203
+ // and on adopt. A clean buffer closes without a word: nothing is at stake.
204
+ const close = useCallback(
205
+ (rel: string): void => {
206
+ const buffer = store.getState().codeFiles[rel];
207
+ if (buffer === undefined) return;
208
+ if (buffer.content === buffer.savedContent) {
209
+ store.closeCodeFile(rel);
210
+ return;
211
+ }
212
+ void confirm({
213
+ title: "Unsaved changes",
214
+ message: `${rel} has edits that have not been written to disk. Close it anyway?`,
215
+ confirmLabel: "Discard",
216
+ danger: true,
217
+ }).then((ok) => {
218
+ if (ok) store.closeCodeFile(rel);
219
+ });
220
+ },
221
+ [confirm, store],
222
+ );
223
+
224
+ if (order.length < 2) return null;
225
+ return (
226
+ <div className="code-tabs" role="tablist" aria-label="Open files" data-testid="code-tabs">
227
+ {dialog}
228
+ {order.map((rel) => (
229
+ <CodeTab key={rel} rel={rel} active={rel === active} store={store} onClose={close} />
230
+ ))}
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function CodeTab({
236
+ rel,
237
+ active,
238
+ store,
239
+ onClose,
240
+ }: {
241
+ rel: string;
242
+ active: boolean;
243
+ store: ReturnType<typeof useStore>;
244
+ onClose: (rel: string) => void;
245
+ }) {
246
+ const buffer = useShotEditor((s) => s.codeFiles[rel]);
247
+ const dirty = buffer !== undefined && buffer.content !== buffer.savedContent;
248
+ const name = rel.slice(rel.lastIndexOf("/") + 1);
249
+ // THE STRIP HAS NO SCROLLBAR (code.css), so the tab that becomes active (a
250
+ // tree click, a new file) brings itself into view, close button and all.
251
+ // jsdom implements no `scrollIntoView`, hence the guard.
252
+ const ref = useRef<HTMLDivElement>(null);
253
+ useEffect(() => {
254
+ const el = ref.current;
255
+ if (active && el !== null && typeof el.scrollIntoView === "function") {
256
+ el.scrollIntoView({ block: "nearest", inline: "nearest" });
257
+ }
258
+ }, [active]);
259
+ return (
260
+ <div ref={ref} className="code-tab" data-active={active ? "" : undefined}>
261
+ <button
262
+ type="button"
263
+ className="code-tab-name"
264
+ role="tab"
265
+ aria-selected={active}
266
+ title={rel}
267
+ data-testid={`code-tab-${rel}`}
268
+ onClick={() => store.activateCodeFile(rel)}
269
+ >
270
+ {name}
271
+ <TabWriter rel={rel} />
272
+ {dirty ? <span className="code-tab-dirty" title="Unsaved changes" /> : null}
273
+ </button>
274
+ <button
275
+ type="button"
276
+ className="code-tab-close"
277
+ aria-label={`Close ${name}`}
278
+ title={`Close ${name}`}
279
+ data-testid={`code-tab-close-${rel}`}
280
+ onClick={() => onClose(rel)}
281
+ >
282
+ ×
283
+ </button>
284
+ </div>
285
+ );
286
+ }
287
+
288
+ /**
289
+ * The file changed on disk under the buffer.
290
+ *
291
+ * BOTH TEXTS ARE REAL WORK, which is why this is a question and not a recovery:
292
+ * the author's buffer is theirs, and whatever landed on disk was landed by
293
+ * something (a second tab, an agent through MCP, an editor outside the studio).
294
+ * Choosing for them loses one of the two.
295
+ */
296
+ export function ConflictStrip({ rel }: { rel: string }) {
297
+ const store = useStore();
298
+ return (
299
+ <div className="code-studio-strip is-conflict" role="alert" data-testid="code-conflict">
300
+ <span>This file changed on disk since you opened it.</span>
301
+ <button
302
+ type="button"
303
+ className="btn"
304
+ data-testid="code-reload"
305
+ title="Throw away this buffer and take what is on disk"
306
+ onClick={() => void store.reloadCodeFile(rel)}
307
+ >
308
+ Reload
309
+ </button>
310
+ <button
311
+ type="button"
312
+ className="btn"
313
+ data-testid="code-overwrite"
314
+ title="Write this buffer over what is on disk"
315
+ onClick={() => void store.saveCodeFile(true, rel)}
316
+ >
317
+ Overwrite
318
+ </button>
319
+ </div>
320
+ );
321
+ }
@@ -0,0 +1,239 @@
1
+ // THE COMPOSER (Paper page 20): a new pin's first message, or a reply.
2
+ //
3
+ // ONE DRAFT PER THREAD. The text lives in the comments store under the thread
4
+ // id (or `NEW_THREAD`), so closing a thread and coming back keeps what was
5
+ // typed. Ctrl or Cmd+Enter posts; plain Enter is a new line.
6
+ //
7
+ // THE TEXTAREA IS THE COMBOBOX. Typing `@` opens the mention menu under it;
8
+ // the arrows walk it, Enter or Tab pick, Escape closes it. Escape with the
9
+ // menu shut cancels: the draft pin, or the thread. Every key the composer
10
+ // answers stops here, so the board's keymap (Escape's own ladder) never sees
11
+ // it twice.
12
+ import { useEffect, useId, useRef, useState } from "react";
13
+ import type { KeyboardEvent, ReactNode } from "react";
14
+ import type { Mentionable } from "../../api.js";
15
+ import { useStore } from "../../state/context.js";
16
+ import {
17
+ COMMENT_BODY_MAX,
18
+ encodeMentions,
19
+ insertMention,
20
+ mentionQuery,
21
+ personLabel,
22
+ } from "../../state/comments.js";
23
+ import { MentionMenu, mentionOptions, optionId } from "./MentionMenu.js";
24
+ import type { MentionOption } from "./MentionMenu.js";
25
+ import { useComments } from "./parts.js";
26
+
27
+ /** The counter shows once this close to the limit. */
28
+ const COUNTER_FROM = COMMENT_BODY_MAX - 200;
29
+
30
+ export function CommentComposer(props: {
31
+ /** A thread id, or `NEW_THREAD` for the draft pin. */
32
+ threadKey: string;
33
+ variant: "new" | "reply";
34
+ onCancel: () => void;
35
+ /** The Frame as the author sees it, kept with a new thread. */
36
+ picture?: () => Promise<Blob | null>;
37
+ /** The new pin's "where", above the field. */
38
+ head?: ReactNode;
39
+ }) {
40
+ const store = useStore();
41
+ const { comments } = store;
42
+ const text = useComments().texts[props.threadKey] ?? "";
43
+ const field = useRef<HTMLTextAreaElement>(null);
44
+ const menuId = useId();
45
+ const [caret, setCaret] = useState(0);
46
+ const [picked, setPicked] = useState<{ id: string; label: string }[]>([]);
47
+ const [result, setResult] = useState<Mentionable | null>(null);
48
+ const [active, setActive] = useState(0);
49
+ /** The `@` the author dismissed with Escape: the menu stays shut for it. */
50
+ const [dismissed, setDismissed] = useState<number | null>(null);
51
+ const [refusal, setRefusal] = useState<string | null>(null);
52
+ const [posting, setPosting] = useState(false);
53
+
54
+ const query = mentionQuery(text, caret);
55
+ const menuOpen = query !== null && query.start !== dismissed && result !== null;
56
+ const options = result === null ? [] : mentionOptions(result);
57
+
58
+ useEffect(() => {
59
+ field.current?.focus();
60
+ }, []);
61
+
62
+ // The menu's people, asked again per query. The last answer wins.
63
+ const q = query === null || query.start === dismissed ? null : query.query;
64
+ useEffect(() => {
65
+ if (q === null) {
66
+ setResult(null);
67
+ return;
68
+ }
69
+ let live = true;
70
+ comments.mentionable(q).then(
71
+ (res) => {
72
+ if (!live) return;
73
+ setResult(res);
74
+ setActive(0);
75
+ },
76
+ () => {
77
+ if (live) setResult(null);
78
+ },
79
+ );
80
+ return () => {
81
+ live = false;
82
+ };
83
+ }, [comments, q]);
84
+
85
+ const write = (next: string, at: number): void => {
86
+ comments.setText(props.threadKey, next);
87
+ setCaret(at);
88
+ setRefusal(null);
89
+ };
90
+
91
+ const pick = (option: MentionOption): void => {
92
+ if (query === null) return;
93
+ const label = personLabel(option.person);
94
+ const next = insertMention(text, query.start, caret, label);
95
+ write(next.text, next.caret);
96
+ setPicked((list) => [...list.filter((p) => p.id !== option.person.id), { id: option.person.id, label }]);
97
+ // SHARE AND MENTION: the Project's access dialog, with this person
98
+ // picked as a Viewer, in the dashboard (granting access lives there). The
99
+ // chip goes in now; the post names them once the share is made.
100
+ if (option.share) {
101
+ const name = store.getState().name;
102
+ if (name !== null) {
103
+ window.open(`/projects/?share=${encodeURIComponent(`shot/${name}`)}&with=${encodeURIComponent(option.person.id)}`, "_blank");
104
+ }
105
+ }
106
+ requestAnimationFrame(() => field.current?.setSelectionRange(next.caret, next.caret));
107
+ };
108
+
109
+ const post = async (): Promise<void> => {
110
+ if (posting) return;
111
+ setPosting(true);
112
+ const said = await comments.post(props.threadKey, encodeMentions(text, picked), props.picture);
113
+ setPosting(false);
114
+ setRefusal(said);
115
+ if (said === null) setPicked([]);
116
+ };
117
+
118
+ const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
119
+ if (menuOpen && options.length > 0 && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
120
+ e.preventDefault();
121
+ const step = e.key === "ArrowDown" ? 1 : -1;
122
+ setActive((i) => (i + step + options.length) % options.length);
123
+ } else if (menuOpen && options.length > 0 && (e.key === "Enter" || e.key === "Tab") && !e.ctrlKey && !e.metaKey) {
124
+ e.preventDefault();
125
+ pick(options[Math.min(active, options.length - 1)]!);
126
+ } else if (e.key === "Escape") {
127
+ e.preventDefault();
128
+ if (menuOpen) setDismissed(query.start);
129
+ else props.onCancel();
130
+ } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
131
+ e.preventDefault();
132
+ void post();
133
+ } else {
134
+ return;
135
+ }
136
+ e.stopPropagation();
137
+ };
138
+
139
+ const over = text.trim().length > COMMENT_BODY_MAX;
140
+ const empty = text.trim() === "";
141
+ const counter =
142
+ text.length >= COUNTER_FROM ? (
143
+ <span className={`comment-counter${over ? " is-over" : ""}`} data-testid="comment-counter">
144
+ {text.length}/{COMMENT_BODY_MAX}
145
+ </span>
146
+ ) : null;
147
+ const atButton = (
148
+ <button
149
+ type="button"
150
+ className="comment-at"
151
+ aria-label="Mention someone"
152
+ title="Mention someone"
153
+ onPointerDown={(e) => e.preventDefault()}
154
+ onClick={() => {
155
+ const el = field.current;
156
+ const at = el?.selectionStart ?? text.length;
157
+ const lead = at > 0 && !/\s/.test(text[at - 1]!) ? " @" : "@";
158
+ write(text.slice(0, at) + lead + text.slice(at), at + lead.length);
159
+ setDismissed(null);
160
+ requestAnimationFrame(() => {
161
+ el?.focus();
162
+ el?.setSelectionRange(at + lead.length, at + lead.length);
163
+ });
164
+ }}
165
+ >
166
+ @
167
+ </button>
168
+ );
169
+ const postButton = (
170
+ <button
171
+ type="button"
172
+ className="comment-post"
173
+ data-testid="comment-post"
174
+ disabled={empty || over || posting}
175
+ onClick={() => void post()}
176
+ >
177
+ Post
178
+ </button>
179
+ );
180
+ const textarea = (
181
+ <textarea
182
+ ref={field}
183
+ className="comment-field"
184
+ data-testid="comment-field"
185
+ rows={props.variant === "new" ? 2 : 1}
186
+ value={text}
187
+ placeholder={props.variant === "new" ? "Add a comment, @ to mention" : "Reply, @ to mention"}
188
+ aria-label={props.variant === "new" ? "Comment" : "Reply"}
189
+ role="combobox"
190
+ aria-autocomplete="list"
191
+ aria-expanded={menuOpen}
192
+ aria-controls={menuId}
193
+ {...(menuOpen && options.length > 0 ? { "aria-activedescendant": optionId(menuId, active) } : {})}
194
+ onChange={(e) => write(e.target.value, e.target.selectionStart)}
195
+ onSelect={(e) => setCaret(e.currentTarget.selectionStart)}
196
+ onKeyDown={onKeyDown}
197
+ />
198
+ );
199
+ const menu = menuOpen ? (
200
+ <MentionMenu id={menuId} result={result} active={active} onPick={pick} onActive={setActive} />
201
+ ) : null;
202
+ const error =
203
+ refusal === null ? null : (
204
+ <p className="comment-refusal" role="alert">
205
+ {refusal}
206
+ </p>
207
+ );
208
+
209
+ if (props.variant === "reply") {
210
+ return (
211
+ <div className="comment-reply" data-testid="comment-composer">
212
+ {menu}
213
+ {error}
214
+ <div className="comment-reply-row">
215
+ {textarea}
216
+ {counter}
217
+ {atButton}
218
+ {postButton}
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+ return (
224
+ <div className="comment-composer" data-testid="comment-composer">
225
+ {props.head}
226
+ <div className="comment-composer-field">{textarea}</div>
227
+ {error}
228
+ <div className="comment-composer-foot">
229
+ <div className="comment-composer-hints">
230
+ {atButton}
231
+ <span className="comment-hint">Ctrl Enter to post · Esc to cancel</span>
232
+ {counter}
233
+ </div>
234
+ {postButton}
235
+ </div>
236
+ {menu}
237
+ </div>
238
+ );
239
+ }