@hyperframes/studio 0.8.50 → 0.8.53-desktop.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 (330) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-PTEnAHhg.js} +3 -3
  2. package/dist/assets/{index-DUAIWxkI.js → index--myXp8ui.js} +1 -1
  3. package/dist/assets/{index-Dt3qBI-9.js → index-BjslOS9g.js} +1 -1
  4. package/dist/assets/index-BnXZuIJD.css +1 -0
  5. package/dist/assets/index-CNyviK_l.js +390 -0
  6. package/dist/index.css +29 -0
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.ts +825 -24
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +8911 -9853
  11. package/dist/index.js.map +1 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +13 -12
  14. package/src/App.tsx +145 -163
  15. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  16. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  17. package/src/captions/components/CaptionOverlay.tsx +8 -4
  18. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  19. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  20. package/src/captions/components/CaptionTimeline.tsx +1 -1
  21. package/src/captions/components/shared.tsx +2 -2
  22. package/src/components/AskAgentModal.tsx +5 -5
  23. package/src/components/EditorShell.selectionSync.test.tsx +9 -4
  24. package/src/components/EditorShell.tsx +59 -62
  25. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  26. package/src/components/LintModal.tsx +6 -16
  27. package/src/components/MediaPreview.tsx +2 -2
  28. package/src/components/RightPanelTabs.test.tsx +151 -0
  29. package/src/components/RightPanelTabs.tsx +59 -0
  30. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  31. package/src/components/StudioHeader.dom.test.tsx +181 -0
  32. package/src/components/StudioHeader.test.ts +0 -12
  33. package/src/components/StudioHeader.tsx +118 -221
  34. package/src/components/StudioLeftPanels.tsx +202 -0
  35. package/src/components/StudioOverlays.tsx +1 -1
  36. package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
  37. package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
  38. package/src/components/StudioToast.tsx +2 -2
  39. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  40. package/src/components/TimelineHistoryButtons.tsx +54 -0
  41. package/src/components/TimelineToolbar.test.tsx +8 -0
  42. package/src/components/TimelineToolbar.tsx +8 -12
  43. package/src/components/dock/Dock.test.tsx +274 -0
  44. package/src/components/dock/Dock.tsx +291 -0
  45. package/src/components/dock/dock.css +28 -0
  46. package/src/components/dock/dockAccessibility.test.ts +166 -0
  47. package/src/components/dock/dockAccessibility.ts +98 -0
  48. package/src/components/dock/dockLayout.test.ts +134 -0
  49. package/src/components/dock/dockLayout.ts +105 -0
  50. package/src/components/dock/dockLayoutSchema.ts +66 -0
  51. package/src/components/dock/dockLayoutStore.test.ts +28 -0
  52. package/src/components/dock/dockLayoutStore.ts +108 -0
  53. package/src/components/dock/dockTestHarness.ts +11 -0
  54. package/src/components/dock/panelRegistry.ts +67 -0
  55. package/src/components/editor/AnimationCard.tsx +4 -4
  56. package/src/components/editor/AnimationCardParts.tsx +6 -6
  57. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  58. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  59. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  60. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  61. package/src/components/editor/DomEditOverlay.tsx +1 -1
  62. package/src/components/editor/EaseCurveSection.tsx +1 -1
  63. package/src/components/editor/EaseParamFields.tsx +5 -5
  64. package/src/components/editor/FileTree.tsx +4 -4
  65. package/src/components/editor/FileTreeIcons.tsx +1 -1
  66. package/src/components/editor/FileTreeNodes.tsx +13 -13
  67. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  68. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  69. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  70. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  71. package/src/components/editor/LayersPanel.test.ts +97 -1
  72. package/src/components/editor/LayersPanel.tsx +7 -5
  73. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  74. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  75. package/src/components/editor/PreviewGuides.tsx +1 -1
  76. package/src/components/editor/PromotableControl.tsx +2 -2
  77. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  78. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  79. package/src/components/editor/SnapToolbar.test.tsx +0 -3
  80. package/src/components/editor/SnapToolbar.tsx +2 -2
  81. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  82. package/src/components/editor/TopologyLens.test.tsx +28 -0
  83. package/src/components/editor/TopologyLens.tsx +7 -5
  84. package/src/components/editor/Transform3DCube.tsx +2 -2
  85. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  86. package/src/components/editor/domEditing.test.ts +1 -1
  87. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  88. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  89. package/src/components/editor/propertyPanelColor.tsx +11 -11
  90. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  91. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  92. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  93. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  94. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  95. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  96. package/src/components/editor/propertyPanelFill.tsx +4 -4
  97. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  98. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  99. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  100. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  101. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  102. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  103. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  104. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  105. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  106. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  107. package/src/components/editor/propertyPanelFont.tsx +5 -5
  108. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  109. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  110. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  111. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  112. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  113. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  114. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  115. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  116. package/src/components/editor/propertyPanelSections.tsx +8 -8
  117. package/src/components/editor/useColorGradingController.test.ts +38 -0
  118. package/src/components/editor/useColorGradingController.ts +9 -1
  119. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  120. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  121. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  122. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  123. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  124. package/src/components/feedback/feedbackTrigger.ts +0 -1
  125. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  126. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  127. package/src/components/nle/NLEContext.tsx +0 -43
  128. package/src/components/nle/NLEPreview.tsx +3 -3
  129. package/src/components/nle/PreviewOverlays.tsx +6 -6
  130. package/src/components/nle/PreviewPane.tsx +3 -3
  131. package/src/components/nle/TimelinePane.tsx +53 -67
  132. package/src/components/panels/SlideshowPanel.tsx +3 -3
  133. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  134. package/src/components/panels/VariablesBindElement.tsx +2 -2
  135. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  136. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  137. package/src/components/panels/VariablesPanel.tsx +4 -4
  138. package/src/components/panels/VariablesValueControls.tsx +2 -2
  139. package/src/components/pointerTargetSize.test.tsx +8 -0
  140. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  141. package/src/components/renders/RenderQueue.test.tsx +7 -0
  142. package/src/components/renders/RenderQueue.tsx +7 -7
  143. package/src/components/renders/RenderQueueItem.tsx +6 -6
  144. package/src/components/renders/RenderQueuePanel.tsx +1 -1
  145. package/src/components/sidebar/AssetCard.tsx +8 -8
  146. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  147. package/src/components/sidebar/AssetsTab.tsx +2 -2
  148. package/src/components/sidebar/AudioRow.tsx +6 -6
  149. package/src/components/sidebar/BlocksTab.tsx +6 -6
  150. package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
  151. package/src/components/sidebar/CompositionsPanel.tsx +15 -0
  152. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  153. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  154. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  155. package/src/components/sidebar/SidebarLintButton.test.tsx +53 -0
  156. package/src/components/sidebar/SidebarLintButton.tsx +50 -0
  157. package/src/components/timelineToolbarStyles.ts +5 -0
  158. package/src/components/ui/Button.test.tsx +252 -0
  159. package/src/components/ui/Button.tsx +89 -126
  160. package/src/components/ui/IconButton.tsx +38 -0
  161. package/src/components/ui/Input.tsx +115 -0
  162. package/src/components/ui/Menu.test.tsx +374 -0
  163. package/src/components/ui/Menu.tsx +213 -0
  164. package/src/components/ui/NumberField.tsx +122 -0
  165. package/src/components/ui/Popover.tsx +74 -0
  166. package/src/components/ui/SearchInput.tsx +2 -2
  167. package/src/components/ui/Select.tsx +100 -0
  168. package/src/components/ui/Slider.tsx +153 -0
  169. package/src/components/ui/Tabs.test.tsx +98 -0
  170. package/src/components/ui/Tabs.tsx +71 -0
  171. package/src/components/ui/Toggle.tsx +63 -0
  172. package/src/components/ui/Tooltip.test.tsx +110 -0
  173. package/src/components/ui/Tooltip.tsx +38 -112
  174. package/src/components/ui/cn.test.ts +30 -0
  175. package/src/components/ui/cn.ts +25 -0
  176. package/src/components/ui/index.ts +29 -2
  177. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  178. package/src/components/ui/valueControls.test.tsx +524 -0
  179. package/src/contexts/PanelLayoutContext.tsx +3 -57
  180. package/src/hooks/appHotkeysDispatch.ts +3 -4
  181. package/src/hooks/timelineEditPermission.ts +31 -0
  182. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
  183. package/src/hooks/useAppHotkeys.test.ts +0 -1
  184. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
  185. package/src/hooks/useAppHotkeys.ts +21 -84
  186. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  187. package/src/hooks/useCaptionDetection.ts +3 -1
  188. package/src/hooks/useDomEditPreviewSync.ts +4 -6
  189. package/src/hooks/useDomEditSession.ts +0 -7
  190. package/src/hooks/useDomEditWiring.ts +4 -9
  191. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  192. package/src/hooks/useEditHistoryActions.ts +106 -0
  193. package/src/hooks/useFileManager.ts +21 -123
  194. package/src/hooks/useLintModal.test.tsx +65 -0
  195. package/src/hooks/useLintModal.ts +3 -0
  196. package/src/hooks/usePanelLayout.test.ts +108 -296
  197. package/src/hooks/usePanelLayout.ts +50 -255
  198. package/src/hooks/usePersistentEditHistory.ts +1 -1
  199. package/src/hooks/useProjectFileWriter.ts +150 -0
  200. package/src/hooks/useRightPanelIntents.test.ts +181 -0
  201. package/src/hooks/useRightPanelIntents.ts +63 -0
  202. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  203. package/src/hooks/useSdkSession.ts +43 -10
  204. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  205. package/src/hooks/useServerConnection.ts +43 -10
  206. package/src/hooks/useSlideshowTabState.test.ts +9 -31
  207. package/src/hooks/useSlideshowTabState.ts +6 -19
  208. package/src/hooks/useStudioContextValue.test.ts +6 -18
  209. package/src/hooks/useStudioContextValue.ts +5 -26
  210. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  211. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  212. package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
  213. package/src/hooks/useTimelineEditing.test.tsx +322 -3
  214. package/src/hooks/useTimelineEditing.ts +103 -16
  215. package/src/hooks/useTimelineEditingTypes.ts +3 -0
  216. package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
  217. package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
  218. package/src/hostMount.test.tsx +222 -0
  219. package/src/index.ts +87 -0
  220. package/src/main.tsx +9 -3
  221. package/src/player/components/AudioWaveform.tsx +1 -1
  222. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  223. package/src/player/components/ClipContextMenu.tsx +3 -3
  224. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  225. package/src/player/components/CompositionThumbnail.tsx +3 -4
  226. package/src/player/components/EditModal.tsx +1 -1
  227. package/src/player/components/ImageThumbnail.tsx +1 -1
  228. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  229. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  230. package/src/player/components/PlayerControls.tsx +1 -1
  231. package/src/player/components/ShortcutsPanel.tsx +9 -9
  232. package/src/player/components/SpeedMenu.tsx +3 -5
  233. package/src/player/components/Timeline.tsx +3 -2
  234. package/src/player/components/TimelineCanvas.tsx +21 -14
  235. package/src/player/components/TimelineClip.tsx +0 -1
  236. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  237. package/src/player/components/TimelineEmptyState.tsx +5 -5
  238. package/src/player/components/TimelineFxButton.tsx +6 -6
  239. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  240. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  241. package/src/player/components/TimelineGroupRow.tsx +1 -1
  242. package/src/player/components/TimelineLanes.test.tsx +32 -0
  243. package/src/player/components/TimelineLanes.tsx +2 -5
  244. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  245. package/src/player/components/TimelineRuler.tsx +1 -1
  246. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  247. package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
  248. package/src/player/components/TimelineTrackHeader.tsx +2 -2
  249. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  250. package/src/player/components/TimelineTypes.ts +2 -1
  251. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  252. package/src/player/components/VideoThumbnail.tsx +4 -2
  253. package/src/player/components/VolumeControl.tsx +2 -2
  254. package/src/player/components/timelineCallbacks.ts +8 -8
  255. package/src/player/components/timelineClipDragPreview.ts +51 -27
  256. package/src/player/components/timelineClipDragTypes.ts +3 -0
  257. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  258. package/src/player/components/timelineDragDrop.ts +57 -15
  259. package/src/player/components/timelineLaneProps.ts +3 -0
  260. package/src/player/components/timelineSnapping.test.ts +27 -0
  261. package/src/player/components/timelineSnapping.ts +14 -1
  262. package/src/player/components/timelineTheme.test.ts +4 -4
  263. package/src/player/components/timelineTheme.ts +30 -30
  264. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  265. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  266. package/src/player/components/useTimelineClipDrag.ts +30 -3
  267. package/src/player/lib/describeClips.ts +3 -54
  268. package/src/player/lib/playbackShortcuts.ts +2 -0
  269. package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
  270. package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
  271. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  272. package/src/player/store/previewIframeStore.test.tsx +29 -0
  273. package/src/player/store/previewIframeStore.ts +25 -0
  274. package/src/renderQueueExports.test.tsx +58 -0
  275. package/src/styles/classCandidates.test.ts +105 -0
  276. package/src/styles/classCandidates.ts +281 -0
  277. package/src/styles/hex-baseline.json +84 -0
  278. package/src/styles/hexRatchet.test.ts +201 -0
  279. package/src/styles/iconTokens.ts +20 -0
  280. package/src/styles/palette-reset.css +6 -0
  281. package/src/styles/studio.css +24 -21
  282. package/src/styles/styleSources.test.ts +20 -0
  283. package/src/styles/styleSources.ts +47 -0
  284. package/src/styles/tailwind-preset.shared.js +3 -0
  285. package/src/styles/theme.css +270 -0
  286. package/src/styles/theme.test.ts +183 -0
  287. package/src/styles/tokenGate.test.ts +109 -0
  288. package/src/utils/sourcePatcher.ts +1 -1
  289. package/src/utils/studioHelpers.test.ts +18 -0
  290. package/src/utils/studioHelpers.ts +11 -8
  291. package/src/utils/studioUiPreferences.test.ts +10 -16
  292. package/src/utils/studioUiPreferences.ts +7 -16
  293. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  294. package/src/utils/timelineDropTrackInsert.ts +83 -0
  295. package/src/utils/typingTarget.ts +4 -0
  296. package/dist/assets/index-n2ml4tUn.js +0 -442
  297. package/dist/assets/index-pO6p0H27.css +0 -1
  298. package/src/components/PanelTabButton.tsx +0 -31
  299. package/src/components/StudioLeftSidebar.tsx +0 -204
  300. package/src/components/nle/TimelineResizeDivider.tsx +0 -101
  301. package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
  302. package/src/components/sidebar/LeftSidebar.tsx +0 -334
  303. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  304. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  305. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  306. package/src/components/storyboard/FramePoster.tsx +0 -86
  307. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  308. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  309. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  310. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  311. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  312. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  313. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  314. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  315. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  316. package/src/components/storyboard/StoryboardView.tsx +0 -169
  317. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  318. package/src/components/storyboard/frameComments.test.ts +0 -95
  319. package/src/components/storyboard/frameComments.ts +0 -115
  320. package/src/components/storyboard/frameStatus.ts +0 -36
  321. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  322. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  323. package/src/components/storyboard/useFrameComments.ts +0 -98
  324. package/src/contexts/ViewModeContext.tsx +0 -142
  325. package/src/hooks/useInspectorSplitResize.ts +0 -51
  326. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  327. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  328. package/src/hooks/useStoryboard.ts +0 -99
  329. package/src/utils/fitPanels.test.ts +0 -164
  330. package/src/utils/fitPanels.ts +0 -151
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Input: a boxed text field. Typing edits a local draft; Enter and blur commit it,
3
+ * Escape abandons it. `onTrack` fires once per commit, never per keystroke.
4
+ */
5
+
6
+ import { Input as BaseInput } from "@base-ui/react/input";
7
+ import { useEffect, useRef, useState, type ComponentPropsWithoutRef } from "react";
8
+ import { cn } from "./cn";
9
+ import type { PreviewState } from "./Button";
10
+
11
+ /**
12
+ * Shared field look (also used by NumberField and Select). Each interactive state is written
13
+ * twice, real and `data-[preview-state=...]`; `valueControls.test.tsx` asserts the lists match.
14
+ */
15
+ export const fieldBase = cn(
16
+ "flex min-w-0 items-center gap-1.5 h-ctl px-2 rounded-md",
17
+ "bg-input border border-border-input text-step-11 font-medium text-text-1",
18
+ "transition-[border-color,background-color] ease-standard duration-focus",
19
+ "hover:border-border-strong data-[preview-state=hover]:border-border-strong",
20
+ "outline-hidden",
21
+ "focus-within:outline-solid focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-accent",
22
+ "data-[preview-state=focus]:outline-solid data-[preview-state=focus]:outline-2 data-[preview-state=focus]:outline-offset-1 data-[preview-state=focus]:outline-accent",
23
+ // `aria-invalid` is not one of Tailwind's built-in aria variants, so the
24
+ // arbitrary form is the one that compiles. The token gate would have caught
25
+ // the bare `aria-invalid:` spelling as a class that resolves to nothing.
26
+ "aria-[invalid]:border-danger",
27
+ "has-[:disabled]:opacity-40 has-[:disabled]:cursor-not-allowed",
28
+ );
29
+
30
+ /** The text itself, inside the box. The box owns the border and the ring. */
31
+ export const fieldText = cn(
32
+ "min-w-0 w-full bg-transparent text-inherit outline-hidden",
33
+ "placeholder:text-text-5 disabled:cursor-not-allowed",
34
+ );
35
+
36
+ export interface InputProps extends Omit<
37
+ ComponentPropsWithoutRef<"input">,
38
+ "value" | "onChange" | "className"
39
+ > {
40
+ /** The committed value. A draft lives inside until Enter or blur. */
41
+ value: string;
42
+ /** Called with the draft on Enter and on blur, only when it really changed. */
43
+ onCommit: (next: string) => void;
44
+ /** Called once per committed change, for design-input telemetry. */
45
+ onTrack?: () => void;
46
+ /** Marks the field invalid: red boundary and `aria-invalid`. */
47
+ invalid?: boolean;
48
+ className?: string;
49
+ "data-preview-state"?: PreviewState;
50
+ }
51
+
52
+ export function Input({
53
+ value,
54
+ onCommit,
55
+ onTrack,
56
+ invalid,
57
+ disabled,
58
+ className,
59
+ "data-preview-state": previewState,
60
+ ...props
61
+ }: InputProps) {
62
+ const [draft, setDraft] = useState(value);
63
+ const valueRef = useRef(value);
64
+ valueRef.current = value;
65
+ const dirtyRef = useRef(false);
66
+
67
+ // An external write (a seek, an undo, another panel) is authoritative, but it
68
+ // must not eat what the user is halfway through typing.
69
+ useEffect(() => {
70
+ if (!dirtyRef.current) setDraft(value);
71
+ }, [value]);
72
+
73
+ const commit = (next: string) => {
74
+ dirtyRef.current = false;
75
+ if (next === valueRef.current) return;
76
+ onCommit(next);
77
+ onTrack?.();
78
+ };
79
+
80
+ return (
81
+ <div
82
+ className={cn(fieldBase, className)}
83
+ aria-invalid={invalid || undefined}
84
+ data-preview-state={previewState}
85
+ >
86
+ <BaseInput
87
+ value={draft}
88
+ disabled={disabled}
89
+ aria-invalid={invalid || undefined}
90
+ className={fieldText}
91
+ onValueChange={(next) => {
92
+ dirtyRef.current = true;
93
+ setDraft(next);
94
+ }}
95
+ onBlur={() => commit(draft)}
96
+ onKeyDown={(event) => {
97
+ if (event.key === "Enter") {
98
+ event.preventDefault();
99
+ commit(draft);
100
+ return;
101
+ }
102
+ if (event.key === "Escape") {
103
+ // Abandon the draft rather than leave it for the blur commit.
104
+ event.preventDefault();
105
+ event.stopPropagation();
106
+ dirtyRef.current = false;
107
+ setDraft(valueRef.current);
108
+ event.currentTarget.blur();
109
+ }
110
+ }}
111
+ {...props}
112
+ />
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,374 @@
1
+ // @vitest-environment happy-dom
2
+ /** Menu, ContextMenu and Popover: key/aria behaviour, dismissal, and that emitted classes resolve. */
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import React, { act, useState } from "react";
6
+ import { createRoot, type Root } from "react-dom/client";
7
+ import { compile } from "tailwindcss";
8
+ import { afterEach, beforeAll, describe, expect, it } from "vitest";
9
+ import { loadStylesheet, STYLES_DIR } from "../../styles/styleSources";
10
+ import { ContextMenu, Menu, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator } from "./Menu";
11
+ import { Popover } from "./Popover";
12
+ import { isTypingTarget } from "../../utils/typingTarget";
13
+ import { shouldIgnorePlaybackShortcutTarget } from "../../player/lib/playbackShortcuts";
14
+
15
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
16
+
17
+ let mounted: { root: Root; host: HTMLElement } | null = null;
18
+
19
+ function render(element: React.ReactElement): HTMLElement {
20
+ const host = document.createElement("div");
21
+ document.body.append(host);
22
+ const root = createRoot(host);
23
+ mounted = { root, host };
24
+ act(() => root.render(element));
25
+ return host;
26
+ }
27
+
28
+ afterEach(() => {
29
+ if (!mounted) return;
30
+ const { root, host } = mounted;
31
+ mounted = null;
32
+ act(() => root.unmount());
33
+ host.remove();
34
+ });
35
+
36
+ /** Base UI moves focus into the popup one task after open, not synchronously. */
37
+ async function settle(): Promise<void> {
38
+ await act(async () => {
39
+ await new Promise((resolve) => setTimeout(resolve, 0));
40
+ });
41
+ }
42
+
43
+ function press(target: Element): void {
44
+ // `composed` is what a real browser sets: without it the event never crosses
45
+ // a shadow boundary and Base UI's document listener never sees it.
46
+ const init = { bubbles: true, cancelable: true, composed: true };
47
+ act(() => {
48
+ target.dispatchEvent(new PointerEvent("pointerdown", init));
49
+ target.dispatchEvent(new MouseEvent("mousedown", init));
50
+ });
51
+ }
52
+
53
+ /**
54
+ * A mouse click, spelled out: bare `.click()` reads as keyboard activation to Base UI,
55
+ * which pre-highlights the first item and shifts every arrow-key assertion by one.
56
+ */
57
+ function clickWithMouse(target: Element): void {
58
+ const init = { bubbles: true, cancelable: true, composed: true, detail: 1 };
59
+ act(() => {
60
+ target.dispatchEvent(new PointerEvent("pointerdown", { ...init, pointerType: "mouse" }));
61
+ target.dispatchEvent(new MouseEvent("mousedown", init));
62
+ target.dispatchEvent(new PointerEvent("pointerup", { ...init, pointerType: "mouse" }));
63
+ target.dispatchEvent(new MouseEvent("mouseup", init));
64
+ target.dispatchEvent(new MouseEvent("click", init));
65
+ });
66
+ }
67
+
68
+ function key(k: string, target: Element | null = document.activeElement): void {
69
+ const init = { key: k, bubbles: true, cancelable: true, composed: true };
70
+ act(() => {
71
+ (target ?? document.body).dispatchEvent(new KeyboardEvent("keydown", init));
72
+ (target ?? document.body).dispatchEvent(new KeyboardEvent("keyup", init));
73
+ });
74
+ }
75
+
76
+ function one<T extends Element>(selector: string, what: string): T {
77
+ const el = document.querySelector<T>(selector);
78
+ if (!el) throw new Error(`${what} not rendered`);
79
+ return el;
80
+ }
81
+
82
+ const trigger = () => one<HTMLElement>('[data-testid="trigger"]', "trigger");
83
+ const popup = () => document.querySelector<HTMLElement>('[role="menu"]');
84
+ const items = () => [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')];
85
+
86
+ /** A Studio element menu: actions, a hairline, a destructive item; `disabled` marks the passed label. */
87
+ function ActionMenu({
88
+ activated,
89
+ disable,
90
+ }: {
91
+ activated: string[];
92
+ disable?: string;
93
+ }): React.JSX.Element {
94
+ return (
95
+ <Menu trigger={<button data-testid="trigger">Actions</button>} aria-label="Element actions">
96
+ <MenuItem shortcut="⌘]" onClick={() => activated.push("front")}>
97
+ Bring to front
98
+ </MenuItem>
99
+ <MenuItem disabled={disable === "group"} onClick={() => activated.push("group")}>
100
+ Group
101
+ </MenuItem>
102
+ <MenuSeparator />
103
+ <MenuItem tone="danger" shortcut="⌫" onClick={() => activated.push("delete")}>
104
+ Delete
105
+ </MenuItem>
106
+ </Menu>
107
+ );
108
+ }
109
+
110
+ async function openActionMenu(activated: string[] = [], disable?: string): Promise<void> {
111
+ render(<ActionMenu activated={activated} disable={disable} />);
112
+ clickWithMouse(trigger());
113
+ await settle();
114
+ expect(popup()).not.toBeNull();
115
+ }
116
+
117
+ describe("Menu behaviour", () => {
118
+ // AE5.
119
+ it("activates the second item with ArrowDown twice then Enter, and closes", async () => {
120
+ const activated: string[] = [];
121
+ await openActionMenu(activated);
122
+
123
+ key("ArrowDown");
124
+ key("ArrowDown");
125
+ key("Enter");
126
+ await settle();
127
+
128
+ expect(activated).toEqual(["group"]);
129
+ expect(popup()).toBeNull();
130
+ });
131
+
132
+ it("does not activate a disabled item, and the arrow keys reach past it", async () => {
133
+ const activated: string[] = [];
134
+ await openActionMenu(activated, "group");
135
+
136
+ const [, disabled, last] = items();
137
+ expect(disabled.getAttribute("data-disabled")).not.toBeNull();
138
+ expect(disabled.getAttribute("aria-disabled")).toBe("true");
139
+
140
+ // Base UI keeps a disabled item in the roving sequence (the ARIA menu
141
+ // pattern: a disabled command stays discoverable). So the guarantee is not
142
+ // that the highlight skips it, it is that nothing runs when it is aimed at
143
+ // directly, by key or by pointer.
144
+ key("ArrowDown");
145
+ key("ArrowDown");
146
+ expect(disabled.getAttribute("data-highlighted")).not.toBeNull();
147
+ key("Enter", disabled);
148
+ act(() => disabled.click());
149
+ await settle();
150
+
151
+ expect(activated).toEqual([]);
152
+ expect(popup()).not.toBeNull();
153
+
154
+ // And the item after it is still reachable, so a disabled row is not a wall.
155
+ key("ArrowDown");
156
+ key("Enter");
157
+ await settle();
158
+
159
+ expect(activated).toEqual(["delete"]);
160
+ expect(last.isConnected).toBe(false);
161
+ });
162
+
163
+ it("renders a shortcut hint that assistive tech does not read as a label", async () => {
164
+ await openActionMenu();
165
+
166
+ const hint = one<HTMLElement>('[role="menuitem"] span[aria-hidden="true"]', "shortcut hint");
167
+
168
+ expect(hint.textContent).toBe("⌘]");
169
+ expect(items()[0].textContent).toContain("Bring to front");
170
+ });
171
+
172
+ it("closes on an outside press that a parent stops in the bubble phase", async () => {
173
+ // U3's finding, on the shipped component: the canvas overlay swallows
174
+ // bubble-phase pointer events, and Base UI's dismissal listens in capture.
175
+ const swallow = (event: React.SyntheticEvent) => event.stopPropagation();
176
+ render(
177
+ <div onPointerDown={swallow} onMouseDown={swallow}>
178
+ <div data-testid="overlay" aria-label="Composition canvas">
179
+ overlay
180
+ </div>
181
+ <Menu trigger={<button data-testid="trigger">Actions</button>} aria-label="Actions">
182
+ <MenuItem>Bring to front</MenuItem>
183
+ </Menu>
184
+ </div>,
185
+ );
186
+ act(() => trigger().click());
187
+ await settle();
188
+ expect(popup()).not.toBeNull();
189
+
190
+ // Witness: a bubble-phase document listener is the thing the overlay
191
+ // starves. If it starts firing, the press stopped being swallowed and this
192
+ // test would pass for a reason that says nothing about capture-phase
193
+ // dismissal.
194
+ let sawBubblePress = false;
195
+ const witness = () => {
196
+ sawBubblePress = true;
197
+ };
198
+ document.addEventListener("pointerdown", witness);
199
+ press(one('[data-testid="overlay"]', "overlay"));
200
+ document.removeEventListener("pointerdown", witness);
201
+
202
+ expect(sawBubblePress).toBe(false);
203
+ expect(popup()).toBeNull();
204
+ });
205
+ });
206
+
207
+ describe("MenuRadioGroup", () => {
208
+ it("selects one item and unselects the rest, and reports aria-checked", async () => {
209
+ function SpeedMenu(): React.JSX.Element {
210
+ const [rate, setRate] = useState(1);
211
+ return (
212
+ <Menu trigger={<button data-testid="trigger">{rate}x</button>} aria-label="Playback speed">
213
+ <MenuRadioGroup value={rate} onValueChange={(next) => setRate(next as number)}>
214
+ {[0.5, 1, 2].map((speed) => (
215
+ <MenuRadioItem key={speed} value={speed}>
216
+ {speed}x
217
+ </MenuRadioItem>
218
+ ))}
219
+ </MenuRadioGroup>
220
+ </Menu>
221
+ );
222
+ }
223
+ render(<SpeedMenu />);
224
+ act(() => trigger().click());
225
+ await settle();
226
+
227
+ const radios = () => [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')];
228
+ const checked = () => radios().filter((el) => el.getAttribute("aria-checked") === "true");
229
+
230
+ expect(radios()).toHaveLength(3);
231
+ expect(checked().map((el) => el.textContent)).toEqual(["1x"]);
232
+
233
+ act(() => radios()[2].click());
234
+ await settle();
235
+
236
+ expect(checked().map((el) => el.textContent)).toEqual(["2x"]);
237
+ expect(trigger().textContent).toBe("2x");
238
+ });
239
+ });
240
+
241
+ describe("ContextMenu", () => {
242
+ it("opens on a right click over its area and closes when an item runs", async () => {
243
+ const activated: string[] = [];
244
+ render(
245
+ <ContextMenu
246
+ aria-label="Clip actions"
247
+ trigger={
248
+ <div data-testid="trigger">
249
+ <span>clip</span>
250
+ </div>
251
+ }
252
+ >
253
+ <MenuItem onClick={() => activated.push("split")}>Split</MenuItem>
254
+ </ContextMenu>,
255
+ );
256
+
257
+ // The trigger renders the caller's element and keeps its children.
258
+ expect(trigger().textContent).toBe("clip");
259
+ expect(popup()).toBeNull();
260
+
261
+ act(() => {
262
+ trigger().dispatchEvent(
263
+ new MouseEvent("contextmenu", { bubbles: true, cancelable: true, composed: true }),
264
+ );
265
+ });
266
+ await settle();
267
+ expect(popup()).not.toBeNull();
268
+
269
+ act(() => items()[0].click());
270
+ await settle();
271
+
272
+ expect(activated).toEqual(["split"]);
273
+ expect(popup()).toBeNull();
274
+ });
275
+ });
276
+
277
+ describe("Popover", () => {
278
+ it("leaves the keys of a text field inside it alone", async () => {
279
+ // The Menu owns ArrowDown and typeahead; a Popover must not, or the rename
280
+ // field in AssetContextMenu would lose its own caret keys.
281
+ render(
282
+ <Popover trigger={<button data-testid="trigger">Rename</button>} aria-label="Rename asset">
283
+ <input data-testid="rename" defaultValue="scene1" />
284
+ </Popover>,
285
+ );
286
+ act(() => trigger().click());
287
+ await settle();
288
+
289
+ const input = one<HTMLInputElement>('[data-testid="rename"]', "rename field");
290
+ act(() => input.focus());
291
+ expect(document.activeElement).toBe(input);
292
+
293
+ key("ArrowDown");
294
+ key("ArrowUp");
295
+ key("s");
296
+ await settle();
297
+
298
+ expect(document.activeElement).toBe(input);
299
+ expect(document.querySelector('[role="menu"]')).toBeNull();
300
+ });
301
+ });
302
+
303
+ describe("hotkey classification", () => {
304
+ it("classifies menu rows and popover contents the way the menus they replace were", async () => {
305
+ // `typingTarget` and `playbackShortcuts` match exact role strings; Base UI renders
306
+ // a `<div>` with those roles, so the roles alone must carry the classification.
307
+ render(
308
+ <>
309
+ <Menu trigger={<button data-testid="trigger">Actions</button>} aria-label="Actions">
310
+ <MenuItem>Split</MenuItem>
311
+ <MenuRadioGroup value={1}>
312
+ <MenuRadioItem value={1}>1x</MenuRadioItem>
313
+ </MenuRadioGroup>
314
+ </Menu>
315
+ </>,
316
+ );
317
+ act(() => trigger().click());
318
+ await settle();
319
+
320
+ const row = one('[role="menuitem"]', "menu item");
321
+ const radio = one('[role="menuitemradio"]', "radio item");
322
+
323
+ for (const el of [row, radio]) {
324
+ expect(isTypingTarget(el)).toBe(false);
325
+ expect(shouldIgnorePlaybackShortcutTarget(el)).toBe(true);
326
+ }
327
+ });
328
+
329
+ it("treats a field inside a Popover as somewhere the user is typing", async () => {
330
+ render(
331
+ <Popover trigger={<button data-testid="trigger">Rename</button>} aria-label="Rename asset">
332
+ <input data-testid="rename" defaultValue="scene1" />
333
+ </Popover>,
334
+ );
335
+ act(() => trigger().click());
336
+ await settle();
337
+
338
+ const input = one('[data-testid="rename"]', "rename field");
339
+
340
+ expect(isTypingTarget(input)).toBe(true);
341
+ expect(shouldIgnorePlaybackShortcutTarget(input)).toBe(true);
342
+ });
343
+ });
344
+
345
+ /**
346
+ * Class resolution is covered by `styles/tokenGate.test.ts`; this checks what the compiled rule
347
+ * does (AE4).
348
+ */
349
+ describe("open motion", () => {
350
+ let compileStudioCss: (candidates: string[]) => string;
351
+
352
+ beforeAll(async () => {
353
+ const compiled = await compile(readFileSync(path.join(STYLES_DIR, "studio.css"), "utf8"), {
354
+ base: STYLES_DIR,
355
+ // Tailwind's signature wants a promise; the shared resolver is sync.
356
+ loadStylesheet: async (id, base) => loadStylesheet(id, base),
357
+ });
358
+ compileStudioCss = (candidates) => compiled.build(candidates);
359
+ });
360
+
361
+ // AE4.
362
+ it("names a motion token that zeroes itself under reduced motion", async () => {
363
+ // The zero-duration case lives in the `duration-open` utility itself
364
+ // (theme.css), not in a `motion-reduce:` class beside it, so a caller
365
+ // cannot use the token and forget the reduced-motion half. The menu's job
366
+ // is to name the token; the stylesheet's job is the media query.
367
+ await openActionMenu();
368
+
369
+ expect([...popup()!.classList]).toContain("duration-open");
370
+ expect(compileStudioCss(["duration-open"])).toMatch(
371
+ /\.duration-open \{[\s\S]*?prefers-reduced-motion: reduce[\s\S]*?transition-duration: 0ms/,
372
+ );
373
+ });
374
+ });