@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
@@ -1,37 +1,197 @@
1
1
  // Only for TEXT layers, and absent otherwise — a typography section over a
2
- // rectangle is four controls that write nothing.
2
+ // rectangle is four controls that write nothing. That gate is `layout.ts`'s
3
+ // table now, not this file's: both sections here used to re-decide it, and a
4
+ // second opinion in the component is the duplication the table removes. The
5
+ // `layer.kind !== "text"` guards on the WRITE helpers below stay, because a
6
+ // mixed selection reaches them by the union rule and a rect must be left alone.
3
7
  //
4
8
  // `verticalAlign` is offered here and is HONOURED, unlike in the 2D lane where
5
9
  // it is declared-and-inert: this lane owns the text box, so top/middle/bottom
6
10
  // is one offset in the rasterizer rather than a change to a layout model.
7
- import { ColorPicker, InspectorRow, InspectorSection, NumberField, SegmentedControl, TextField } from "@dailies-studio/ui";
8
- import { DEFAULT_TEXT_STAGGER } from "@dailies-studio/shot/doc";
9
- import type { TextStagger, TextStyle } from "@dailies-studio/shot/doc";
10
- import type { Layer3D, TextLayer3D } from "@dailies-studio/shot/doc";
11
- import { NO_SCRUB, useSelectedLayers, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
11
+ //
12
+ // EVERY READ IS A SCALAR, one per row. This file used to read the whole
13
+ // `TextStyle` object once and take every field off it, and `useSelectionValue`
14
+ // compares with `Object.is`: the object was mixed by identity for every
15
+ // multi-selection, so the rows showed the FIRST layer's values as if the
16
+ // selection agreed on them. One read per row is also what lets each widget
17
+ // carry its own "Mixed".
18
+ //
19
+ // THE FONT ROWS read what the family can draw (`selectionFontFacts`, over the
20
+ // lazily loaded catalog and workspace list) and write only through the
21
+ // store's font verbs, which fetch the face first and write once it lands.
22
+ import { useEffect, useMemo, useRef, useState } from "react";
23
+ import type { Ref } from "react";
24
+ import {
25
+ ColorField,
26
+ Combobox,
27
+ IconItalic,
28
+ InspectorRow,
29
+ InspectorSection,
30
+ NumberField,
31
+ SegmentedControl,
32
+ TextField,
33
+ useLocked,
34
+ } from "@dailies-studio/ui";
35
+ import { ExposeDot } from "../ExposeDot.js";
36
+ import { KeyframeDot } from "../KeyframeDot.js";
37
+ import type { ScrubHandlers } from "@dailies-studio/ui";
38
+ import {
39
+ fixedTextBox,
40
+ glyphEngineRefusals,
41
+ isGenericFontFamily,
42
+ primaryFontFamily,
43
+ textStyleForRole,
44
+ } from "@dailies-studio/shot/doc";
45
+ import type { TextEngine, TextRole, TextStyle } from "@dailies-studio/shot/doc";
46
+ import type { Layer3D, Paint, TextLayer3D } from "@dailies-studio/shot/doc";
47
+ import { loadGoogleFontRanges } from "../../../fonts/googleFonts.js";
48
+ import { useShotEditor, useStore } from "../../../state/context.js";
49
+ import { selectionFontFacts } from "../../../state/fonts.js";
50
+ import type { LayerFontFacts } from "../../../state/fonts.js";
51
+ import { requestFontPicker } from "../../fonts/pickerRequest.js";
52
+ import { useSelectedLayers, useSelectionScrub, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
53
+ import { FontField } from "./FontField.js";
54
+ import { PaintRows, type PaintEdit } from "./PaintRows.js";
55
+
56
+ /** Shown when the selection disagrees, in the two widgets that have no `mixed`
57
+ * prop of their own. `SegmentedControl` renders an unmatched value as "none of
58
+ * these selected", which is the same statement in its own vocabulary. */
59
+ const MIXED_PLACEHOLDER = "Mixed";
60
+ const NO_SEGMENT = "";
61
+ const TEXT_ROLE_OPTIONS: { value: TextRole; label: string; title: string }[] = [
62
+ { value: "display", label: "Disp", title: "Display" },
63
+ { value: "displayMedium", label: "Mid", title: "Display medium" },
64
+ { value: "body", label: "Body", title: "Body" },
65
+ { value: "emphasis", label: "Emph", title: "Emphasis" },
66
+ ];
67
+
68
+ /** None / Underline / Strike. `undefined` is the stored absence, and the
69
+ * control needs a value it can compare, so absence reads as the empty string
70
+ * and writing it writes `undefined` back. Both engines draw all three.
71
+ *
72
+ * A ONE-LETTER segment says its word to a screen reader and to the pointer
73
+ * (`ariaLabel`, `title`): "U" announced as "U" names nothing. */
74
+ const DECORATION_OPTIONS = [
75
+ { value: "", label: "None" },
76
+ { value: "underline", label: "U", ariaLabel: "Underline", title: "Underline" },
77
+ { value: "line-through", label: "S", ariaLabel: "Strikethrough", title: "Strikethrough" },
78
+ ];
79
+ const VERTICAL_OPTIONS = [
80
+ { value: "top", label: "T", ariaLabel: "Top", title: "Top" },
81
+ { value: "middle", label: "M", ariaLabel: "Middle", title: "Middle" },
82
+ { value: "bottom", label: "B", ariaLabel: "Bottom", title: "Bottom" },
83
+ ];
84
+
85
+ type StyleNumber = "sizePx" | "lineHeight" | "letterSpacingPx";
86
+
87
+ /** What the selected text's family can draw (state/fonts.ts). */
88
+ type SelectionFontFacts = LayerFontFacts | { kind: "mixed" } | null;
89
+
90
+ const WEIGHT_NAMES: Readonly<Record<number, string>> = {
91
+ 100: "Thin",
92
+ 200: "Extra Light",
93
+ 300: "Light",
94
+ 400: "Regular",
95
+ 500: "Medium",
96
+ 600: "Semi Bold",
97
+ 700: "Bold",
98
+ 800: "Extra Bold",
99
+ 900: "Black",
100
+ };
101
+
102
+ /** "Semi Bold 600". A weight with no name (450) reads as its number. */
103
+ function weightLabel(weight: number): string {
104
+ const name = WEIGHT_NAMES[weight];
105
+ return name === undefined ? String(weight) : `${name} ${weight}`;
106
+ }
12
107
 
13
108
  export function TypographySection() {
109
+ const store = useStore();
14
110
  const write = useSelectionWrite();
111
+ // kind gate removed: see layout.ts
112
+ const content = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.content : undefined));
113
+ // The font rows read the TEXT layers alone, as the font verbs write them:
114
+ // a rect beside a text layer has no family to disagree with.
15
115
  const layers = useSelectedLayers();
16
- const anyText = layers.some((l) => l.kind === "text");
17
- const style = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.style : undefined));
18
- const content = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.content : ""));
19
- if (!anyText || style.value === undefined) return null;
20
-
21
- const patchStyle = (next: Partial<TextStyle>): void =>
22
- write((layer) =>
23
- layer.kind === "text"
24
- ? ({ ...layer, text: { ...layer.text, style: { ...layer.text.style, ...next } } } as Layer3D)
25
- : layer,
26
- );
116
+ const texts = useMemo(() => layers.filter((layer): layer is TextLayer3D => layer.kind === "text"), [layers]);
117
+ const assets = useShotEditor((s) => s.doc?.assets);
118
+ const catalog = useShotEditor((s) => s.fontCatalog);
119
+ const workspace = useShotEditor((s) => s.workspaceFonts?.fonts ?? null);
120
+ const facts = useMemo(
121
+ () => selectionFontFacts(texts, assets ?? {}, catalog, workspace),
122
+ [texts, assets, catalog, workspace],
123
+ );
124
+ // The primary family, so `Inter` and `"Inter", sans-serif` are one family.
125
+ const families = texts.map((layer) => primaryFontFamily(layer.text.style.fontFamily));
126
+ const family = families[0] ?? "";
127
+ // LAZY, and only for text: the catalog while a layer waits on it (a
128
+ // catalog copy, or a machine font that Self-host may name), the workspace
129
+ // list once (a workspace copy reads as a plain file until it lands). A load
130
+ // that fails stays failed until the next mount: the effect runs on these
131
+ // flags, not on every render.
132
+ const needsCatalog = facts?.kind === "loading";
133
+ const needsWorkspace = texts.length > 0 && workspace === null;
134
+ useEffect(() => {
135
+ if (needsCatalog) void store.loadFontCatalog();
136
+ }, [store, needsCatalog]);
137
+ useEffect(() => {
138
+ if (needsWorkspace) void store.loadWorkspaceFonts();
139
+ }, [store, needsWorkspace]);
140
+ const align = useSelectionValue((layer) => readStyle(layer)?.align);
141
+ // Absent MEANS "top" (types.ts), so the default is applied inside the read
142
+ // and a layer that stores it explicitly agrees with one that does not. The
143
+ // undefined that survives is the one that says "not a text layer".
144
+ const verticalAlign = useSelectionValue((layer) => {
145
+ const style = readStyle(layer);
146
+ return style === undefined ? undefined : (style.verticalAlign ?? "top");
147
+ });
148
+ const decoration = useSelectionValue((layer) => {
149
+ const style = readStyle(layer);
150
+ return style === undefined ? undefined : (style.decoration ?? "");
151
+ });
152
+ // Absent MEANS canvas and always will (schema v30), so the default is
153
+ // applied in the read: a layer that stores "canvas" and one that stores
154
+ // nothing must show the same segment.
155
+ const engine = useSelectionValue((layer) =>
156
+ layer.kind === "text" ? (layer.text.engine ?? "canvas") : undefined,
157
+ );
158
+ // WHY the glyph segment is unavailable, in the validator's own words. Only
159
+ // the first is shown: a control has one line of room, and an author fixes
160
+ // these one at a time anyway.
161
+ const glyphBlocked = useSelectionValue((layer) =>
162
+ layer.kind === "text" ? (glyphEngineRefusals(layer)[0]?.message ?? "") : undefined,
163
+ );
164
+ // Absent MEANS fixed (schema v38), so the default is applied in the read.
165
+ const box = useSelectionValue((layer) => (layer.kind === "text" ? (layer.text.box ?? "fixed") : undefined));
166
+ const size = useStyleNumber("sizePx");
167
+ const lineHeight = useStyleNumber("lineHeight");
168
+ const letterSpacing = useStyleNumber("letterSpacingPx");
169
+ const role = useSelectionValue(textRoleOf);
170
+
171
+ const patchStyle = (next: Partial<TextStyle>): void => write((layer) => withStyle(layer, next));
27
172
 
28
- const s = style.value;
29
173
  return (
30
- <InspectorSection title="Text" persistKey="shot:text">
31
- <InspectorRow label="Content">
174
+ <InspectorSection className="insp-object" title="Text" persistKey="shot:text">
175
+ {/* THE ONE HIGH-VALUE PATH WITH NO DIAMOND, so it mounts the pin itself.
176
+ `text.content` is not in `TRACKABLE_PATHS` — a string has no
177
+ in-between and the table refuses it by name — yet it is the field a
178
+ reusable component publishes first: one Title definition placed five
179
+ times with five headlines is the whole reason exposed props exist.
180
+ The kind is stated rather than looked up, because there is no table
181
+ row here to look it up from. A GROWING box, so the row aligns to its
182
+ start: the label stays level with the first line and the pin sits
183
+ at the box's top right, not centred on a paragraph. */}
184
+ <InspectorRow
185
+ label="Content"
186
+ align="start"
187
+ trailing={<ExposeDot path="text.content" label="Content" kind="string" />}
188
+ >
32
189
  <TextField
33
- value={content.value ?? ""}
190
+ value={content.mixed ? "" : (content.value ?? "")}
191
+ {...(content.mixed ? { placeholder: MIXED_PLACEHOLDER } : {})}
34
192
  ariaLabel="Text content"
193
+ // A textarea keeps "\n": Enter breaks the line, blur commits.
194
+ multiline
35
195
  onCommit={(next) =>
36
196
  write((layer) =>
37
197
  layer.kind === "text" ? ({ ...layer, text: { ...layer.text, content: next } } as Layer3D) : layer,
@@ -39,132 +199,541 @@ export function TypographySection() {
39
199
  }
40
200
  />
41
201
  </InspectorRow>
202
+ {/* No typed family any more: the picker's search is how a family is
203
+ named, and every pick goes through the store's one apply path. */}
42
204
  <InspectorRow label="Font">
43
- <TextField value={s.fontFamily} ariaLabel="Font family" onCommit={(next) => patchStyle({ fontFamily: next })} />
205
+ <FontField value={family} mixed={families.some((f) => f !== family)} />
44
206
  </InspectorRow>
45
- <InspectorRow>
46
- <NumberField value={s.sizePx} label="Size" ariaLabel="Font size" unit="px" min={1} step={1} onCommit={(v) => patchStyle({ sizePx: v })} />
207
+ {facts?.kind === "machine" ? <MachineFontNote family={facts.family} selfHost={facts.selfHost?.family ?? null} /> : null}
208
+ {/* Role writes its weight with the rest of the role in one edit, and
209
+ the store's `followFontWeights` then swaps the files to match. */}
210
+ <InspectorRow label="Role">
211
+ <SegmentedControl
212
+ ariaLabel="Text role"
213
+ value={role.mixed ? NO_SEGMENT : (role.value ?? NO_SEGMENT)}
214
+ options={TEXT_ROLE_OPTIONS}
215
+ onChange={(next) => write((layer) => withRole(layer, next as TextRole))}
216
+ />
47
217
  </InspectorRow>
218
+ {/* "Size", not the table's "Text size": the section is already called Text,
219
+ and a tooltip that disagreed with the pill beside it reads as a second
220
+ property. */}
48
221
  <InspectorRow>
49
- <NumberField value={s.weight} label="Weight" ariaLabel="Font weight" min={100} max={900} step={100} onCommit={(v) => patchStyle({ weight: v })} />
222
+ <NumberField
223
+ value={size.value ?? 16}
224
+ mixed={size.mixed}
225
+ label="Size"
226
+ ariaLabel="Font size"
227
+ unit="px"
228
+ min={1}
229
+ // 1px is the smallest type that is type at all.
230
+ step={1}
231
+ trailing={<KeyframeDot path="text.style.sizePx" label="Size" />}
232
+ onCommit={size.commit}
233
+ onScrubStart={size.scrub.start}
234
+ onScrub={size.scrub.move}
235
+ onScrubEnd={size.scrub.end}
236
+ />
50
237
  </InspectorRow>
51
- <InspectorRow>
52
- <NumberField value={s.lineHeight} label="Line" ariaLabel="Line height" step={0.05} min={0.5} onCommit={(v) => patchStyle({ lineHeight: v })} />
238
+ <WeightRow texts={texts} facts={facts} />
239
+ {/* Auto width grows with the text and never wraps; Fixed wraps inside
240
+ its width. Fixed keeps the measured size, so the switch moves
241
+ nothing on screen. */}
242
+ <InspectorRow label="Box">
243
+ <SegmentedControl
244
+ ariaLabel="Text box"
245
+ value={box.mixed ? NO_SEGMENT : (box.value ?? "fixed")}
246
+ options={[
247
+ { value: "auto", label: "Auto width" },
248
+ { value: "fixed", label: "Fixed" },
249
+ ]}
250
+ onChange={(next) =>
251
+ write((layer) =>
252
+ next === "fixed"
253
+ ? fixedTextBox(layer)
254
+ : layer.kind === "text"
255
+ ? ({ ...layer, text: { ...layer.text, box: "auto" } } as Layer3D)
256
+ : layer,
257
+ )
258
+ }
259
+ />
53
260
  </InspectorRow>
261
+ {/* No colour row here: the glyph colour is the Fill section's (F20),
262
+ which this layer always shows, and a second row was a second
263
+ diamond over the one `text.style.color` track. */}
54
264
  <InspectorRow>
55
- <NumberField value={s.letterSpacingPx} label="Letter" ariaLabel="Letter spacing" unit="px" step={0.1} onCommit={(v) => patchStyle({ letterSpacingPx: v })} />
265
+ <NumberField
266
+ value={lineHeight.value ?? 1.2}
267
+ mixed={lineHeight.mixed}
268
+ label="Line"
269
+ ariaLabel="Line height"
270
+ title="Line height (multiplier)"
271
+ step={0.05}
272
+ min={0.5}
273
+ // Below 0.5 the lines overlap into mush.
274
+ trailing={<KeyframeDot path="text.style.lineHeight" />}
275
+ onCommit={lineHeight.commit}
276
+ onScrubStart={lineHeight.scrub.start}
277
+ onScrub={lineHeight.scrub.move}
278
+ onScrubEnd={lineHeight.scrub.end}
279
+ />
56
280
  </InspectorRow>
57
- <InspectorRow label="Color">
58
- <ColorPicker value={s.color} ariaLabel="Text colour" scrub={NO_SCRUB} commit={(hex) => patchStyle({ color: hex })} />
281
+ <InspectorRow>
282
+ <NumberField
283
+ value={letterSpacing.value ?? 0}
284
+ mixed={letterSpacing.mixed}
285
+ label="Letter"
286
+ ariaLabel="Letter spacing"
287
+ unit="px"
288
+ step={0.1}
289
+ trailing={<KeyframeDot path="text.style.letterSpacingPx" />}
290
+ onCommit={letterSpacing.commit}
291
+ onScrubStart={letterSpacing.scrub.start}
292
+ onScrub={letterSpacing.scrub.move}
293
+ onScrubEnd={letterSpacing.scrub.end}
294
+ />
59
295
  </InspectorRow>
296
+ {/* JUSTIFY IS GLYPH-ONLY, and that is a capability rather than a
297
+ preference: the Canvas2D path has always drawn `justify` as `left`,
298
+ because `measureText` gives no way to distribute the slack a line
299
+ leaves without re-shaping it by hand. Offering the segment on a
300
+ canvas layer would offer a setting that does nothing. */}
60
301
  <InspectorRow label="Align">
61
302
  <SegmentedControl
62
303
  ariaLabel="Text align"
63
- value={s.align}
304
+ value={align.mixed ? NO_SEGMENT : (align.value ?? "left")}
64
305
  options={[
65
- { value: "left", label: "L" },
66
- { value: "center", label: "C" },
67
- { value: "right", label: "R" },
306
+ { value: "left", label: "L", ariaLabel: "Left", title: "Left" },
307
+ { value: "center", label: "C", ariaLabel: "Centre", title: "Centre" },
308
+ { value: "right", label: "R", ariaLabel: "Right", title: "Right" },
309
+ {
310
+ value: "justify",
311
+ label: "J",
312
+ ariaLabel: "Justify",
313
+ title:
314
+ engine.value === "glyph"
315
+ ? "Justify"
316
+ : "Justify needs the Glyph engine: the Canvas engine draws it as Left",
317
+ disabled: engine.mixed || engine.value !== "glyph",
318
+ },
68
319
  ]}
69
320
  onChange={(next) => patchStyle({ align: next as TextStyle["align"] })}
70
321
  />
71
322
  </InspectorRow>
323
+ <InspectorRow label="Decoration">
324
+ <SegmentedControl
325
+ ariaLabel="Text decoration"
326
+ value={decoration.mixed ? NO_SEGMENT : (decoration.value ?? "")}
327
+ options={DECORATION_OPTIONS}
328
+ onChange={(next) => write((layer) => withDecoration(layer, next))}
329
+ />
330
+ </InspectorRow>
72
331
  <InspectorRow label="Vertical">
73
332
  <SegmentedControl
74
333
  ariaLabel="Vertical align"
75
- value={s.verticalAlign ?? "top"}
334
+ value={verticalAlign.mixed ? NO_SEGMENT : (verticalAlign.value ?? "top")}
335
+ options={VERTICAL_OPTIONS}
336
+ onChange={(next) => patchStyle({ verticalAlign: next as NonNullable<TextStyle["verticalAlign"]> })}
337
+ />
338
+ </InspectorRow>
339
+ {/* LAST, because it is the rarest edit and the one with consequences:
340
+ switching a layer to Glyph changes which engine draws it, and the
341
+ disabled reason is the validator's own sentence so the control and
342
+ the save gate can never disagree. */}
343
+ <InspectorRow label="Engine">
344
+ <SegmentedControl
345
+ ariaLabel="Text engine"
346
+ value={engine.mixed ? NO_SEGMENT : (engine.value ?? "canvas")}
76
347
  options={[
77
- { value: "top", label: "T" },
78
- { value: "middle", label: "M" },
79
- { value: "bottom", label: "B" },
348
+ { value: "canvas", label: "Canvas" },
349
+ {
350
+ value: "glyph",
351
+ label: "Glyph",
352
+ title: glyphBlocked.mixed
353
+ ? "Some of these layers cannot use the Glyph engine"
354
+ : (glyphBlocked.value ?? "") === ""
355
+ ? "Glyph: outlines from a project font, drawn on the GPU"
356
+ : (glyphBlocked.value ?? ""),
357
+ disabled: glyphBlocked.mixed || (glyphBlocked.value ?? "") !== "",
358
+ },
80
359
  ]}
81
- onChange={(next) => patchStyle({ verticalAlign: next as NonNullable<TextStyle["verticalAlign"]> })}
360
+ onChange={(next) =>
361
+ write((layer) =>
362
+ layer.kind === "text"
363
+ ? ({ ...layer, text: { ...layer.text, engine: next as TextEngine } } as Layer3D)
364
+ : layer,
365
+ )
366
+ }
82
367
  />
83
368
  </InspectorRow>
84
369
  </InspectorSection>
85
370
  );
86
371
  }
87
372
 
373
+ /** The one value the selected text layers share, or Mixed. */
374
+ function agreed<T>(values: readonly T[]): { value: T | undefined; mixed: boolean } {
375
+ const value = values[0];
376
+ return { value, mixed: values.some((v) => v !== value) };
377
+ }
378
+
88
379
  /**
89
- * The SHAPE of a per-unit stagger, which is a property of the text rather than
90
- * of any one block — see `TextStagger` for why it cannot live on the block.
380
+ * WEIGHT, with Italic in its box's accessory lane. ONE labelled 28px pill in
381
+ * every state and both FieldLabels modes, only the control inside it changing,
382
+ * so nothing moves when the catalog lands and Italic is never remounted:
383
+ * - a catalog or workspace family with named weights: a menu of exactly
384
+ * those ("Bold 700"), one entry when it has one;
385
+ * - a variable family: a number clamped to its axis, typed to 1 and stepped
386
+ * by 100;
387
+ * - anything else (an uploaded file, a machine font, loading, Mixed): 100 to
388
+ * 900 by 100, as before.
389
+ * The number field takes no `label` of its own: under "named" it would draw
390
+ * its name above the box, a line the menu does not have.
391
+ * Every commit is `setFontWeight`, which fetches the face first, so a
392
+ * workspace family read as a plain file while its list loads still follows.
91
393
  *
92
- * Its own section rather than four more rows under Text: it is inert until a
93
- * `stagger` block exists on the layer, so it deserves to be collapsed away for
94
- * the layers that never animate.
394
+ * THE MENU WAITS FOR A FOCUSED NUMBER TO BLUR. A load that lands while the
395
+ * author types in the number (the catalog, the workspace list) would swap it
396
+ * for the menu, unmounting the input with its draft and dropping focus on
397
+ * <body>, where the next Backspace deletes the layer. The number holds its
398
+ * place until focus leaves it; its blur commits the draft as always, then
399
+ * the menu takes the slot. Focus is read on `document`, after React's own
400
+ * blur handler has committed.
95
401
  *
96
- * Every field writes a WHOLE record spread from the default, so the first edit
97
- * materialises what "absent means the default" already meant. Reading does the
98
- * same, so the fields show the real starting values instead of blanks.
402
+ * A DRAG previews through the inspector's scrub lane and commits nothing
403
+ * there. A move writes `weight` alone, which fetches nothing (the store
404
+ * follows files only on a commit), and that is safe to show: a variable file
405
+ * draws every weight on its axis, and a plain file draws whatever weight it
406
+ * is given. The release cancels the scrub and hands the last value to
407
+ * `setFontWeight`, the one undo step.
99
408
  */
100
- export function TextStaggerSection() {
101
- const write = useSelectionWrite();
102
- const layers = useSelectedLayers();
103
- const anyText = layers.some((l) => l.kind === "text");
104
- const stagger = useSelectionValue((layer) =>
105
- layer.kind === "text" ? (layer.text.stagger ?? DEFAULT_TEXT_STAGGER) : undefined,
106
- );
107
- if (!anyText || stagger.value === undefined) return null;
108
-
109
- const patch = (next: Partial<TextStagger>): void =>
110
- write((layer) =>
111
- layer.kind === "text"
112
- ? ({
113
- ...layer,
114
- text: { ...layer.text, stagger: { ...DEFAULT_TEXT_STAGGER, ...layer.text.stagger, ...next } },
115
- } as Layer3D)
116
- : layer,
409
+ function WeightRow(props: { texts: readonly TextLayer3D[]; facts: SelectionFontFacts }) {
410
+ const store = useStore();
411
+ const { facts } = props;
412
+ const weight = agreed(props.texts.map((layer) => layer.text.style.weight));
413
+ const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { weight: Number(raw) }));
414
+ const dragged = useRef<number | null>(null);
415
+ const commit = (next: number): void => void store.setFontWeight(next);
416
+ const family = facts?.kind === "catalog" || facts?.kind === "workspace" ? facts : null;
417
+ const axis = family?.axis ?? null;
418
+ // A variable Google family's commit plans its faces with the subset ranges,
419
+ // a chunk nothing loads before a pick. Cold, the release that cancels the
420
+ // scrub would show the old weight for that round trip. A failure here is
421
+ // the commit's to retry and report.
422
+ const warmRanges = family?.kind === "catalog" && axis !== null;
423
+ useEffect(() => {
424
+ if (warmRanges) loadGoogleFontRanges().catch(() => {});
425
+ }, [warmRanges]);
426
+ // The Italic key is the row's one element this file holds; the row's one
427
+ // input is the number's (the menu's search is portaled away).
428
+ const italic = useRef<HTMLButtonElement>(null);
429
+ const [typing, setTyping] = useState(false);
430
+ useEffect(() => {
431
+ const inNumber = (node: EventTarget | null): boolean => {
432
+ const row = italic.current?.closest(".insp-prop");
433
+ return row != null && node instanceof HTMLInputElement && row.contains(node);
434
+ };
435
+ const onIn = (e: FocusEvent): void => setTyping(inNumber(e.target));
436
+ const onOut = (e: FocusEvent): void => setTyping(inNumber(e.relatedTarget));
437
+ document.addEventListener("focusin", onIn);
438
+ document.addEventListener("focusout", onOut);
439
+ return () => {
440
+ document.removeEventListener("focusin", onIn);
441
+ document.removeEventListener("focusout", onOut);
442
+ };
443
+ }, []);
444
+ let control;
445
+ if (family !== null && axis === null && family.weights.length > 0 && !typing) {
446
+ // A stored weight the family has no file for (a Role's 500 on 400 and
447
+ // 700) is still what the document holds, so the trigger names it.
448
+ const listed = !weight.mixed && weight.value !== undefined && family.weights.includes(weight.value);
449
+ control = (
450
+ <Combobox
451
+ ariaLabel="Font weight"
452
+ options={family.weights.map((w) => ({ value: String(w), label: weightLabel(w) }))}
453
+ value={listed ? String(weight.value) : ""}
454
+ placeholder={weight.mixed ? MIXED_PLACEHOLDER : weightLabel(weight.value ?? 400)}
455
+ onChange={(next) => commit(Number(next))}
456
+ />
117
457
  );
458
+ } else {
459
+ control = (
460
+ <NumberField
461
+ value={weight.value ?? 400}
462
+ mixed={weight.mixed}
463
+ ariaLabel="Font weight"
464
+ min={axis?.[0] ?? 100}
465
+ max={axis?.[1] ?? 900}
466
+ {...(axis === null ? { step: 100 } : { step: 1, nudge: 100 })}
467
+ onCommit={commit}
468
+ onScrubStart={() => {
469
+ dragged.current = null;
470
+ scrub.start();
471
+ }}
472
+ onScrub={(next) => {
473
+ dragged.current = next;
474
+ scrub.move(next);
475
+ }}
476
+ onScrubEnd={(ok) => {
477
+ scrub.end(false);
478
+ const last = dragged.current;
479
+ dragged.current = null;
480
+ if (ok && last !== null) commit(last);
481
+ }}
482
+ />
483
+ );
484
+ }
485
+ return (
486
+ <InspectorRow label="Weight" trailing={<ItalicKey ref={italic} texts={props.texts} facts={facts} />}>
487
+ {control}
488
+ </InspectorRow>
489
+ );
490
+ }
118
491
 
119
- const g = stagger.value;
492
+ /**
493
+ * ITALIC, a verb in the Weight box's lane: pressed while the text is italic,
494
+ * Mixed while the layers disagree (a press makes every one italic, the house
495
+ * switch policy), and refused with the reason when the family has no italic
496
+ * (`aria-disabled`, still pressable so the press can say why). Disabled under
497
+ * `LockedControls`. `setFontStyle` fetches the face first.
498
+ */
499
+ function ItalicKey(props: { ref: Ref<HTMLButtonElement>; texts: readonly TextLayer3D[]; facts: SelectionFontFacts }) {
500
+ const store = useStore();
501
+ const locked = useLocked();
502
+ const { facts } = props;
503
+ const italic = agreed(props.texts.map((layer) => layer.text.style.fontStyle === "italic"));
504
+ const pressed = italic.mixed ? "mixed" : italic.value === true;
505
+ const missing =
506
+ pressed !== true && (facts?.kind === "catalog" || facts?.kind === "workspace") && !facts.italic
507
+ ? `${facts.family} has no italic style.`
508
+ : null;
120
509
  return (
121
- <InspectorSection title="Stagger" persistKey="shot:textStagger">
122
- <InspectorRow label="Unit">
123
- <SegmentedControl
124
- ariaLabel="Stagger unit"
125
- value={g.unit}
126
- options={[
127
- { value: "char", label: "Char" },
128
- { value: "word", label: "Word" },
129
- { value: "line", label: "Line" },
130
- ]}
131
- onChange={(next) => patch({ unit: next as TextStagger["unit"] })}
132
- />
133
- </InspectorRow>
134
- <InspectorRow>
135
- <NumberField
136
- value={g.spread}
137
- label="Spread"
138
- ariaLabel="Stagger spread"
139
- min={0}
140
- max={1}
141
- step={0.05}
142
- onCommit={(v) => patch({ spread: v })}
143
- />
144
- </InspectorRow>
145
- <InspectorRow>
146
- <NumberField
147
- value={g.risePx}
148
- label="Rise"
149
- ariaLabel="Stagger rise"
150
- unit="px"
151
- step={1}
152
- onCommit={(v) => patch({ risePx: v })}
153
- />
154
- </InspectorRow>
155
- <InspectorRow>
156
- <NumberField
157
- value={g.fadeFrom}
158
- label="Fade from"
159
- ariaLabel="Stagger fade from"
160
- min={0}
161
- max={1}
162
- step={0.05}
163
- onCommit={(v) => patch({ fadeFrom: v })}
510
+ <button
511
+ ref={props.ref}
512
+ type="button"
513
+ className="insp-key is-toggle"
514
+ aria-label="Italic"
515
+ aria-pressed={pressed}
516
+ title={missing ?? "Italic"}
517
+ {...(missing === null ? {} : { "aria-disabled": true })}
518
+ disabled={locked}
519
+ onClick={() => {
520
+ if (missing !== null) store.reportError(missing);
521
+ else void store.setFontStyle(pressed === true ? "normal" : "italic");
522
+ }}
523
+ >
524
+ <IconItalic />
525
+ </button>
526
+ );
527
+ }
528
+
529
+ /**
530
+ * A canvas text layer with no font file draws whatever the computer has
531
+ * (`drawsMachineFont`), so the note says so under the Font row, with the one
532
+ * way out: the catalog's copy when Google Fonts has the family, else the
533
+ * picker's upload view. A generic keyword (`sans-serif`) opens the picker's
534
+ * list instead, since no file is that family.
535
+ *
536
+ * SELF-HOST REMOVES THE NOTE: once the copy lands the layer has a file. So a
537
+ * press made from the verb hands focus to the Font row's trigger, the field
538
+ * the note sits under (its previous sibling), unless focus moved on while the
539
+ * file downloaded.
540
+ */
541
+ function MachineFontNote(props: { family: string; selfHost: string | null }) {
542
+ const store = useStore();
543
+ const locked = useLocked();
544
+ // A generic keyword (`sans-serif`) names no font a file could be: every
545
+ // computer has its own, so the way out is choosing one.
546
+ const generic = isGenericFontFamily(props.family);
547
+ const selfHost = async (verb: HTMLButtonElement): Promise<void> => {
548
+ const trigger = verb.closest(".insp-notice")?.previousElementSibling?.querySelector<HTMLElement>("button");
549
+ const held = document.activeElement === verb;
550
+ const outcome = await store.selfHostFont();
551
+ const now = document.activeElement;
552
+ if (outcome.status === "applied" && held && (now === verb || now === null || now === document.body)) {
553
+ trigger?.focus();
554
+ }
555
+ };
556
+ return (
557
+ <div className="insp-notice is-warn" data-testid="machine-font-note">
558
+ {generic
559
+ ? `This text draws the computer's own ${props.family} font. Viewers, renders and exports may draw a different one.`
560
+ : `Only this computer has ${props.family}. Viewers, renders and exports may draw another font.`}
561
+ <div className="insp-buttons">
562
+ {generic ? (
563
+ <button type="button" className="btn" disabled={locked} onClick={() => requestFontPicker("list")}>
564
+ Choose a font
565
+ </button>
566
+ ) : props.selfHost !== null ? (
567
+ <button type="button" className="btn" disabled={locked} onClick={(e) => void selfHost(e.currentTarget)}>
568
+ {`Self-host ${props.selfHost}`}
569
+ </button>
570
+ ) : (
571
+ <button type="button" className="btn" disabled={locked} onClick={() => requestFontPicker("upload")}>
572
+ Upload font file
573
+ </button>
574
+ )}
575
+ </div>
576
+ </div>
577
+ );
578
+ }
579
+
580
+ /**
581
+ * A text layer's Fill is what its GLYPHS are painted with (F20), the way Figma
582
+ * and Framer read it. `paint.fill` on text is the plate behind the words, so
583
+ * `FillSection` titles itself "Background" there. Over a mixed selection this
584
+ * one says "Text fill", so the two never share a name.
585
+ *
586
+ * The layer fill's own rows (`PaintRows`), over the one paint the glyphs show:
587
+ * Solid is `text.style.color`, and Linear or Radial is `text.style.fill`
588
+ * (schema v47), written by `withTextPaint` so the document holds one or the
589
+ * other. Two cases keep the plain colour row, each saying why, because the
590
+ * save gate refuses a gradient there: a keyed colour (a `text.style.color`
591
+ * track beside a fill is a second writer) and the Glyph engine.
592
+ */
593
+ export function TextFillSection() {
594
+ const layers = useSelectedLayers();
595
+ const write = useSelectionWrite();
596
+ const texts = layers.filter((layer): layer is TextLayer3D => layer.kind === "text");
597
+ // Each layer's OWN paint as painted, so a multi-selection edit changes one
598
+ // field per layer and never copies the lead's paint onto the rest.
599
+ const painted = new Map(texts.map((layer) => [layer.id, textPaintOf(layer)]));
600
+ const edited = (layer: Layer3D, edit: PaintEdit): Layer3D =>
601
+ layer.kind === "text" ? withTextPaint(layer, edit(painted.get(layer.id) ?? textPaintOf(layer))) : layer;
602
+ const scrub = useSelectionScrub((layer, raw) => edited(layer, raw as PaintEdit));
603
+ const blocked = texts.some((layer) => layer.tracks?.["text.style.color"] !== undefined)
604
+ ? "Remove the colour keys to fill the text with a gradient."
605
+ : texts.some((layer) => layer.text.engine === "glyph")
606
+ ? "Gradient text needs the Canvas engine."
607
+ : null;
608
+ const lead = texts[0];
609
+ return (
610
+ <InspectorSection
611
+ className="insp-object"
612
+ title={layers.every((layer) => layer.kind === "text") ? "Fill" : "Text fill"}
613
+ persistKey="shot:textFill"
614
+ >
615
+ {blocked !== null || lead === undefined ? (
616
+ <>
617
+ <TextColorRow />
618
+ {blocked !== null ? <p className="insp-note">{blocked}</p> : null}
619
+ </>
620
+ ) : (
621
+ <PaintRows
622
+ // NO `key`, for `FillSection`'s reason: a remount mid-drag drops the
623
+ // picker's pointer capture.
624
+ value={textPaintOf(lead)}
625
+ all={texts.map(textPaintOf)}
626
+ ariaPrefix="Text"
627
+ label={null}
628
+ solidTrailing={<KeyframeDot path="text.style.color" />}
629
+ onScrubStart={scrub.start}
630
+ onScrub={(edit) => scrub.move(edit)}
631
+ onScrubEnd={scrub.end}
632
+ onCommit={(edit) => write((layer) => edited(layer, edit))}
164
633
  />
165
- </InspectorRow>
634
+ )}
166
635
  </InspectorSection>
167
636
  );
168
637
  }
169
638
 
170
- void ({} as TextLayer3D);
639
+ /** The one paint a text layer's glyphs show: its gradient fill, else its
640
+ * colour as a solid. */
641
+ function textPaintOf(layer: TextLayer3D): Paint {
642
+ return layer.text.style.fill ?? { type: "solid", color: layer.text.style.color };
643
+ }
644
+
645
+ /** The ONE text paint edit, so a drag's move and its release land the same
646
+ * document. A solid writes `color` and drops `fill`; a gradient writes
647
+ * `fill` and keeps `color`, which a later switch back to Solid replaces. */
648
+ function withTextPaint(layer: TextLayer3D, paint: Paint): Layer3D {
649
+ const { fill: _drop, ...style } = layer.text.style;
650
+ return {
651
+ ...layer,
652
+ text: { ...layer.text, style: paint.type === "solid" ? { ...style, color: paint.color } : { ...style, fill: paint } },
653
+ };
654
+ }
655
+
656
+ /** `text.style.color`: the glyph colour, the Fill section's one row, its
657
+ * diamond inside the field. */
658
+ function TextColorRow() {
659
+ const write = useSelectionWrite();
660
+ const color = useSelectionValue((layer) => readStyle(layer)?.color);
661
+ // THE DRAG HALF: the picker's SV square, hue bar and alpha bar all report
662
+ // through `scrub.move` and never `commit` while the pointer is down.
663
+ const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { color: String(raw) }));
664
+ return (
665
+ <InspectorRow>
666
+ <ColorField
667
+ value={color.value ?? "#ffffff"}
668
+ mixed={color.mixed}
669
+ ariaLabel="Text colour"
670
+ scrub={scrub}
671
+ commit={(hex) => write((layer) => withStyle(layer, { color: hex }))}
672
+ trailing={<KeyframeDot path="text.style.color" />}
673
+ />
674
+ </InspectorRow>
675
+ );
676
+ }
677
+
678
+ interface NumberRow {
679
+ value: number | undefined;
680
+ mixed: boolean;
681
+ commit: (value: number) => void;
682
+ scrub: ScrubHandlers;
683
+ }
684
+
685
+ /** One numeric style field: the scalar it READS, the typed value it COMMITS and
686
+ * the label drag it SCRUBS, so a row is one call rather than three. */
687
+ function useStyleNumber(field: StyleNumber): NumberRow {
688
+ const write = useSelectionWrite();
689
+ const { value, mixed } = useSelectionValue((layer) => readStyle(layer)?.[field]);
690
+ const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { [field]: Number(raw) }));
691
+ return { value, mixed, commit: (next) => write((layer) => withStyle(layer, { [field]: next })), scrub };
692
+ }
693
+
694
+ function readStyle(layer: Layer3D): TextStyle | undefined {
695
+ return layer.kind === "text" ? layer.text.style : undefined;
696
+ }
697
+
698
+ /** Infer a role only while every persisted metric agrees with it. An authored
699
+ * custom style remains intentionally unmatched rather than acquiring a label
700
+ * the document does not store. */
701
+ function textRoleOf(layer: Layer3D): TextRole | undefined {
702
+ const style = readStyle(layer);
703
+ if (style === undefined) return undefined;
704
+ return TEXT_ROLE_OPTIONS.find(({ value }) => {
705
+ const role = textStyleForRole(value, style.sizePx);
706
+ return (
707
+ style.weight === role.weight &&
708
+ style.lineHeight === role.lineHeight &&
709
+ Math.abs(style.letterSpacingPx - role.letterSpacingPx) < 1e-9
710
+ );
711
+ })?.value;
712
+ }
713
+
714
+ /** Decoration is the one style field the control can set back to ABSENT, and
715
+ * absent is not `undefined`: under `exactOptionalPropertyTypes` a spread of
716
+ * `{ decoration: undefined }` is a different document from one with no key,
717
+ * and the schema holds no key. So it is deleted rather than assigned. */
718
+ function withDecoration(layer: Layer3D, next: string): Layer3D {
719
+ if (layer.kind !== "text") return layer;
720
+ if (next === "") {
721
+ const { decoration: _dropped, ...style } = layer.text.style;
722
+ return { ...layer, text: { ...layer.text, style } } as Layer3D;
723
+ }
724
+ return withStyle(layer, { decoration: next as NonNullable<TextStyle["decoration"]> });
725
+ }
726
+
727
+ /** The ONE style edit, so a drag's move and its release land the same document.
728
+ * A non-text layer in the selection is left alone rather than grown a `text`
729
+ * member it has no meaning for. */
730
+ function withStyle(layer: Layer3D, next: Partial<TextStyle>): Layer3D {
731
+ if (layer.kind !== "text") return layer;
732
+ return { ...layer, text: { ...layer.text, style: { ...layer.text.style, ...next } } } as Layer3D;
733
+ }
734
+
735
+ /** Apply a role as raw values and preserve each selected layer's own size. */
736
+ function withRole(layer: Layer3D, role: TextRole): Layer3D {
737
+ if (layer.kind !== "text") return layer;
738
+ return withStyle(layer, textStyleForRole(role, layer.text.style.sizePx));
739
+ }