@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
@@ -27,23 +27,38 @@
27
27
  //
28
28
  // ARROWS are listed as both "nudge the playhead" and "nudge the layer".
29
29
  // SELECTION WINS; an empty selection falls through to the playhead. That is
30
- // the only reading where both are reachable without a mode.
30
+ // the only reading where both are reachable without a mode. The playhead
31
+ // half then forks three ways — ⇧ is a second, ↑/↓ walk the edit points, and
32
+ // ←/→ step a frame — because the vertical pair was otherwise a second
33
+ // spelling of the horizontal one.
31
34
  //
32
35
  // `C` was listed as both the cut tool and split-at-playhead. THE CUT TOOL IS
33
36
  // GONE — it was a fourteenth row in the tool table that split a scene under
34
- // the cursor, which `⌘B` already does from the playhead with no mode to
35
- // arm and no mode to leave. `⌘B` is now the only splitter, and `⇧C` is
36
- // Blender's "frame all" rather than the camera-move it used to be (the
37
- // `+ shot` button owns that, via `store.addShotAtPlayhead`).
37
+ // the cursor, which `⌘⇧B` now does from the playhead with no mode to arm
38
+ // and no mode to leave. The splitters are `⌘B` (the razor, over the layers
39
+ // under the playhead) and `⌘⇧B` (the shot itself), and no letter arms
40
+ // either. `⇧C` is Blender's "frame all" rather than the camera-move it used
41
+ // to be (the `+ shot` button owns that, via `store.addShotAtPlayhead`).
38
42
  //
39
43
  // SPACE is both play/pause and hold-to-pan. It fires on KEYUP, and only when
40
44
  // no pan drag happened during the hold — which is why the stage owns the
41
45
  // suppression (it is the half that knows) and the chrome, which cannot pan,
42
46
  // always fires.
43
- import { toolForKey } from "../tools.js";
47
+ import { isInsertTool, specOf, toolForKey } from "../tools.js";
44
48
  import type { ShotTool } from "../tools.js";
45
49
  import type { StageLayout } from "../stage/panes.js";
50
+ // The layout table: the digit each of the three answers to, the words that name
51
+ // one inside a sentence, and the tail both pane refusals below share with the
52
+ // View menu and the segmented control.
53
+ import { layoutForEvent, layoutSpec, paneOnlyRefusal } from "./layouts.js";
54
+ import { formatCombo } from "./shortcuts.js";
55
+ // The digit for Video, from the same table the bar and the menu row read.
56
+ import { VIEW_MODE_SPECS } from "./viewMode.js";
57
+ import { VIEW_ONLY } from "./workspace.js";
58
+ import type { ViewMode } from "./viewMode.js";
59
+ import type { SketchTool } from "./types.js";
46
60
  import { ORBIT_STEP_DEG } from "../stage/workingView.js";
61
+ import type { AlignMode, FlipAxis, RestackTarget } from "@dailies-studio/shot/doc";
47
62
 
48
63
  export type KeyAction =
49
64
  | { kind: "tool"; tool: ShotTool }
@@ -51,10 +66,42 @@ export type KeyAction =
51
66
  | { kind: "redo" }
52
67
  | { kind: "save" }
53
68
  | { kind: "playToggle" }
69
+ /**
70
+ * SHUTTLE: one rung along `SHUTTLE_RATES`, and run.
71
+ *
72
+ * `dir` is which way along the LADDER, never which way the picture goes: from
73
+ * −2 a `+1` is −1, which is still reverse and slower. The store owns the
74
+ * ladder because the rung depends on the rate the transport is already at,
75
+ * which this table deliberately does not take.
76
+ */
77
+ | { kind: "shuttle"; dir: -1 | 1 }
78
+ /** Stop, and put the rate back to 1. NOT `playToggle`: an author leaning on K
79
+ * means "stop", and a toggle would start the transport again on the second
80
+ * press of a key whose whole job is to end a shuttle. */
81
+ | { kind: "shuttleStop" }
54
82
  /** Move the playhead by whole frames. */
55
83
  | { kind: "step"; delta: number }
84
+ /** Move the playhead by whole SECONDS. Its own action and not a `step` with a
85
+ * bigger delta, because this table cannot know the fps: a second is
86
+ * `doc.fps` frames, and inventing a number here would be a second opinion on
87
+ * the document's own rate. `delta` is ±1 second. */
88
+ | { kind: "stepSeconds"; delta: -1 | 1 }
89
+ /** The playhead to the previous / next EDIT POINT: a shot boundary, a layer's
90
+ * lifespan edge, or a marker. The three places something in the film
91
+ * actually changes, which is what an author scanning a cut is looking for. */
92
+ | { kind: "jumpEdit"; dir: -1 | 1 }
93
+ /** The playhead to the selection's own in or out point. */
94
+ | { kind: "seekSelectionEdge"; edge: "in" | "out" }
95
+ /** Put one edge of the work area at the playhead. */
96
+ | { kind: "setWorkArea"; edge: "in" | "out" }
97
+ /** Drop the work area: playback goes back to looping the whole document. */
98
+ | { kind: "clearWorkArea" }
56
99
  /** Move the selection, in design px. */
57
100
  | { kind: "nudge"; dx: number; dy: number }
101
+ | { kind: "nudgeKeys"; delta: number }
102
+ | { kind: "jumpKeyframe"; dir: -1 | 1 }
103
+ | { kind: "copyKeyframes" }
104
+ | { kind: "pasteKeyframes" }
58
105
  | { kind: "fit" }
59
106
  /** Fit the whole document across the timeline dock. A SECOND fit and not an
60
107
  * arm of the first: `⇧Z` addresses the active stage pane, and the dock is
@@ -62,28 +109,84 @@ export type KeyAction =
62
109
  | { kind: "fitTimeline" }
63
110
  | { kind: "zoom"; direction: 1 | -1 }
64
111
  | { kind: "zoomReset" }
65
- /** How many panes the stage shows. `toggle` means "…and back again if that
66
- * is where we already are", which is what numpad-0 needs and what the
67
- * 1 / 2 / 3 row deliberately does NOT: pressing 2 twice must not leave the
68
- * split. */
112
+ /** WHICH EDITOR the stage shows, and over how much of it. `toggle` means
113
+ * "…and back again if that is where we already are", which is what numpad-0
114
+ * needs and what the ⇧1 / ⇧2 / ⇧3 row deliberately does NOT: pressing ⇧2
115
+ * twice must not leave the split. Asking for any of the three also ends a
116
+ * screening. See `setViewLayout`. */
69
117
  | { kind: "stageLayout"; mode: StageLayout["mode"]; toggle?: boolean }
118
+ /** WATCHING OR WORKING: ⇧4 goes to the finished picture, Escape's top rung
119
+ * comes back. Not a layout. It is drawn OVER whichever of the three the
120
+ * author left, and hands it back untouched. */
121
+ | { kind: "viewMode"; mode: ViewMode }
70
122
  /** Point the shot camera where the working pane is looking. */
71
123
  | { kind: "alignCameraToView" }
124
+ /** PILOT the shot camera from the Camera pane, or stop: Num0. Blender's
125
+ * numpad-0 looks through the camera; ours also puts the author's hand on
126
+ * it, and the badge over the pane with its ESC is what keeps that honest. */
127
+ | { kind: "pilotToggle" }
128
+ /** Escape's first rung while piloting: hands are off the camera, which stays
129
+ * where it is. */
130
+ | { kind: "pilotEject" }
72
131
  /** Drop a marker at the playhead. */
73
132
  | { kind: "addMarker" }
74
133
  /** Hide a side rail, or every panel at once. */
75
134
  | { kind: "togglePanel"; panel: "rail" | "inspector" | "all" }
135
+ /**
136
+ * Open the app menu, with its search field focused.
137
+ *
138
+ * The only action in this table whose effect is CHROME rather than document or
139
+ * workspace state, which is why the store answers it by parking a mailbox
140
+ * (`appMenu`) rather than by doing anything: a keydown listener on the window
141
+ * cannot reach a component's popover, and a second listener inside that
142
+ * component is the thing this file's header forbids.
143
+ */
144
+ | { kind: "appMenu" }
145
+ /** Put the caret in the left column's search box. A mailbox for `appMenu`'s
146
+ * reason: only the panel holds the input. */
147
+ | { kind: "focusSearch" }
76
148
  | { kind: "animateToggle" }
77
149
  | { kind: "openExport" }
78
150
  | { kind: "deleteSelection" }
151
+ /** Delete the selected layers AND pull everything after them back, closing
152
+ * the gap they leave. Its own action and not a flag on `deleteSelection`,
153
+ * because plain delete leaving a gap is the deliberate answer: a gap shows
154
+ * the scene background, which is a picture in this product. */
155
+ | { kind: "rippleDelete" }
79
156
  | { kind: "duplicate" }
157
+ /** The selected layers onto the layer clip (`store.copyLayers`), and the
158
+ * same plus a delete; the paste lands copies in their Shot. */
159
+ | { kind: "copyLayers" }
160
+ | { kind: "cutLayers" }
161
+ | { kind: "pasteLayers" }
162
+ /** The motion clip (`store.copyAnimation` / `copyEffects`) and its pastes
163
+ * onto the selection. Animation pastes at the playhead from the keyboard;
164
+ * "At original time" is a menu row only. */
165
+ | { kind: "copyAnimation" }
166
+ | { kind: "pasteAnimation" }
167
+ | { kind: "copyEffects" }
168
+ | { kind: "pasteEffects" }
80
169
  | { kind: "selectAll" }
81
170
  | { kind: "deselect" }
171
+ /** THE RAZOR: cut the layers under the playhead. See `store.splitAtPlayhead`
172
+ * for the subject ladder it walks. */
82
173
  | { kind: "splitAtPlayhead" }
174
+ /** Cut the SHOT in two at the playhead. A separate verb and a separate key
175
+ * because it is a different subject, not a different mode of the razor: one
176
+ * cuts content inside a shot, the other cuts the shot itself and moves every
177
+ * master frame after it. */
178
+ | { kind: "splitSceneAtPlayhead" }
83
179
  | { kind: "group" }
84
180
  | { kind: "ungroup" }
181
+ /** ARRANGE the selection (`store.restack`): each layer among its own
182
+ * siblings, which is paint order. The bracket chords, see `arrangeChord`. */
183
+ | { kind: "arrange"; to: RestackTarget }
184
+ /** An Arrange chord over the storyboard or the material graph, CLAIMED and
185
+ * dropped: nothing there stacks, and the layer selection under the canvas
186
+ * is not the subject (the lanes' own rule), but on a Mac ⌘[ and ⌘] would
187
+ * otherwise be the browser's Back and Forward, out of the editor. */
188
+ | { kind: "arrangeNowhere" }
85
189
  | { kind: "toggleLock" }
86
- | { kind: "toggleHidden" }
87
190
  /** Finish the path the pen is drawing, keeping it. */
88
191
  | { kind: "penCommit" }
89
192
  /** Throw away the path the pen is drawing. */
@@ -92,13 +195,25 @@ export type KeyAction =
92
195
  | { kind: "enterIsolation" }
93
196
  /** Come back up ONE level. */
94
197
  | { kind: "exitIsolation" }
198
+ /** Back to the Shot a door into Components came from: its Shot, frame and
199
+ * selection. The bar's "Back to Shot" makes the same move by click. */
200
+ | { kind: "backToShot" }
201
+ | { kind: "closeComponentContext" }
202
+ /** Leave the code studio. The panel owns the confirm when the buffer is
203
+ * dirty, because only the panel can ask. */
204
+ | { kind: "closeCodeComponent" }
205
+ /** Leave Components while it shows no component (its empty state). */
206
+ | { kind: "leaveComponents" }
207
+ /** Back out of the entered shot, to the scene context. */
208
+ | { kind: "leaveScene" }
95
209
  /** Begin a Blender-style modal transform in the working pane. The stage
96
210
  * decides the TARGET (selection = layers, empty = the camera); the action
97
211
  * only carries which transform the letter named. */
98
212
  | { kind: "modalStart"; mode: "move" | "rotate" | "scale" }
99
- /** Constrain the live modal transform to one axis; the same axis again
100
- * clears it (the reducer owns that toggle, not this table). */
101
- | { kind: "modalAxis"; axis: "x" | "y" | "z" }
213
+ /** Constrain the live modal transform to one axis — or, with `plane`, to the
214
+ * PLANE normal to it (Blender's `⇧X`). The same constraint again clears it
215
+ * (the reducer owns that toggle, not this table). */
216
+ | { kind: "modalAxis"; axis: "x" | "y" | "z"; plane: boolean }
102
217
  /** One character typed into the live transform's numeric buffer: a digit, a
103
218
  * decimal point, or `-` to flip the sign (Blender's own rule). `G X 200
104
219
  * Enter` is what this exists for. */
@@ -133,15 +248,110 @@ export type KeyAction =
133
248
  /**
134
249
  * Hide explicitly, rather than toggling `selection[0]`'s own flag.
135
250
  *
136
- * `"selection"` hides what is selected, `"others"` hides everything else
137
- * (⇧H), `"reveal"` unhides the whole scene (⌥H). The toggle had no way back
251
+ * `"selection"` hides what is selected (H), `"others"` hides everything else
252
+ * (⌥⇧H), `"reveal"` unhides the whole scene (⌘⇧H). The toggle had no way back
138
253
  * once the hidden layers were deselected, which is most of the time.
139
254
  */
140
255
  | { kind: "setHidden"; scope: "selection" | "others" | "reveal" }
141
- /** ⌥G / ⌥R / ⌥S: back to the layer's own zero on one channel. */
256
+ /** ⌥G / ⌥R / ⌥⇧S: back to the layer's own zero on one channel. */
142
257
  | { kind: "clearTransform"; channel: "offset" | "rotation" | "scale" }
258
+ /** ⌥A / ⌥D / ⌥W / ⌥S / ⌥H / ⌥V: the six alignments (`store.alignSelection`). */
259
+ | { kind: "align"; mode: AlignMode }
260
+ /** A bare digit with a selection: opacity in tenths, and 0 is full. */
261
+ | { kind: "opacity"; value: number }
262
+ /** Q / W: trim the selection's in or out point to the playhead. W keeps the
263
+ * playhead's own frame, so the out lands one past it. */
264
+ | { kind: "trimToPlayhead"; edge: "in" | "out" }
265
+ /** ⇧H / ⇧V: mirror the selection on the screen's x or y (Figma's keys). */
266
+ | { kind: "flip"; axis: FlipAxis }
143
267
  /** ⇧Z: wireframe on, and back to whatever the shading was. Blender's. */
144
- | { kind: "wireframeToggle" };
268
+ | { kind: "wireframeToggle" }
269
+ /** ⌥Z: the working pane's grid floor on / off. The alt lane is where this
270
+ * chrome keeps the verbs whose letter is already spoken for, and the grid
271
+ * is the Z family's overlay (Blender keeps its grid behind ⇧⌥Z's overlay
272
+ * toggle; this stage has exactly one overlay floor, so one key does). */
273
+ | { kind: "gridToggle" }
274
+ /** ⌥M: the dock's Sound face on / off (gap 8). M for mixer; bare M drops a
275
+ * marker, so the mixer takes the alt lane. */
276
+ | { kind: "soundMode" }
277
+ /** ⇧R: the camera pane's rulers on / off (Paper page 24). Figma's and
278
+ * Photoshop's key family; the plain letter stays rotate / rect. */
279
+ | { kind: "rulersToggle" }
280
+ /** ⌘, : the Preferences dialog, every Mac app's key for it. */
281
+ | { kind: "openPrefs" }
282
+ // ── THE STORYBOARD'S OWN LANE ────────────────────────────────────────────
283
+ //
284
+ // Prefixed `storyboard`, produced only with `storyboard: true` (or, for the
285
+ // toggle, from the ordinary shell), for the graph lane's reason: Delete over
286
+ // a card must remove the card, never the layer the shell still has selected
287
+ // underneath the board.
288
+ | { kind: "storyboardToggle" }
289
+ | { kind: "storyboardDelete" }
290
+ | { kind: "storyboardSelectAll" }
291
+ | { kind: "storyboardEscape" }
292
+ | { kind: "storyboardDuplicate" }
293
+ | { kind: "storyboardRename" }
294
+ // COMMENTS (plan C5, S5): C comments on the board card selected or, off
295
+ // the board, arms the comment tool on the Studio's camera pane. N and
296
+ // Shift+N walk the open threads in board order, on the board only.
297
+ | { kind: "storyboardComment" }
298
+ | { kind: "storyboardNextThread" }
299
+ | { kind: "storyboardPrevThread" }
300
+ /**
301
+ * ⌘C / ⌘X / ⌘V ON THE BOARD, AND ITS ARM RETURNS `false`. That is the whole
302
+ * design and it is not a stub.
303
+ *
304
+ * These three currently fall through to `null`, nothing is
305
+ * `preventDefault`ed, and THAT IS PRECISELY why the browser's own `paste`
306
+ * event still reaches the board and lands image and video FILES from the
307
+ * system clipboard. Binding them to a handled action would make `handleKey`
308
+ * answer true, which preventDefaults the keydown, which cancels the paste
309
+ * event, which is the only thing carrying `clipboardData.files`. The
310
+ * capability would disappear and nothing would say so.
311
+ *
312
+ * So the document handlers in StoryboardView.tsx do the work, and these rows exist
313
+ * only so the three combos are documented and `shortcutFor` can print them in
314
+ * a menu. `ShortcutRow.action` is required, so a documented combo must
315
+ * resolve to something; resolving to an arm that returns `false` keeps
316
+ * behaviour byte-identical.
317
+ *
318
+ * A test asserts the arm returns false. It fails the moment someone tidies
319
+ * this to `true`, which is the point.
320
+ */
321
+ | { kind: "storyboardClipboard" }
322
+ // ── THE MATERIAL GRAPH'S OWN LANE ────────────────────────────────────────
323
+ //
324
+ // Every one of these is prefixed `graph` and every one of them is produced
325
+ // ONLY with `materialGraph: true`. That is not decoration: `graphDelete` and
326
+ // `deleteSelection` are the same key over two different subjects, and a lane
327
+ // that answered the layer verb inside the canvas would delete the MESH the
328
+ // author is painting rather than the node under their cursor.
329
+ /** Open the node palette. */
330
+ | { kind: "graphPalette" }
331
+ /** Remove the selected nodes and wires. */
332
+ | { kind: "graphDelete" }
333
+ | { kind: "graphDuplicate" }
334
+ | { kind: "graphCopy" }
335
+ | { kind: "graphCut" }
336
+ | { kind: "graphPaste" }
337
+ | { kind: "graphSelectAll" }
338
+ /** Box the selection, and take it back out again. */
339
+ | { kind: "graphGroup" }
340
+ | { kind: "graphUngroup" }
341
+ /** Go INTO the selected Subgraph node's function body. */
342
+ | { kind: "graphEnter" }
343
+ /** Show or hide the live thumbnail on the selected cards. */
344
+ | { kind: "graphPreviewToggle" }
345
+ /** Fit the whole graph in the canvas. */
346
+ | { kind: "graphFrame" }
347
+ | { kind: "graphTidy" }
348
+ /** Move the selected nodes, in GRAPH px. */
349
+ | { kind: "graphNudge"; dx: number; dy: number }
350
+ /** Escape, whose meaning inside the workspace is a four-rung ladder the
351
+ * STORE owns — the palette, then the selection, then one function level,
352
+ * then the workspace itself. The table cannot answer it, because three of
353
+ * the four rungs are state this file deliberately does not take. */
354
+ | { kind: "graphEscape" };
145
355
 
146
356
  /** What the key event lands in. Only what actually changes the ANSWER — a
147
357
  * keymap that took the whole store would be a keymap nobody could test. */
@@ -149,6 +359,31 @@ export interface KeyContext {
149
359
  /** The event target is a field, a textarea or a contenteditable. */
150
360
  editing: boolean;
151
361
  hasSelection: boolean;
362
+ /**
363
+ * At least one KEYFRAME is selected.
364
+ *
365
+ * A subject of its own, above the layer selection on the rungs that both
366
+ * could claim: the arrows nudge keys when there are keys, and layers
367
+ * otherwise. That is `deleteSelection`'s own most-specific-subject-wins
368
+ * logic, applied to the other gesture that has two possible targets.
369
+ *
370
+ * Absent counts as false, so every caller that predates keyframes keeps
371
+ * today's answers. */
372
+ hasKeyframeSelection?: boolean;
373
+ /** Sound mode has clips picked (`selectedAudio`): Q / W, Escape and Delete
374
+ * answer to them. */
375
+ hasAudioSelection?: boolean;
376
+ /** Something is on the keyframe clipboard. Gates ⌘V so that with an empty
377
+ * clipboard the key falls through to the browser rather than being
378
+ * swallowed by a verb with nothing to do. Absent = false. */
379
+ hasKeyframeClip?: boolean;
380
+ /** Something is on the layer clip, which gates ⌘V the same way. Absent =
381
+ * false. */
382
+ hasLayerClip?: boolean;
383
+ /** An animation, or a layer's effects, is on the motion clip. Gates ⌥⌘V and
384
+ * ⇧⌥⌘V the way `hasLayerClip` gates ⌘V. Absent = false. */
385
+ hasAnimationClip?: boolean;
386
+ hasEffectsClip?: boolean;
152
387
  /**
153
388
  * The pen HOLDS AT LEAST ONE POINT — not "the path tool is armed".
154
389
  *
@@ -166,19 +401,82 @@ export interface KeyContext {
166
401
  /** The author is inside an isolated subtree. Only Escape's precedence reads
167
402
  * it, and only once nothing is selected — see the lane. Absent = false. */
168
403
  isolation?: boolean;
404
+ /** A component DEFINITION is open (the outline is scoped to its interior).
405
+ * A rung on Escape's ladder, so the gesture that opened a component context
406
+ * can close all of it. Absent = false. */
407
+ editingDef?: boolean;
408
+ /** Components was opened from a Shot (`componentsOrigin`), so Escape goes
409
+ * back to that Shot, frame and selection. Absent = false. */
410
+ componentsOrigin?: boolean;
411
+ /** THE STORYBOARD IS OPEN. Mode-level, unlike `materialGraph`: there is no
412
+ * working stage pane beside this canvas (the preview is a monitor that
413
+ * takes its own pointer), and a key pressed over the inspector must still
414
+ * delete a card. Absent = false. */
415
+ storyboard?: boolean;
416
+ /** A CODE component is open (its source beside its own render). The other
417
+ * family's rung, at the same height as the definition's because both put
418
+ * the stage in the component context and neither can nest in the other.
419
+ * Absent = false. */
420
+ codeComponent?: boolean;
421
+ /** Components is up with no component open (its empty state). Escape
422
+ * leaves it, at the subjects' height. Absent = false. */
423
+ componentsOpen?: boolean;
424
+ /** A SHOT is open (the author is authoring its elements rather than
425
+ * arranging shots). Escape's LAST rung, below the definition, because the
426
+ * component context nests inside the shot context and Escape climbs out one
427
+ * level per press. Absent = false. */
428
+ enteredScene?: boolean;
429
+ /**
430
+ * THE AUTHOR IS PILOTING THE SHOT CAMERA from the Camera pane.
431
+ *
432
+ * Escape's first rung of all: a mode the author armed most recently, and
433
+ * one whose badge promises "ESC TO EJECT". Video and pilot never coexist
434
+ * (entering Video ejects), so the order between the two rungs is moot.
435
+ * Absent = false.
436
+ */
437
+ pilot?: boolean;
438
+ /**
439
+ * THE AUTHOR IS WATCHING: Video is on screen (`viewMode.ts`'s `videoView`).
440
+ *
441
+ * Escape's TOP rung, above the deselect, because the selection SURVIVES a
442
+ * screening: it is invisible in Video and comes back with the handles, so
443
+ * an author who watched their shot and pressed Escape means "put the editor
444
+ * back", not "drop what I had selected". Absent = false.
445
+ */
446
+ video?: boolean;
169
447
  /**
170
448
  * The CURSOR IS OVER THE WORKING PANE and a modal transform could start.
171
449
  *
172
450
  * Stage-side callers only: the chrome has no cursor-over-pane answer, and
173
- * passing nothing keeps G arming the move tool from a panel exactly as it
174
- * always has. Cursor position and not `activePane`, because a modal needs an
451
+ * passing nothing leaves G unbound from a panel. Cursor position and not `activePane`, because a modal needs an
175
452
  * anchor point and activePane is only written on press/wheel — the mouse can
176
453
  * hover one pane while the other is active. Absent = false.
177
454
  */
178
455
  workingPane?: boolean;
456
+ /**
457
+ * The working pane under the cursor is also the FRAME being edited: an open
458
+ * definition's one pane (StageApp's `framePane`). A letter a tool claims
459
+ * keeps its tool there, as it does over the camera pane, so R arms Rect
460
+ * while G and S keep their modals. Stage-side only, like `workingPane`.
461
+ * Absent = false.
462
+ */
463
+ framePane?: boolean;
179
464
  /** A modal transform is LIVE. Owns its terminator and axis keys, the way
180
465
  * `penActive` owns Enter/Escape. Absent = false. */
181
466
  transformModal?: boolean;
467
+ /**
468
+ * THE KEY LANDED INSIDE THE MATERIAL GRAPH CANVAS.
469
+ *
470
+ * Measured from the event's target (`closest(".material-graph-canvas")`) and
471
+ * not from "the workspace is open", which is the distinction the whole lane
472
+ * rests on: the workspace shows a stage pane BESIDE the graph, and a key
473
+ * pressed over that pane still aims the working view. Only the canvas itself
474
+ * owns the graph verbs.
475
+ *
476
+ * Absent = false, so every caller and probe that predates the lane keeps
477
+ * today's answers.
478
+ */
479
+ materialGraph?: boolean;
182
480
  }
183
481
 
184
482
  export interface KeyEventLike {
@@ -187,9 +485,16 @@ export interface KeyEventLike {
187
485
  altKey: boolean;
188
486
  ctrlKey: boolean;
189
487
  metaKey: boolean;
190
- /** `KeyboardEvent.code`, for the numpad lane. Optional so every caller and
191
- * probe that predates it keeps today's answers; absent = no numpad. */
488
+ /** `KeyboardEvent.code`: the numpad lane, the shifted layout digits, and
489
+ * the chord letters when `key` is composed (see `chordChar`). Optional so
490
+ * every caller and probe that predates it keeps today's answers; absent =
491
+ * no numpad, and no shifted-digit layout key. */
192
492
  code?: string;
493
+ /** `KeyboardEvent.repeat`: the key is held and this keydown is the OS
494
+ * repeating it. The table answers a repeat as it answers the press; the
495
+ * play toggle ignores it, so a held Space toggles once (the store's
496
+ * `handleKey`). Absent = a press. */
497
+ repeat?: boolean;
193
498
  }
194
499
 
195
500
  /** How far a plain arrow moves a layer, and how far a shifted one does. Design
@@ -197,21 +502,89 @@ export interface KeyEventLike {
197
502
  * the view would be a different edit depending on how far you were zoomed in. */
198
503
  export const NUDGE_PX = 1;
199
504
  export const NUDGE_COARSE_PX = 10;
505
+ /** Shift's multiplier on a keyframe nudge, in FRAMES. Ten, the same relationship
506
+ * the layer nudge has — but frames rather than px, because a keyframe moves in
507
+ * time and a design pixel means nothing on that axis. */
508
+ export const NUDGE_COARSE_FRAMES = 10;
200
509
 
201
510
  /**
202
- * The view-mode digits, LEFT TO RIGHT as the toolbar draws them.
511
+ * …and the same pair for the GRAPH canvas, which is a different space.
203
512
  *
204
- * Keyed on `event.key` and not on a folded letter, which is what keeps them
205
- * bare-only for free: shift-1 reports "!" on every layout that has one, so a
206
- * shifted digit falls through to the tool lane and finds nothing rather than
207
- * needing a guard. The modifier lanes above have already returned by here, so
208
- * ⌘1 stays the browser's tab switch.
513
+ * A node lives in canvas px, where one px is invisible: the cards are ~180 px
514
+ * wide and the grid they land on is 16. So the fine step is half a grid cell,
515
+ * which is the smallest nudge that reads as a move, and the coarse one is ten
516
+ * of those — the same 10x relationship the layer nudge has, so the modifier
517
+ * means one thing in both places.
209
518
  */
210
- const LAYOUT_KEYS: Readonly<Record<string, StageLayout["mode"]>> = {
211
- "1": "working",
212
- "2": "split",
213
- "3": "camera",
214
- };
519
+ export const GRAPH_NUDGE_PX = 8;
520
+ export const GRAPH_NUDGE_COARSE_PX = 80;
521
+
522
+ /**
523
+ * The Project context's refusal for anything that acts on elements, in one
524
+ * sentence: a tool "draws" them, a rail button that inserts without a drag
525
+ * "adds" them. The keys and the board's tool rail both say it this way.
526
+ */
527
+ export function sceneContextRefusal(label: string, verb: string): string {
528
+ return `${label} ${verb} elements, and elements live inside a shot. Open a shot from the timeline to add one to it.`;
529
+ }
530
+
531
+ /**
532
+ * Why this tool's key does nothing in this layout, or null when it works.
533
+ *
534
+ * The seven creating tools draw on the frame, which over a Shot is the CAMERA
535
+ * pane and nowhere else (see `isInsertTool`), so in the working-only layout
536
+ * their letters armed a tool that no press could reach. That is worse than a
537
+ * dead key: with the tool armed, a press in the working pane falls through the
538
+ * stage's generic lanes and ORBITS the view, deselects, or drags whatever was
539
+ * selected, so the author sees the tool do the wrong thing rather than
540
+ * nothing, and has no reason to suspect the layout.
541
+ *
542
+ * `mode` is the layout the tools are judged against, which every caller reads
543
+ * through `toolLayoutMode`: inside a definition the stage's one pane is both
544
+ * the frame and the 3D view, so every tool's pane is on screen.
545
+ *
546
+ * A MESSAGE rather than a boolean, and pure: the store owns `state.error` (this
547
+ * is `noWorkingPane`'s job the other way round, and it names the digit to press
548
+ * for the same reason), and the tool dock puts the identical sentence in the
549
+ * disabled button's title — so what the key says and what the button says about
550
+ * where a tool works cannot drift apart.
551
+ */
552
+ export function toolRefusal(
553
+ tool: ShotTool,
554
+ mode: StageLayout["mode"],
555
+ /** The author is in the SCENE context — arranging shots, not authoring the
556
+ * elements of one. Absent = false, so every caller that predates the two
557
+ * contexts keeps today's answers. */
558
+ sceneContext = false,
559
+ /** The Project is view only for this person: true, or the sentence that
560
+ * says why (`readOnlyReason`). Absent = false. */
561
+ readOnly: boolean | string = false,
562
+ /** Why the isolated layer cannot take an insert (`isolationInsertRefusal`),
563
+ * or null. True in every layout, so it outranks the pane answer. */
564
+ isolation: string | null = null,
565
+ ): string | null {
566
+ if (!isInsertTool(tool)) return null;
567
+ // Every creating tool writes the document, so a viewer has none of them.
568
+ if (readOnly !== false) return readOnly === true ? VIEW_ONLY : readOnly;
569
+ // THE CONTEXT ANSWER COMES FIRST, because it is the one that is true whatever
570
+ // the layout is: elements live inside a shot, so no layout up here makes a
571
+ // creating tool legal, and a layout sentence would send the author to a
572
+ // digit that would not help.
573
+ if (sceneContext) return sceneContextRefusal(specOf(tool).label, "draws");
574
+ if (isolation !== null) return isolation;
575
+ const pane = specOf(tool).pane;
576
+ // The lead clause is this tool's own; the tail is the one every "this layout
577
+ // cannot do that" refusal shares (`paneOnlyRefusal`), so the digits it names
578
+ // follow a rebind and the words follow a rename with no edit here. Both arms
579
+ // have narrowed `mode` to a single pane by the time they ask.
580
+ if (pane === "camera" && mode === "working") {
581
+ return formatCombo(`${specOf(tool).label} draws in ${layoutSpec(pane).noun}, and ${paneOnlyRefusal(mode)}`);
582
+ }
583
+ if (pane === "working" && mode === "camera") {
584
+ return formatCombo(`${specOf(tool).label} draws in ${layoutSpec(pane).noun}, and ${paneOnlyRefusal(mode)}`);
585
+ }
586
+ return null;
587
+ }
215
588
 
216
589
  const ARROWS: Readonly<Record<string, readonly [number, number]>> = {
217
590
  ArrowLeft: [-1, 0],
@@ -220,7 +593,72 @@ const ARROWS: Readonly<Record<string, readonly [number, number]>> = {
220
593
  ArrowDown: [0, 1],
221
594
  };
222
595
 
223
- export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction | null {
596
+ /**
597
+ * ARRANGE ON THE BRACKET KEYS, Figma's chords (founder decision 3, critic 5).
598
+ *
599
+ * ⌘] / Ctrl+] brings the selection forward one sibling and ⌘[ / Ctrl+[ sends
600
+ * it backward, on every platform. To the FRONT and BACK the chord splits by
601
+ * platform, as Figma's does: Ctrl+⇧ on Windows and Linux, ⌥⌘ on a Mac. ⌘⇧]
602
+ * and ⌘⇧[ are the Mac's next and previous TAB, so they are never claimed: they
603
+ * answer null and the browser keeps them. The split reads the modifier itself
604
+ * (meta is a Mac's command key, and Windows never delivers its own) rather
605
+ * than the platform, so this table stays a pure function of the event.
606
+ *
607
+ * Keyed on `event.code`, never `key`: Shift makes `]` a `}` and Option makes it
608
+ * a quote on a Mac, so the character is no witness to the key. ⌘[ and ⌘] ARE
609
+ * taken from Chrome's Back and Forward on a Mac, as Figma takes them, and
610
+ * `handleKey` claims them like ⌘K. The bare brackets stay the panel toggles,
611
+ * in lane 3.
612
+ */
613
+ function arrangeChord(event: KeyEventLike): KeyAction | null {
614
+ const side =
615
+ event.code === "BracketRight" ? 1 : event.code === "BracketLeft" ? -1 : 0;
616
+ if (side === 0) return null;
617
+ const { shiftKey: shift, altKey: alt, ctrlKey: ctrl, metaKey: meta } = event;
618
+ if (!shift && !alt)
619
+ return { kind: "arrange", to: side > 0 ? "forward" : "backward" };
620
+ const macFront = meta && alt && !shift && !ctrl;
621
+ const pcFront = ctrl && shift && !alt && !meta;
622
+ if (macFront || pcFront)
623
+ return { kind: "arrange", to: side > 0 ? "front" : "back" };
624
+ return null;
625
+ }
626
+
627
+ /**
628
+ * An Arrange chord over a canvas with nothing to arrange (the storyboard, the
629
+ * material graph, lanes 1e and 1f): CLAIMED and dropped, so ⌘[ and ⌘] are not
630
+ * the browser's Back and Forward there either. A character lane 2a keeps for
631
+ * itself before it asks `arrangeChord` (a letter, a digit, the zoom keys: a
632
+ * layout can put `+` or `=` on a bracket key) is left to the browser, as every
633
+ * combo those lanes do not bind.
634
+ */
635
+ function arrangeNowhere(event: KeyEventLike, lower: string): KeyAction | null {
636
+ if (/^[a-z0-9=+-]$/.test(lower)) return null;
637
+ return arrangeChord(event) === null ? null : { kind: "arrangeNowhere" };
638
+ }
639
+
640
+ /**
641
+ * The letter or digit a ctrl or alt chord names.
642
+ *
643
+ * A plain one in `key` wins, so a non-QWERTY layout keeps its own letters
644
+ * (AZERTY's ⌘A is the key printed A). When `key` is NOT a plain letter or digit
645
+ * the physical key answers instead: a Mac's Option composes it ("ç" for ⌥⌘C,
646
+ * "¬" for ⌥L, a dead key for ⌥I), Shift turns a digit into punctuation, and a
647
+ * Cyrillic layout reports "я" on the key every shortcut chart calls Z.
648
+ * Anything else (`=`, `+`, `-`, a bracket) comes back as it was typed.
649
+ */
650
+ function chordChar(event: KeyEventLike, lower: string): string {
651
+ if (/^[a-z0-9]$/.test(lower)) return lower;
652
+ const code = event.code ?? "";
653
+ if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
654
+ if (/^Digit[0-9]$/.test(code)) return code.slice(5);
655
+ return lower;
656
+ }
657
+
658
+ export function resolveKey(
659
+ event: KeyEventLike,
660
+ context: KeyContext,
661
+ ): KeyAction | null {
224
662
  // 1. A field owns its own keystrokes. Every editor that has ever swallowed
225
663
  // Space while the author was naming something shipped that bug by checking
226
664
  // the KEY before the TARGET.
@@ -230,6 +668,21 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
230
668
  const lower = key.length === 1 ? key.toLowerCase() : key;
231
669
  const mod = event.metaKey || event.ctrlKey;
232
670
 
671
+ // 1a. WINDOWS ALTGR IS NOT A CHORD. AltGr arrives as ctrlKey + altKey, so a
672
+ // Polish author typing "ć" or a German one typing "@" or "€" would
673
+ // otherwise reach the ctrl lane and fire whatever the physical key's
674
+ // letter is bound to. A composed character (one that is not a plain
675
+ // letter or digit) is typing, never a shortcut. A Mac's ⌥⌘ carries
676
+ // metaKey and is a real chord, so it passes.
677
+ if (
678
+ event.ctrlKey &&
679
+ event.altKey &&
680
+ !event.metaKey &&
681
+ key.length === 1 &&
682
+ !/^[a-z0-9]$/i.test(key)
683
+ )
684
+ return null;
685
+
233
686
  // 1b. A PATH IN PROGRESS owns Enter and Escape, and only those two.
234
687
  //
235
688
  // Above every other lane because both keys already mean something here
@@ -244,9 +697,14 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
244
697
 
245
698
  // 1c. A LIVE MODAL TRANSFORM owns everything bare.
246
699
  //
247
- // Enter/Escape terminate it and x/y/z constrain it (shift allowed —
248
- // shift is the live precision modifier, so ⇧Z mid-modal is still the z
249
- // axis, not fit). Every OTHER bare key answers null so no tool can arm
700
+ // Enter/Escape terminate it and x/y/z constrain it. SHIFT IS PART OF THE
701
+ // CONSTRAINT: `⇧X` locks to the PLANE normal to x — Blender's own
702
+ // binding, and the one thing a keyboard-only author could not say here
703
+ // (the drawn gizmo grew plane pads for it; this is the same constraint on
704
+ // the keys). It used to fall through as "still the z axis, not fit",
705
+ // which was right while there was no plane to mean instead. Shift keeps
706
+ // its precision job on MOTION, which is a different event entirely.
707
+ // Every OTHER bare key answers null so no tool can arm
250
708
  // and no panel can toggle under an open scrub. Modifier combos fall
251
709
  // through on purpose: the table stays honest about what ⌘Z means — the
252
710
  // stage's capture listener is the one that swallows it while the modal
@@ -255,25 +713,28 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
255
713
  //
256
714
  // ALT FALLS THROUGH, and `mod` alone does not cover it: `mod` is
257
715
  // ctrl/meta, so ⌥G reached this lane and RESTARTED the modal instead of
258
- // reaching lane 2b's `clearTransform` (Blender's ⌥G / ⌥R / ⌥S, "clear
716
+ // reaching lane 2b's `clearTransform` (Blender's ⌥G / ⌥R, and ⌥⇧S, "clear
259
717
  // this channel"), and ⌥X set an axis. The alt bindings are a different
260
718
  // verb that happens to share a letter, so the lane has to let them past.
261
719
  if (context.transformModal === true && !mod && !event.altKey) {
262
720
  if (key === "Enter") return { kind: "modalConfirm" };
263
721
  if (key === "Escape") return { kind: "modalCancel" };
264
- if (lower === "x") return { kind: "modalAxis", axis: "x" };
265
- if (lower === "y") return { kind: "modalAxis", axis: "y" };
266
- if (lower === "z") return { kind: "modalAxis", axis: "z" };
722
+ if (lower === "x")
723
+ return { kind: "modalAxis", axis: "x", plane: event.shiftKey === true };
724
+ if (lower === "y")
725
+ return { kind: "modalAxis", axis: "y", plane: event.shiftKey === true };
726
+ if (lower === "z")
727
+ return { kind: "modalAxis", axis: "z", plane: event.shiftKey === true };
267
728
  // SWITCHING TRANSFORM MID-GESTURE, which in Blender feels like a mode
268
729
  // change rather than a restart: `G` then `R` keeps the anchor the author
269
730
  // started from, so the rotation is measured from where they pressed and not
270
731
  // from wherever the pointer had wandered to.
271
732
  if (lower === "g") return { kind: "modalStart", mode: "move" };
272
- // `R` NEEDS A SELECTION, exactly as the starter lane below does and for the
273
- // same reason: the CAMERA has no modal roll. Without the guard, switching
274
- // to rotate on a camera modal armed a transform that could never emit,
275
- // under a pill that still said CAMERA MOVE.
276
- if (lower === "r" && context.hasSelection) return { kind: "modalStart", mode: "rotate" };
733
+ // `R` NEEDS NO SELECTION ANY MORE: the camera has a modal roll, so
734
+ // switching to rotate mid-gesture is symmetric with `G` and `S` whichever
735
+ // subject the modal is on. `framingDeltaFor`'s rotate arm is what it emits
736
+ // into, and `axisRefusal` names the two axes a framing cannot turn about.
737
+ if (lower === "r") return { kind: "modalStart", mode: "rotate" };
277
738
  if (lower === "s") return { kind: "modalStart", mode: "scale" };
278
739
  // NUMERIC ENTRY: `G X 200 Enter`, which is the single biggest thing a
279
740
  // Blender user's fingers reach for and finds missing. It goes through the
@@ -330,20 +791,30 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
330
791
  Numpad2: { dx: 0, dy: 1 },
331
792
  };
332
793
  const one = step[event.code]!;
333
- return { kind: "orbitStep", dx: one.dx * ORBIT_STEP_DEG, dy: one.dy * ORBIT_STEP_DEG };
794
+ return {
795
+ kind: "orbitStep",
796
+ dx: one.dx * ORBIT_STEP_DEG,
797
+ dy: one.dy * ORBIT_STEP_DEG,
798
+ };
334
799
  }
335
800
  case "Numpad0":
336
- // A TOGGLE, matching Blender: numpad-0 goes to the camera and numpad-0
337
- // again comes back. Straight to `camera` meant an author who pressed it
338
- // twice was stuck there with no way back but the mouse.
339
- if (!mod) return { kind: "stageLayout", mode: "camera", toggle: true };
801
+ // PILOT, and back: Blender's numpad-0 looks through the camera, and
802
+ // ours puts the author's hands on it in the Camera pane. A toggle, so
803
+ // pressing it twice leaves the camera where it is and the hands off.
804
+ // The layouts keep ⇧1 / ⇧2 / ⇧3; the store brings the Camera pane on
805
+ // screen if the 3D layout has none.
806
+ if (!mod) return { kind: "pilotToggle" };
340
807
  break;
341
808
  case "NumpadAdd":
342
809
  case "NumpadSubtract":
343
810
  // The existing zoom verb, which already routes to the active pane —
344
811
  // so it dollies the working camera and zooms the flat one, correctly,
345
812
  // with no new action.
346
- if (!mod) return { kind: "zoom", direction: event.code === "NumpadAdd" ? 1 : -1 };
813
+ if (!mod)
814
+ return {
815
+ kind: "zoom",
816
+ direction: event.code === "NumpadAdd" ? 1 : -1,
817
+ };
347
818
  break;
348
819
  case "NumpadDivide":
349
820
  // LITERALLY Blender's local view. Ours is isolation, and the two mean
@@ -371,96 +842,353 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
371
842
  if (!mod && key.length === 1) return null;
372
843
  }
373
844
 
845
+ // 1e. THE MATERIAL GRAPH CANVAS owns its own vocabulary.
846
+ //
847
+ // Above the modifier lanes because ⌘D, ⌘G and ⌘A all mean something else
848
+ // in here, and BELOW the field lane because a node's inline number field
849
+ // is still a field. The lane answers `"pass"` for exactly the keys that
850
+ // have to keep meaning what they mean everywhere — undo, redo, save, the
851
+ // app menu, and the space bar — and `null` for everything it does not
852
+ // claim, which is what keeps a bare tool letter from arming the rect tool
853
+ // under a graph nobody could draw a rectangle in.
854
+ if (context.materialGraph === true) {
855
+ const graph = materialGraphKey(event, key, lower, mod);
856
+ if (graph !== "pass") return graph;
857
+ }
858
+
859
+ // 1f. THE STORYBOARD holds the canvas while it is open, on the graph lane's
860
+ // terms: pass what must keep meaning what it means everywhere, blank the
861
+ // rest, because nothing under the board can take a tool letter.
862
+ if (context.storyboard === true) {
863
+ const board = storyboardKey(event, key, lower, mod);
864
+ if (board !== "pass") return board;
865
+ }
866
+
374
867
  // 2a. ctrl / meta.
868
+ //
869
+ // EVERY CASE NAMES THE MODIFIERS IT OWNS, and a combo it does not own
870
+ // answers null. The lane used to switch on the letter alone, so ⌘⇧C
871
+ // copied, ⌥⌘S saved and Ctrl+Alt+D duplicated: every extra modifier was
872
+ // a second spelling of the bare chord, and no chord with one could ever
873
+ // be bound to anything else. A refused combo is null and not
874
+ // `arrangeChord`: a letter is never a bracket.
375
875
  if (mod) {
376
- switch (lower) {
876
+ const shift = event.shiftKey;
877
+ const alt = event.altKey;
878
+ const plain = !shift && !alt;
879
+ switch (chordChar(event, lower)) {
377
880
  case "z":
378
- return event.shiftKey ? { kind: "redo" } : { kind: "undo" };
881
+ if (alt) return null;
882
+ return shift ? { kind: "redo" } : { kind: "undo" };
379
883
  case "y":
380
- return { kind: "redo" };
884
+ return plain ? { kind: "redo" } : null;
885
+ // ── KEYFRAME COPY / PASTE, AND ONLY WHEN THERE IS ONE ─────────────
886
+ //
887
+ // Both fall through to the BROWSER when their subject is absent, which
888
+ // is why they answer `null` rather than a no-op verb: ⌘C with nothing
889
+ // keyframed must still copy whatever the page has selected, and a
890
+ // chrome that swallowed it would break the one shortcut every user
891
+ // trusts. The field lane returns above this, so text copy is untouched
892
+ // either way. Keys first, then layers, the most specific subject the way
893
+ // `deleteSelection` reads it. The store keeps one clip: a copy of either
894
+ // clears the other, so at most one of the two paste bits is set.
895
+ // ── THE MOTION CLIP ON ⌥ ──────────────────────────────────────────
896
+ //
897
+ // ⌥⌘C / ⌥⌘V carry a layer's animation and ⇧⌥⌘C / ⇧⌥⌘V its effects. Each
898
+ // needs a selected layer (and a paste a clip of its kind), else it is the
899
+ // browser's, the same rule as the plain pair below.
900
+ case "c":
901
+ if (alt) {
902
+ if (!context.hasSelection) return null;
903
+ return shift ? { kind: "copyEffects" } : { kind: "copyAnimation" };
904
+ }
905
+ if (!plain) return null;
906
+ if (context.hasKeyframeSelection === true) return { kind: "copyKeyframes" };
907
+ return context.hasSelection ? { kind: "copyLayers" } : null;
908
+ case "x":
909
+ if (!plain) return null;
910
+ if (context.hasKeyframeSelection === true) return null;
911
+ return context.hasSelection ? { kind: "cutLayers" } : null;
912
+ case "v":
913
+ if (alt) {
914
+ if (!context.hasSelection) return null;
915
+ if (shift) return context.hasEffectsClip === true ? { kind: "pasteEffects" } : null;
916
+ return context.hasAnimationClip === true ? { kind: "pasteAnimation" } : null;
917
+ }
918
+ if (!plain) return null;
919
+ if (context.hasKeyframeClip === true) return { kind: "pasteKeyframes" };
920
+ return context.hasLayerClip === true ? { kind: "pasteLayers" } : null;
381
921
  case "s":
382
- return { kind: "save" };
922
+ return plain ? { kind: "save" } : null;
383
923
  case "d":
384
- return { kind: "duplicate" };
924
+ return plain ? { kind: "duplicate" } : null;
385
925
  case "e":
386
926
  // Export. The one destination-producing verb in the whole chrome, so
387
927
  // it gets the letter rather than a chord.
388
- return { kind: "openExport" };
928
+ return plain ? { kind: "openExport" } : null;
389
929
  case "a":
390
- return { kind: "selectAll" };
930
+ // ⌘A selects all and ⌘⇧A drops it, the pair every design tool ships.
931
+ // ⌥A was the deselect until the alt lane was cleared for new verbs.
932
+ if (alt) return null;
933
+ return shift ? { kind: "deselect" } : { kind: "selectAll" };
934
+ case "h":
935
+ // ⌘⇧H shows everything again. The reveal moved here off ⌥H, which is
936
+ // left free; ⌘H alone is the Mac's "hide the app" and is never claimed.
937
+ return shift && !alt ? { kind: "setHidden", scope: "reveal" } : null;
938
+ case ",":
939
+ return plain ? { kind: "openPrefs" } : null;
940
+ case "k":
941
+ // The app menu, with its search focused — the ⌘K every tool now ships.
942
+ // It CLAIMS Chrome's own ⌘K (the address bar's search shortcut), which is
943
+ // deliberate and is why `handleKey` answers true: the alternative is a
944
+ // menu whose advertised key opens the browser instead.
945
+ return plain ? { kind: "appMenu" } : null;
391
946
  case "b":
392
- // Split at the playhead, and the ONLY splitter now that the cut tool is
393
- // gone — see the header.
394
- return { kind: "splitAtPlayhead" };
947
+ // THE TWO SPLITTERS, and the only ones now that the cut tool is gone —
948
+ // see the header. ⇧ picks the bigger subject, the same way it does on
949
+ // ⌘G / ⌘⇧G: the bare key cuts inside the shot and the shifted one cuts
950
+ // the shot. Nothing else claims ⌘⇧B, so the shift has to be read
951
+ // HERE or a shot split would silently resolve as a razor.
952
+ if (alt) return null;
953
+ return shift ? { kind: "splitSceneAtPlayhead" } : { kind: "splitAtPlayhead" };
395
954
  case "g":
396
- return event.shiftKey ? { kind: "ungroup" } : { kind: "group" };
955
+ if (alt) return null;
956
+ return shift ? { kind: "ungroup" } : { kind: "group" };
397
957
  case "0":
398
- return { kind: "zoomReset" };
958
+ return plain ? { kind: "zoomReset" } : null;
959
+ // The zoom keys own Shift: "+" IS a shifted "=" on most layouts.
399
960
  case "=":
400
961
  case "+":
401
- return { kind: "zoom", direction: 1 };
962
+ return alt ? null : { kind: "zoom", direction: 1 };
402
963
  case "-":
403
- return { kind: "zoom", direction: -1 };
964
+ return alt ? null : { kind: "zoom", direction: -1 };
404
965
  default:
405
966
  // An unclaimed modifier combo belongs to the BROWSER (⌘T, ⌘W, ⌘R).
406
967
  // Falling through to the letter lane here would arm a tool on ⌘R and
407
- // reload the page at the same time.
408
- return null;
968
+ // reload the page at the same time. The one exception is Arrange's
969
+ // bracket chords, asked HERE, after every character above: a layout
970
+ // whose bracket key types a character this lane binds keeps that
971
+ // meaning (German `+` sits on BracketRight, and ⌘+ is zoom in).
972
+ return arrangeChord(event);
409
973
  }
410
974
  }
411
975
 
412
- // 2b. alt.
976
+ // 2b. alt. A Mac's Option composes the key ("¬" for ⌥L, a dead key for ⌥I,
977
+ // "⁄" for ⇧⌥1), so the letter comes from `code` when `key` is not one
978
+ // (`chordChar`). Like lane 2a, every case names whether it owns Shift.
413
979
  if (event.altKey) {
414
- switch (lower) {
980
+ const shift = event.shiftKey;
981
+ const altKey = chordChar(event, lower);
982
+ switch (altKey) {
415
983
  case "l":
416
- return { kind: "toggleLock" };
984
+ return shift ? null : { kind: "toggleLock" };
417
985
  case "h":
418
- // REVEAL EVERYTHING, Blender's own ⌥H, replacing a toggle that read
419
- // `selection[0]`'s flag — so five layers hidden one at a time had no
420
- // key that brought them back.
421
- return { kind: "setHidden", scope: "reveal" };
986
+ // ⌥⇧H HIDES EVERYTHING ELSE. It was ⇧H, which is cleared for a new
987
+ // verb. Bare ⌥H centres horizontally (the align block below).
988
+ return shift
989
+ ? { kind: "setHidden", scope: "others" }
990
+ : { kind: "align", mode: "hcenter" };
991
+ // ── ALIGN, Figma's ⌥ letters ───────────────────────────────────────
992
+ //
993
+ // A and D are the left and right edges, W and S the top and bottom (the
994
+ // WASD block), H and V the two centres. Bare only: ⌥⇧S is the size
995
+ // clear and ⌥⇧H hides everything else.
422
996
  case "a":
423
- // Blender's deselect-all. Its own action and NOT Escape's lane, which
424
- // also leaves a subtree.
425
- return { kind: "deselect" };
997
+ return shift ? null : { kind: "align", mode: "left" };
998
+ case "d":
999
+ return shift ? null : { kind: "align", mode: "right" };
1000
+ case "w":
1001
+ return shift ? null : { kind: "align", mode: "top" };
1002
+ case "v":
1003
+ return shift ? null : { kind: "align", mode: "vcenter" };
426
1004
  case "c":
427
1005
  // ⌥C and not bare C, because the alt lane is where this chrome keeps
428
1006
  // the verbs whose letter is already spoken for.
429
- return { kind: "alignCameraToView" };
430
- // ── CLEAR TRANSFORM, Blender's ⌥G / ⌥R / ⌥S ────────────────────────
1007
+ return shift ? null : { kind: "alignCameraToView" };
1008
+ case "m":
1009
+ return shift ? null : { kind: "soundMode" };
1010
+ case "z":
1011
+ // ⌥Z: the grid floor. Z is the viewport-look letter (bare Z shading,
1012
+ // ⇧Z wireframe), and the grid is that family's overlay.
1013
+ return shift ? null : { kind: "gridToggle" };
1014
+ // ── THE KEYFRAME JUMPS, MOVED HERE OFF BARE J / K ──────────────────
1015
+ //
1016
+ // They held the bare letters until the shuttle arrived, and the shuttle
1017
+ // has the better claim: J and K are the transport's own keys in every
1018
+ // NLE an author comes here from, and a keyframe jump is the more local
1019
+ // verb of the two. So they take the alt lane, which is where this file
1020
+ // keeps the verbs whose letter is spoken for — the same move ⌥C and ⌥Z
1021
+ // already made.
1022
+ case "j":
1023
+ return shift ? null : { kind: "jumpKeyframe", dir: -1 };
1024
+ case "k":
1025
+ return shift ? null : { kind: "jumpKeyframe", dir: 1 };
1026
+ // ── THE WORK AREA: ⌥I, ⌥O, ⌥X ──────────────────────────────────────
1027
+ //
1028
+ // I and O are in and out in every editor that has the idea, and both
1029
+ // BARE letters are tools here (`i` is Media, `o` is Ellipse), so the alt
1030
+ // lane is where they land. ⌥X clears, because X is the axis letter and
1031
+ // has no meaning of its own outside a live modal.
1032
+ case "i":
1033
+ return shift ? null : { kind: "setWorkArea", edge: "in" };
1034
+ case "o":
1035
+ return shift ? null : { kind: "setWorkArea", edge: "out" };
1036
+ case "x":
1037
+ return shift ? null : { kind: "clearWorkArea" };
1038
+ // ── CLEAR TRANSFORM, Blender's ⌥G / ⌥R, and ⌥⇧S ───────────────────
431
1039
  //
432
1040
  // Back to the layer's own zero: position, rotation, or size. It is the
433
1041
  // fastest way out of a transform that went wrong several edits ago, and
434
- // undo cannot reach it once anything else has happened since.
1042
+ // undo cannot reach it once anything else has happened since. Size is
1043
+ // ⌥⇧S: bare ⌥S is align bottom.
435
1044
  case "g":
436
- return { kind: "clearTransform", channel: "offset" };
1045
+ return shift ? null : { kind: "clearTransform", channel: "offset" };
437
1046
  case "r":
438
- return { kind: "clearTransform", channel: "rotation" };
1047
+ return shift ? null : { kind: "clearTransform", channel: "rotation" };
439
1048
  case "s":
440
- return { kind: "clearTransform", channel: "scale" };
1049
+ return shift
1050
+ ? { kind: "clearTransform", channel: "scale" }
1051
+ : { kind: "align", mode: "bottom" };
1052
+ // ── AXIS VIEWS WITHOUT A NUMPAD, Blender's "Emulate Numpad" ────────
1053
+ //
1054
+ // The numpad lane above keys on `event.code.startsWith("Numpad")`, so a
1055
+ // laptop without one had NO KEYBOARD PATH to front, right or top — the
1056
+ // three views a 3D pane is most often asked for. Blender ships a
1057
+ // preference that maps the top-row digits; this chrome keeps the bare
1058
+ // digits free and puts the stage layouts on ⇧1 / ⇧2 / ⇧3. The alt lane
1059
+ // is where this file keeps the verbs whose letter is spoken for, so they
1060
+ // land here with the same digits, and ⇧ for the opposite side.
1061
+ case "1":
1062
+ return {
1063
+ kind: "workingViewPreset",
1064
+ preset: shift ? "back" : "front",
1065
+ };
1066
+ case "3":
1067
+ return {
1068
+ kind: "workingViewPreset",
1069
+ preset: shift ? "left" : "right",
1070
+ };
1071
+ case "7":
1072
+ return {
1073
+ kind: "workingViewPreset",
1074
+ preset: shift ? "bottom" : "top",
1075
+ };
1076
+ case "5":
1077
+ return shift ? null : { kind: "toggleWorkingOrtho" };
441
1078
  default:
442
1079
  return null;
443
1080
  }
444
1081
  }
445
1082
 
446
1083
  // 3. Bare and shifted.
1084
+ // ── KEYFRAMES BEFORE LAYERS, ON THE HORIZONTAL PAIR ONLY ────────────────
1085
+ //
1086
+ // A selected keyframe is a MORE SPECIFIC subject than the layer under it —
1087
+ // the same most-specific-wins ladder `deleteSelection` walks — so ←/→ moves
1088
+ // the keys in time when there are keys. ⇧ is the same 10x it is everywhere
1089
+ // else in this table.
1090
+ //
1091
+ // ↑/↓ ARE DELIBERATELY NOT CLAIMED. A keyframe's other axis is its VALUE, and
1092
+ // there is no step size that means anything across kinds: 1 is imperceptible
1093
+ // on a position and the whole range on an opacity. It stays a drag in the
1094
+ // graph, where the axis has a scale on it.
1095
+ if (
1096
+ (key === "ArrowLeft" || key === "ArrowRight") &&
1097
+ context.hasKeyframeSelection === true
1098
+ ) {
1099
+ const dir = key === "ArrowLeft" ? -1 : 1;
1100
+ return {
1101
+ kind: "nudgeKeys",
1102
+ delta: dir * (event.shiftKey ? NUDGE_COARSE_FRAMES : 1),
1103
+ };
1104
+ }
447
1105
  const arrow = ARROWS[key];
448
1106
  if (arrow !== undefined) {
449
- const step = event.shiftKey ? NUDGE_COARSE_PX : NUDGE_PX;
450
1107
  // SELECTION WINS. With nothing selected the same keys move the playhead,
451
1108
  // which is the only reading where both meanings are reachable without a
452
1109
  // mode to switch between them.
453
- return context.hasSelection
454
- ? { kind: "nudge", dx: arrow[0] * step, dy: arrow[1] * step }
455
- : { kind: "step", delta: arrow[0] !== 0 ? arrow[0] : arrow[1] };
1110
+ if (context.hasSelection) {
1111
+ const step = event.shiftKey ? NUDGE_COARSE_PX : NUDGE_PX;
1112
+ return { kind: "nudge", dx: arrow[0] * step, dy: arrow[1] * step };
1113
+ }
1114
+ const dir = arrow[0] !== 0 ? arrow[0] : arrow[1];
1115
+ // ── THE PLAYHEAD'S OWN THREE READINGS ────────────────────────────────
1116
+ //
1117
+ // ⇧ IS A SECOND, on all four arrows. Shift is the coarse step everywhere
1118
+ // else in this table (10 px on a nudge, 10 frames on a keyframe), and a
1119
+ // frame's own coarse unit is the second — which is also the only jump size
1120
+ // that means the same thing at 24, 30 and 60 fps. It cannot be spelled as
1121
+ // a frame count here: see `stepSeconds`.
1122
+ if (event.shiftKey) return { kind: "stepSeconds", delta: dir < 0 ? -1 : 1 };
1123
+ // ↑ / ↓ WALK THE EDIT POINTS. Bare, and it costs nothing: with nothing
1124
+ // selected the vertical pair used to be a second, identical spelling of
1125
+ // ←/→'s one-frame step, so the only key this claims is a duplicate. What
1126
+ // it buys is the gesture an author scanning a cut actually wants —
1127
+ // straight to the next place something happens.
1128
+ if (arrow[0] === 0) return { kind: "jumpEdit", dir: dir < 0 ? -1 : 1 };
1129
+ return { kind: "step", delta: dir };
1130
+ }
1131
+ // ── BARE DIGITS SET OPACITY, the After Effects and Figma keys ───────────
1132
+ //
1133
+ // 1 is 10% through 9 at 90%, and 0 is 100%. Selection only, by `key`: a
1134
+ // shifted digit is a layout (`layoutForEvent`), a numpad digit returned in
1135
+ // lane 1d, and a live modal kept the digits as typed numbers in lane 1c.
1136
+ // With nothing selected the digits answer null and stay free.
1137
+ if (/^[0-9]$/.test(key) && !event.shiftKey && context.hasSelection) {
1138
+ const n = Number(key);
1139
+ return { kind: "opacity", value: n === 0 ? 1 : n / 10 };
456
1140
  }
457
- const mode = LAYOUT_KEYS[key];
458
- if (mode !== undefined) return { kind: "stageLayout", mode };
1141
+ // ── THE SHUTTLE: J, K, ⇧K ────────────────────────────────────────────────
1142
+ //
1143
+ // J runs backwards and steps down the ladder, K stops, ⇧K runs forwards and
1144
+ // steps up. Both bare letters were free once the keyframe jumps moved to the
1145
+ // alt lane above, and ⌘K stays the app menu (a lane resolved long before
1146
+ // this one).
1147
+ //
1148
+ // ── L IS NOT AVAILABLE, AND IS NOT SHADOWED ──────────────────────────────
1149
+ //
1150
+ // Every NLE spells this J / K / L, and this chrome cannot: bare `L` is the
1151
+ // LINE tool (`TOOL_SPECS.line.key`), reached through `toolForKey` at the
1152
+ // bottom of this lane, and ⇧L is the Arrow tool beside it. A shuttle bound
1153
+ // above them would shadow both, which is the one thing this file's header
1154
+ // forbids — the tool would still be armable from the dock and from nowhere
1155
+ // on the keyboard, with nothing saying so. The alt lane cannot take it
1156
+ // either: ⌥L is `toggleLock`.
1157
+ //
1158
+ // So the third rung is ⇧K, on the shuttle's own remaining letter. Shift
1159
+ // already means "the same verb, the other subject" in this table (⌘B / ⌘⇧B,
1160
+ // ⌘G / ⌘⇧G, ⌘A / ⌘⇧A), and K is the letter an author's hand is already on.
1161
+ if (lower === "j" && !event.shiftKey) return { kind: "shuttle", dir: -1 };
1162
+ if (lower === "k")
1163
+ return event.shiftKey ? { kind: "shuttle", dir: 1 } : { kind: "shuttleStop" };
1164
+ // `;` and `'` seek to the selection's own in and out — the two keys to the
1165
+ // right of the shuttle's run, and free: no tool claims a punctuation mark,
1166
+ // and the bracket pair one row up already proved this lane safe to be bare.
1167
+ if (key === ";") return { kind: "seekSelectionEdge", edge: "in" };
1168
+ if (key === "'") return { kind: "seekSelectionEdge", edge: "out" };
1169
+ // The three layouts on ⇧1 / ⇧2 / ⇧3, LEFT TO RIGHT as the toolbar draws
1170
+ // them, by the physical key (`layoutForEvent`). The modifier lanes above
1171
+ // have already returned by here, so ⌘1 stays the browser's tab switch. The
1172
+ // BARE digits answer nothing: they are kept free for numeric verbs.
1173
+ const mode = layoutForEvent(event);
1174
+ if (mode !== null) return { kind: "stageLayout", mode };
1175
+ // ── AND ⇧4 IS THE PICTURE ─────────────────────────────────────────────
1176
+ //
1177
+ // Next in the row the three layouts occupy. It is deliberately NOT a toggle
1178
+ // (Escape is the way back, one rung above every other Escape verb) because
1179
+ // ⇧4 names a place rather than a switch, exactly as ⇧1 / ⇧2 / ⇧3 do.
1180
+ if (event.shiftKey && event.code === VIEW_MODE_SPECS.video.code)
1181
+ return { kind: "viewMode", mode: "video" };
459
1182
  if (key === " ") return { kind: "playToggle" };
460
1183
  // `\` fits the timeline. It is the NLE convention (Premiere and Resolve both
461
1184
  // bind "zoom to sequence" to it) and it is a key no letter lane can reach, so
462
1185
  // the dock's fit costs the tool table nothing.
463
1186
  if (key === "\\") return { kind: "fitTimeline" };
1187
+ // `/` puts the caret in the column's search, the key the field's own hint
1188
+ // shows. By `key`, so a layout that types `/` with Shift still reaches it;
1189
+ // the numpad's `/` returned in lane 1d as isolation. A field already owns
1190
+ // its keystrokes (lane 1), so typing a path into an input is untouched.
1191
+ if (key === "/") return { kind: "focusSearch" };
464
1192
  // The bracket keys hide their own side, and `.` hides everything. Bare, and
465
1193
  // safe to be bare: none of the three is a tool letter, and this lane's
466
1194
  // complaint was that the panels leave too little stage — a chord to see your
@@ -470,30 +1198,75 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
470
1198
  if (key === "]") return { kind: "togglePanel", panel: "inspector" };
471
1199
  if (key === ".") return { kind: "togglePanel", panel: "all" };
472
1200
  // ESCAPE, IN PRECEDENCE ORDER: abandon the pen (lane 1b, above), then
473
- // deselect, then leave one level of isolation. Innermost subject first, and
474
- // the two are ordered rather than exclusive because an author inside a card
475
- // with a child selected presses Escape twice — once to drop the selection,
476
- // once to come out. Collapsing them would make the second press do nothing
477
- // and the first do too much.
1201
+ // deselect, then leave one level of isolation, then the definition's own two
1202
+ // rungs (its layers back to its Studio, then the Studio out of the definition
1203
+ // entirely), then leave the shot. Innermost subject first, and they are
1204
+ // ordered rather than exclusive because an author inside a card with a child
1205
+ // selected presses Escape twice — once to drop the selection, once to come
1206
+ // out. Collapsing them would make the second press do nothing and the first
1207
+ // do too much.
1208
+ //
1209
+ // The definition sits above the shot because it NESTS INSIDE one: the
1210
+ // component context is entered from the shot context and every ladder in this
1211
+ // chrome climbs one level per press. The component context and isolation are
1212
+ // mutually exclusive since v14 (entering a definition clears isolation), so at
1213
+ // most one of the two middle rungs fires — but a selection inside either still
1214
+ // takes the first press.
1215
+ //
1216
+ // A SHOT DOOR GOES BACK TO ITS SHOT. When Components was opened from a
1217
+ // Shot (`componentsOrigin`), one press restores that Shot, its frame and its
1218
+ // selection, which is what "Back to Shot" in the bar does.
478
1219
  if (key === "Escape") {
479
- if (context.hasSelection) return { kind: "deselect" };
1220
+ // ── RUNG -1: HANDS OFF THE CAMERA ───────────────────────────────────
1221
+ //
1222
+ // The pilot badge promises "ESC TO EJECT", and a mode that writes the
1223
+ // document on every drag has to be the first thing Escape leaves. The
1224
+ // selection, the container and the screening all survive it.
1225
+ if (context.pilot === true) return { kind: "pilotEject" };
1226
+ // ── RUNG 0: PUT THE EDITOR BACK ─────────────────────────────────────
1227
+ //
1228
+ // Above the deselect, and that order is the whole rule: the selection
1229
+ // survives a screening (Video simply stops drawing its handles), so an
1230
+ // author who pressed ⇧4, watched, and pressed Escape is asking for the
1231
+ // editor back and would be surprised to find their selection gone and the
1232
+ // picture unchanged. Every rung below is about the CONTAINER the author is
1233
+ // inside; this one is about whether they are looking or working, which sits
1234
+ // over all of them.
1235
+ if (context.video === true) return { kind: "viewMode", mode: "edit" };
1236
+ if (context.hasSelection || context.hasAudioSelection === true) return { kind: "deselect" };
480
1237
  if (context.isolation === true) return { kind: "exitIsolation" };
1238
+ if (context.componentsOrigin === true) return { kind: "backToShot" };
1239
+ if (context.editingDef === true) return { kind: "closeComponentContext" };
1240
+ // The other family's subject, at the same height and never both: the two
1241
+ // are mutually exclusive by construction (each entry clears the other), so
1242
+ // this is a sibling rung rather than one below.
1243
+ if (context.codeComponent === true) return { kind: "closeCodeComponent" };
1244
+ if (context.componentsOpen === true) return { kind: "leaveComponents" };
1245
+ if (context.enteredScene === true) return { kind: "leaveScene" };
481
1246
  return { kind: "deselect" };
482
1247
  }
483
1248
  // ENTER goes the other way: into the selected layer. Unclaimed until now, and
484
1249
  // the pen already owns it while a path is open (lane 1b).
485
- if (key === "Enter" && context.hasSelection) return { kind: "enterIsolation" };
486
- if (key === "Delete" || key === "Backspace") return { kind: "deleteSelection" };
1250
+ if (key === "Enter" && context.hasSelection)
1251
+ return { kind: "enterIsolation" };
1252
+ // ⇧BACKSPACE IS THE RIPPLE, above the plain delete because that line reads
1253
+ // neither key's modifiers. Backspace and not Delete, and only Backspace: the
1254
+ // two keys are one binding everywhere else in this table, and splitting them
1255
+ // by name is what keeps a Del pressed near the shift key from closing a gap
1256
+ // the author wanted. Every NLE spells the ripple with a shift for the same
1257
+ // reason.
1258
+ if (key === "Backspace" && event.shiftKey) return { kind: "rippleDelete" };
1259
+ if (key === "Delete" || key === "Backspace")
1260
+ return { kind: "deleteSelection" };
487
1261
  // HOME frames the active pane, the Blender "frame all". A key no field
488
1262
  // shares (lane 1 already returned for a field) and no tool can reach.
489
1263
  if (key === "Home") return { kind: "fit" };
490
1264
  if (lower === "a" && event.shiftKey) return { kind: "animateToggle" };
491
- // ── BLENDER'S SELECTION KEYS ──────────────────────────────────────────
1265
+ // ── BLENDER'S SELECTION KEY ───────────────────────────────────────────
492
1266
  //
493
- // Bare `A` selects all and `⌥A` deselects, which is muscle memory a Blender
494
- // user brings with them. `A` was free; the alt lane handles its partner.
495
- // `⌥A` must be its own action rather than reusing Escape's lane, because
496
- // Escape also LEAVES A SUBTREE and deselecting must not.
1267
+ // Bare `A` selects all, which is muscle memory a Blender user brings with
1268
+ // them. Its partner is ⌘⇧A in lane 2a, its own action rather than Escape's,
1269
+ // because Escape also LEAVES A SUBTREE and deselecting must not.
497
1270
  if (lower === "a" && !event.shiftKey) return { kind: "selectAll" };
498
1271
  // `B` arms a one-shot box select. Stage-local: the letter still has one owner
499
1272
  // here, and the stage reads the armed bit beside `e.shiftKey` in its own
@@ -507,10 +1280,17 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
507
1280
  //
508
1281
  // `toggleFlag` reads `selection[0]`'s own flag, so after hiding five layers
509
1282
  // one at a time there was NO key that brought them back — the selection was
510
- // gone with them. Explicit verbs fix that: H hides, ⌥H reveals everything,
511
- // ⇧H hides everything else. The outliner's eye button keeps the toggle.
512
- if (lower === "h" && !event.shiftKey && !event.altKey) return { kind: "setHidden", scope: "selection" };
513
- if (lower === "h" && event.shiftKey) return { kind: "setHidden", scope: "others" };
1283
+ // gone with them. Explicit verbs fix that: H hides, ⌘⇧H reveals everything
1284
+ // (lane 2a), ⌥⇧H hides everything else (lane 2b). The outliner's eye button
1285
+ // keeps the toggle.
1286
+ if (lower === "h" && !event.shiftKey)
1287
+ return { kind: "setHidden", scope: "selection" };
1288
+ // ⇧H / ⇧V FLIP, Figma's keys. Only with a selection, so with none both stay
1289
+ // free for the browser.
1290
+ if (event.shiftKey && context.hasSelection) {
1291
+ if (lower === "h") return { kind: "flip", axis: "x" };
1292
+ if (lower === "v") return { kind: "flip", axis: "y" };
1293
+ }
514
1294
  // ⇧Z IS THE WIREFRAME TOGGLE, matching Blender. It used to fit the active
515
1295
  // pane, which `Home` already does and which is the Blender-correct key for
516
1296
  // it — so the letter was spent on a duplicate.
@@ -525,36 +1305,292 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
525
1305
  // key. The `+ shot` button in the dock is where an author looks for a move,
526
1306
  // and it is still there.
527
1307
  if (lower === "c" && event.shiftKey) return { kind: "fit" };
1308
+ // BARE C ARMS THE COMMENT TOOL (S5): the next click on the camera pane
1309
+ // leaves a Studio comment at the playhead. On the board the same key
1310
+ // comments on the selected card.
1311
+ if (lower === "c" && !event.shiftKey) return { kind: "storyboardComment" };
528
1312
  // BARE M drops a marker, which is the binding Premiere, Resolve and Final Cut
529
1313
  // all ship. Guarded to bare rather than left open, because ⇧M is "go to the
530
1314
  // next marker" in the same three tools and this chrome does not have that
531
1315
  // verb yet — claiming the shifted letter now would make it unavailable later
532
1316
  // without breaking somebody's muscle memory twice.
533
1317
  if (lower === "m" && !event.shiftKey) return { kind: "addMarker" };
1318
+ // Q AND W TRIM TO THE PLAYHEAD: Premiere's letters, without the ripple. Bare
1319
+ // and selection only: neither letter is a tool (`toolForKey`), and with
1320
+ // nothing selected both stay free.
1321
+ if (!event.shiftKey && (context.hasSelection || context.hasAudioSelection === true)) {
1322
+ if (lower === "q") return { kind: "trimToPlayhead", edge: "in" };
1323
+ if (lower === "w") return { kind: "trimToPlayhead", edge: "out" };
1324
+ }
534
1325
 
535
- // THE MODAL STARTERS, checked immediately before the tool table so bare G
536
- // and R shadow the move/rect tools ONLY with the cursor over the working
1326
+ // THE MODAL STARTERS, checked immediately before the tool table so bare G, R
1327
+ // and S shadow the move/rect tools ONLY with the cursor over a 3D working
537
1328
  // pane. Blender's letters, Blender's scope: a transform needs an anchor
538
1329
  // under the cursor, so from a panel or the camera pane the letters keep
539
- // arming tools exactly as before. `R` additionally requires a selection —
540
- // with nothing selected G/S address the CAMERA (the stage decides that from
541
- // the same selection), but the camera has no modal roll, so R falls through
542
- // to the rect tool rather than claiming a dead key.
543
- if (context.workingPane === true && !event.shiftKey) {
1330
+ // arming tools exactly as before.
1331
+ //
1332
+ // ── R NO LONGER NEEDS A SELECTION ──────────────────────────────────────
1333
+ //
1334
+ // With nothing selected all three address the CAMERA (the stage decides that
1335
+ // from the same selection), and the camera now HAS a modal roll — so R is
1336
+ // symmetric with G and S rather than falling through to the rect tool.
1337
+ //
1338
+ // THIS TAKES A KEY FROM THE RECT TOOL, deliberately and only here: R still
1339
+ // arms rect over the camera pane, over every panel and from the toolbar. The
1340
+ // trade is that roll was otherwise unreachable by any gesture at all (the
1341
+ // frustum gizmo has no grip for it — see `framingDeltaFor`), and an
1342
+ // unreachable channel costs more than one shadowed letter in one pane.
1343
+ //
1344
+ // ── BUT NOT OVER A DEFINITION'S FRAME ─────────────────────────────────
1345
+ //
1346
+ // Inside a definition the stage draws one working pane, and it is the frame
1347
+ // the tools draw on (`framePane`), so the trade above does not hold there:
1348
+ // the starters would shadow R over the whole stage while the footer says
1349
+ // "Rect (R)", and the camera roll they bought does not exist. A letter a
1350
+ // tool claims keeps its tool over the frame. Today that is R alone, so G
1351
+ // and S keep their modals.
1352
+ if (
1353
+ context.workingPane === true &&
1354
+ !event.shiftKey &&
1355
+ !(context.framePane === true && toolForKey(lower) !== null)
1356
+ ) {
544
1357
  if (lower === "g") return { kind: "modalStart", mode: "move" };
545
1358
  if (lower === "s") return { kind: "modalStart", mode: "scale" };
546
- if (lower === "r" && context.hasSelection) return { kind: "modalStart", mode: "rotate" };
1359
+ if (lower === "r") return { kind: "modalStart", mode: "rotate" };
547
1360
  }
548
1361
 
1362
+ // ⇧S OPENS THE STORYBOARD from the ordinary shell. Unclaimed by every tool
1363
+ // (`toolForKey` binds ⇧L and ⇧P, never ⇧S) and by the modal starters above,
1364
+ // which read the bare letter only.
1365
+ if (lower === "s" && event.shiftKey) return { kind: "storyboardToggle" };
1366
+ // ⇧R IS THE RULERS, for the same reason: the modal starters and the tool
1367
+ // table read bare R only.
1368
+ if (lower === "r" && event.shiftKey) return { kind: "rulersToggle" };
1369
+
549
1370
  const tool = toolForKey(lower, event.shiftKey);
550
1371
  return tool === null ? null : { kind: "tool", tool };
551
1372
  }
552
1373
 
1374
+ /**
1375
+ * The storyboard's lane, as its own function so lane 1f reads as one line.
1376
+ * `"pass"` keeps undo, redo, save, the app menu and SPACE, which plays.
1377
+ * `null` blanks every tool letter and modal starter.
1378
+ *
1379
+ * THE ARROWS DO NOTHING HERE: a focused strip or Story card walks with them
1380
+ * and stops them there, and a card has no position to nudge.
1381
+ */
1382
+ function storyboardKey(
1383
+ event: KeyEventLike,
1384
+ key: string,
1385
+ lower: string,
1386
+ mod: boolean,
1387
+ ): KeyAction | null | "pass" {
1388
+ if (mod) {
1389
+ switch (lower) {
1390
+ case "z":
1391
+ case "y":
1392
+ case "s":
1393
+ case "k":
1394
+ return "pass";
1395
+ case "a":
1396
+ return { kind: "storyboardSelectAll" };
1397
+ case "d":
1398
+ return { kind: "storyboardDuplicate" };
1399
+ // THE CLIPBOARD, AND ITS ARM RETURNS FALSE ON PURPOSE. See the union.
1400
+ // Native DOM copy / cut / paste handlers do the work, and they are the
1401
+ // only route by which a pasted PNG can reach the board.
1402
+ case "c":
1403
+ case "x":
1404
+ case "v":
1405
+ return { kind: "storyboardClipboard" };
1406
+ default:
1407
+ return arrangeNowhere(event, lower);
1408
+ }
1409
+ }
1410
+ if (event.altKey) return null;
1411
+ const comment = commentKey(event, lower);
1412
+ if (comment !== null) return comment;
1413
+ if (key === " ") return "pass";
1414
+ if (key === "Delete" || key === "Backspace") return { kind: "storyboardDelete" };
1415
+ if (key === "Escape") return { kind: "storyboardEscape" };
1416
+ if (key === "F2") return { kind: "storyboardRename" };
1417
+ // `/` passes through: the board has no search field of its own.
1418
+ if (key === "/") return "pass";
1419
+ if (lower === "s" && event.shiftKey) return { kind: "storyboardToggle" };
1420
+ return null;
1421
+ }
1422
+
1423
+ /**
1424
+ * THE SHOT BOARD'S SKETCH KEYS: the arrows move the selected items (Shift:
1425
+ * 10), `[` and `]` restack one step, Enter edits the one text item selected,
1426
+ * and V T R A I C arm its tools. Bare C is Component there, not Comment: a
1427
+ * sketch has no card to comment on.
1428
+ *
1429
+ * THEIR OWN UNION AND THEIR OWN RESOLVER, outside `KeyAction`: only the shot
1430
+ * board's element answers them (ShotBoard.tsx) and stops them there, so an
1431
+ * arrow pressed in the inspector moves nothing. The store's `handleKey` never
1432
+ * meets one, and its dispatch stays exhaustive over `KeyAction`.
1433
+ */
1434
+ export type SketchKeyAction =
1435
+ | { kind: "sketchMove"; dx: number; dy: number }
1436
+ | { kind: "sketchRestack"; by: 1 | -1 }
1437
+ | { kind: "sketchEdit" }
1438
+ | { kind: "sketchTool"; tool: SketchTool };
1439
+
1440
+ /** A key on a shot board: its sketch lane first (bare or shifted, never with
1441
+ * ⌘ or ⌥, never into a field), then the board's own lane. */
1442
+ export function resolveShotBoardKey(event: KeyEventLike, context: KeyContext): KeyAction | SketchKeyAction | null {
1443
+ if (!context.editing && !event.ctrlKey && !event.metaKey && !event.altKey) {
1444
+ const sketch = sketchKey(event, event.key, event.key.toLowerCase());
1445
+ if (sketch !== null) return sketch;
1446
+ }
1447
+ return resolveKey(event, { ...context, storyboard: true });
1448
+ }
1449
+
1450
+ const SKETCH_TOOL_KEYS: Readonly<Record<string, SketchTool>> = {
1451
+ v: "select",
1452
+ t: "text",
1453
+ r: "box",
1454
+ a: "arrow",
1455
+ i: "image",
1456
+ c: "component",
1457
+ };
1458
+ const ARROW_STEPS: Readonly<Record<string, readonly [number, number]>> = {
1459
+ ArrowLeft: [-1, 0],
1460
+ ArrowRight: [1, 0],
1461
+ ArrowUp: [0, -1],
1462
+ ArrowDown: [0, 1],
1463
+ };
1464
+
1465
+ function sketchKey(event: KeyEventLike, key: string, lower: string): SketchKeyAction | null {
1466
+ const step = ARROW_STEPS[key];
1467
+ if (step !== undefined) {
1468
+ const by = event.shiftKey ? 10 : 1;
1469
+ return { kind: "sketchMove", dx: step[0] * by, dy: step[1] * by };
1470
+ }
1471
+ if (key === "[") return { kind: "sketchRestack", by: -1 };
1472
+ if (key === "]") return { kind: "sketchRestack", by: 1 };
1473
+ if (key === "Enter") return { kind: "sketchEdit" };
1474
+ const tool = event.shiftKey ? undefined : SKETCH_TOOL_KEYS[lower];
1475
+ return tool === undefined ? null : { kind: "sketchTool", tool };
1476
+ }
1477
+
1478
+ /** The comment keys: bare C (Shift+C stays "fit"), N and Shift+N. */
1479
+ function commentKey(event: KeyEventLike, lower: string): KeyAction | null {
1480
+ if (lower === "c" && !event.shiftKey) return { kind: "storyboardComment" };
1481
+ if (lower === "n") return { kind: event.shiftKey ? "storyboardPrevThread" : "storyboardNextThread" };
1482
+ return null;
1483
+ }
1484
+
1485
+ /**
1486
+ * The graph canvas's lane, as its own function so lane 1e reads as one line.
1487
+ *
1488
+ * `"pass"` means "this key is not mine, keep walking the ordinary lanes" and is
1489
+ * reserved for the four that must survive a context change: ⌘Z, ⌘⇧Z / ⌘Y, ⌘S
1490
+ * and ⌘K. Everything else in this lane is either a graph verb or `null`.
1491
+ *
1492
+ * SPACE USED TO BE A FIFTH, and is not any more. Over this canvas it is
1493
+ * hold-to-pan, which React Flow implements on its OWN window listener — so the
1494
+ * lane has to return `null` rather than `"pass"`: `null` means `handleKey`
1495
+ * reports the key unhandled and never calls `preventDefault`, which is exactly
1496
+ * what lets that listener see it. Play/pause is untouched everywhere else in
1497
+ * the workspace, including the dock right below this canvas.
1498
+ *
1499
+ * THE NULL RETURN IS THE POINT elsewhere too. Bare letters reach the tool table
1500
+ * at the bottom of `resolveKey`, and a graph canvas has no rectangle to draw and
1501
+ * no pen to arm — an `R` that silently armed the rect tool would leave the
1502
+ * author back on the stage with a tool they never chose, several clicks later.
1503
+ */
1504
+ function materialGraphKey(
1505
+ event: KeyEventLike,
1506
+ key: string,
1507
+ lower: string,
1508
+ mod: boolean,
1509
+ ): KeyAction | null | "pass" {
1510
+ if (mod) {
1511
+ switch (lower) {
1512
+ // The four that mean the same thing everywhere in this chrome.
1513
+ case "z":
1514
+ case "y":
1515
+ case "s":
1516
+ case "k":
1517
+ return "pass";
1518
+ case "d":
1519
+ return { kind: "graphDuplicate" };
1520
+ case "c":
1521
+ return { kind: "graphCopy" };
1522
+ case "x":
1523
+ return { kind: "graphCut" };
1524
+ case "v":
1525
+ return { kind: "graphPaste" };
1526
+ case "a":
1527
+ return { kind: "graphSelectAll" };
1528
+ case "g":
1529
+ return event.shiftKey
1530
+ ? { kind: "graphUngroup" }
1531
+ : { kind: "graphGroup" };
1532
+ default:
1533
+ // An unclaimed combo belongs to the BROWSER, exactly as in lane 2a,
1534
+ // which claims Arrange's brackets, and so does this lane.
1535
+ return arrangeNowhere(event, lower);
1536
+ }
1537
+ }
1538
+ // The alt lane's verbs are all about layers and the working view, and none of
1539
+ // them has a meaning over a node. Blanked rather than passed, so ⌥G cannot
1540
+ // clear a mesh's position from inside the graph.
1541
+ if (event.altKey) return null;
1542
+
1543
+ // SPACE BELONGS TO REACT FLOW HERE — hold-to-pan, on its own window
1544
+ // listener. `null` and not `"pass"`: see the header. Play/pause keeps the
1545
+ // key everywhere in this workspace that is not this canvas.
1546
+ if (key === " ") return null;
1547
+ // TAB and ⇧A both open the palette: Tab is what Blender's and Unreal's node
1548
+ // editors bind, ⇧A is Blender's Add menu, and an author arrives with one or
1549
+ // the other in their fingers. Tab is not a focus trap here because the canvas
1550
+ // has no tabbable content of its own; the panels around it still do.
1551
+ if (key === "Tab") return { kind: "graphPalette" };
1552
+ if (lower === "a" && event.shiftKey) return { kind: "graphPalette" };
1553
+ if (key === "Delete" || key === "Backspace") return { kind: "graphDelete" };
1554
+ if (lower === "d" && event.shiftKey) return { kind: "graphDuplicate" };
1555
+ if (key === "Enter") return { kind: "graphEnter" };
1556
+ if (lower === "p" && !event.shiftKey) return { kind: "graphPreviewToggle" };
1557
+ // HOME is this chrome's "frame all" and `F` is what every node editor binds.
1558
+ // Both, because the graph is the one surface an author reaches from muscle
1559
+ // memory built somewhere else.
1560
+ if (key === "Home" || (lower === "f" && !event.shiftKey))
1561
+ return { kind: "graphFrame" };
1562
+ // T for TIDY. A bare letter this lane used to blank; it is the one arrangement
1563
+ // key a node editor is expected to have, and there is no tool for it to
1564
+ // collide with because this canvas arms none.
1565
+ if (lower === "t" && !event.shiftKey) return { kind: "graphTidy" };
1566
+ const arrow = ARROWS[key];
1567
+ if (arrow !== undefined) {
1568
+ const step = event.shiftKey ? GRAPH_NUDGE_COARSE_PX : GRAPH_NUDGE_PX;
1569
+ return { kind: "graphNudge", dx: arrow[0] * step, dy: arrow[1] * step };
1570
+ }
1571
+ if (key === "Escape") return { kind: "graphEscape" };
1572
+ return null;
1573
+ }
1574
+
553
1575
  /** Is the event target something the author is typing into? Exported because
554
1576
  * both documents ask it and neither should re-derive it. */
555
1577
  export function isEditable(target: EventTarget | null): boolean {
556
- if (target === null || typeof (target as HTMLElement).tagName !== "string") return false;
1578
+ if (target === null || typeof (target as HTMLElement).tagName !== "string")
1579
+ return false;
557
1580
  const el = target as HTMLElement;
558
1581
  if (el.isContentEditable) return true;
559
- return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT";
1582
+ return (
1583
+ el.tagName === "INPUT" ||
1584
+ el.tagName === "TEXTAREA" ||
1585
+ el.tagName === "SELECT"
1586
+ );
1587
+ }
1588
+
1589
+ /** Is the target a control the keyboard focused, which Enter and Space press?
1590
+ * Not part of `isEditable`: a focused button does not own letters or Delete. */
1591
+ export function ownsActivation(target: EventTarget | null): boolean {
1592
+ if (target === null || typeof (target as HTMLElement).matches !== "function") return false;
1593
+ return (target as HTMLElement).matches(
1594
+ 'button:focus-visible, [role="menuitem"]:focus-visible, [role="option"]:focus-visible, [role="tab"]:focus-visible, [role="radio"]:focus-visible',
1595
+ );
560
1596
  }