@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,98 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * Tabs: proves the shared strip reaches every tab by arrow key, something
5
+ * three of Studio's six hand-rolled strips don't.
6
+ */
7
+ import React, { act } from "react";
8
+ import { afterEach, expect, it } from "vitest";
9
+ import { cleanupMounted, mountHost } from "./mountHost.testHelpers";
10
+ import { Tab, TabPanel, Tabs, TabsList } from "./Tabs";
11
+
12
+ const TABS = ["code", "comps", "assets", "catalog"];
13
+
14
+ afterEach(cleanupMounted);
15
+
16
+ function mount(): HTMLElement {
17
+ return mountHost(
18
+ <Tabs defaultValue="code">
19
+ <TabsList aria-label="Sidebar panels">
20
+ {TABS.map((id) => (
21
+ <Tab key={id} value={id}>
22
+ {id}
23
+ </Tab>
24
+ ))}
25
+ </TabsList>
26
+ {TABS.map((id) => (
27
+ <TabPanel key={id} value={id}>
28
+ {id} panel
29
+ </TabPanel>
30
+ ))}
31
+ </Tabs>,
32
+ );
33
+ }
34
+
35
+ function tab(host: HTMLElement, id: string): HTMLElement {
36
+ const el = host.querySelector<HTMLElement>(`[data-tab-id="${id}"]`);
37
+ if (!el) throw new Error(`no tab for ${id}`);
38
+ return el;
39
+ }
40
+
41
+ function selected(host: HTMLElement): string | null {
42
+ return (
43
+ host.querySelector('[role="tab"][aria-selected="true"]')?.getAttribute("data-tab-id") ?? null
44
+ );
45
+ }
46
+
47
+ /** Base UI moves the roving tabindex synchronously and the focus one task later. */
48
+ async function arrow(key: string): Promise<void> {
49
+ const target = document.activeElement ?? document.body;
50
+ act(() => {
51
+ target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, composed: true }));
52
+ });
53
+ await act(async () => {
54
+ await new Promise((resolve) => setTimeout(resolve, 0));
55
+ });
56
+ }
57
+
58
+ it("moves selection and focus with the arrow keys", async () => {
59
+ const host = mount();
60
+ act(() => tab(host, "code").focus());
61
+
62
+ await arrow("ArrowRight");
63
+
64
+ expect(selected(host)).toBe("comps");
65
+ expect(document.activeElement).toBe(tab(host, "comps"));
66
+ });
67
+
68
+ it("jumps to the first and last tab with Home and End", async () => {
69
+ const host = mount();
70
+ act(() => tab(host, "code").focus());
71
+
72
+ await arrow("End");
73
+ expect(selected(host)).toBe("catalog");
74
+ expect(document.activeElement).toBe(tab(host, "catalog"));
75
+
76
+ await arrow("Home");
77
+ expect(selected(host)).toBe("code");
78
+ expect(document.activeElement).toBe(tab(host, "code"));
79
+ });
80
+
81
+ it("labels the visible panel with its tab", () => {
82
+ const host = mount();
83
+ const panel = host.querySelector('[role="tabpanel"]:not([hidden])');
84
+
85
+ expect(panel?.textContent).toBe("code panel");
86
+ expect(panel?.getAttribute("aria-labelledby")).toBe(tab(host, "code").id);
87
+ });
88
+
89
+ it("carries data-tab-id, which the sidebar's focus restore looks up", () => {
90
+ // LeftSidebar finds a tab with `[data-tab-id="…"]` to restore focus after a
91
+ // programmatic switch. An attribute the primitive owns survives a reskin;
92
+ // the class it used to match on does not.
93
+ const host = mount();
94
+
95
+ expect(
96
+ [...host.querySelectorAll("[data-tab-id]")].map((el) => el.getAttribute("data-tab-id")),
97
+ ).toEqual(TABS);
98
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tabs — Base UI's tabs wearing Studio's tokens; replaces six hand-rolled
3
+ * strips with one implementation (roving tabindex, arrow/Home/End, `aria-controls`).
4
+ */
5
+
6
+ import { Tabs as BaseTabs } from "@base-ui/react/tabs";
7
+ import type { ComponentPropsWithoutRef, ElementType } from "react";
8
+ import { cn } from "./cn";
9
+ import type { PreviewState } from "./Button";
10
+
11
+ /**
12
+ * Base UI lets `className` be a function of the part's state. Studio's parts
13
+ * merge a string with `cn`, so the string form is the one they accept.
14
+ */
15
+ type StyledProps<T extends ElementType> = Omit<ComponentPropsWithoutRef<T>, "className"> & {
16
+ className?: string;
17
+ };
18
+
19
+ /**
20
+ * Groups a tab list with its panels. Uncontrolled by default; pass `value` and
21
+ * `onValueChange` for a strip whose selection lives in a store.
22
+ */
23
+ export function Tabs(props: ComponentPropsWithoutRef<typeof BaseTabs.Root>) {
24
+ return <BaseTabs.Root {...props} />;
25
+ }
26
+
27
+ /** The strip. Give it an `aria-label`: a tablist with no name is unlabelled. */
28
+ export function TabsList({ className, ...props }: StyledProps<typeof BaseTabs.List>) {
29
+ return (
30
+ <BaseTabs.List
31
+ // Arrow keys move selection, matching today's sidebar strip.
32
+ activateOnFocus
33
+ className={cn("inline-flex items-center gap-0.5 rounded-lg bg-surface-alt p-1", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ interface TabProps extends StyledProps<typeof BaseTabs.Tab> {
40
+ value: string;
41
+ "data-preview-state"?: PreviewState;
42
+ }
43
+
44
+ /**
45
+ * aria-selected drives the selected look (Base UI sets it) so assistive tech
46
+ * and sighted users match. data-preview-state is for gallery shots only.
47
+ */
48
+ export function Tab({ value, className, ...props }: TabProps) {
49
+ return (
50
+ <BaseTabs.Tab
51
+ value={value}
52
+ data-tab-id={value}
53
+ className={cn(
54
+ "inline-flex h-ctl-sm cursor-pointer select-none items-center justify-center rounded-sm px-2.5",
55
+ "text-step-11 font-semibold whitespace-nowrap text-text-3",
56
+ "transition-[background-color,color] ease-out-quint duration-hover",
57
+ "hover:text-text-1 data-[preview-state=hover]:text-text-1",
58
+ "aria-selected:bg-hover aria-selected:text-text-0",
59
+ "outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent",
60
+ "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",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ /** The panel for one tab. Base UI wires `aria-labelledby` back to its tab. */
69
+ export function TabPanel({ className, ...props }: StyledProps<typeof BaseTabs.Panel>) {
70
+ return <BaseTabs.Panel className={cn("outline-hidden", className)} {...props} />;
71
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Toggle: a switch, not a checkbox (immediate effect). `role="switch"` needed an entry in
3
+ * `typingTarget.ts` so Space is not leaked to the global playback shortcut.
4
+ */
5
+
6
+ import { Switch } from "@base-ui/react/switch";
7
+ import { cn } from "./cn";
8
+ import type { PreviewState } from "./Button";
9
+
10
+ export interface ToggleProps {
11
+ /** Accessible name. */
12
+ label: string;
13
+ checked: boolean;
14
+ /** Called on every flip. */
15
+ onCommit: (next: boolean) => void;
16
+ /** Called once per flip, for design-input telemetry. */
17
+ onTrack?: () => void;
18
+ disabled?: boolean;
19
+ className?: string;
20
+ "data-preview-state"?: PreviewState;
21
+ }
22
+
23
+ export function Toggle({
24
+ label,
25
+ checked,
26
+ onCommit,
27
+ onTrack,
28
+ disabled,
29
+ className,
30
+ "data-preview-state": previewState,
31
+ }: ToggleProps) {
32
+ return (
33
+ <Switch.Root
34
+ aria-label={label}
35
+ checked={checked}
36
+ disabled={disabled}
37
+ onCheckedChange={(next) => {
38
+ onCommit(next);
39
+ onTrack?.();
40
+ }}
41
+ data-preview-state={previewState}
42
+ className={cn(
43
+ "relative inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full p-0.5",
44
+ "border border-border-input bg-input",
45
+ "transition-[background-color,border-color] ease-standard duration-press",
46
+ "hover:border-border-strong data-[preview-state=hover]:border-border-strong",
47
+ "data-[checked]:border-accent data-[checked]:bg-accent",
48
+ "outline-hidden focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent",
49
+ "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",
50
+ "disabled:cursor-not-allowed disabled:opacity-40",
51
+ className,
52
+ )}
53
+ >
54
+ <Switch.Thumb
55
+ className={cn(
56
+ "size-3 rounded-full bg-text-3",
57
+ "transition-[transform,background-color] ease-standard duration-press",
58
+ "data-[checked]:translate-x-3 data-[checked]:bg-bg-0",
59
+ )}
60
+ />
61
+ </Switch.Root>
62
+ );
63
+ }
@@ -0,0 +1,110 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * Tooltip a11y contract Base UI must not lose: focus opens it, Escape
5
+ * dismisses it (WCAG 1.4.13), aria-describedby ties trigger to bubble (4.1.2).
6
+ */
7
+ import React, { act } from "react";
8
+ import { afterEach, expect, it } from "vitest";
9
+ import { cleanupMounted, mountHost } from "./mountHost.testHelpers";
10
+ import { Tooltip } from "./Tooltip";
11
+
12
+ afterEach(cleanupMounted);
13
+
14
+ function mount(): HTMLElement {
15
+ const host = mountHost(
16
+ <Tooltip label="Selection tool (V)" delay={0}>
17
+ <button type="button" data-testid="trigger">
18
+ V
19
+ </button>
20
+ </Tooltip>,
21
+ );
22
+ const trigger = host.querySelector<HTMLElement>('[data-testid="trigger"]');
23
+ if (!trigger) throw new Error("trigger not rendered");
24
+ return trigger;
25
+ }
26
+
27
+ /** Base UI opens and closes across a task, not synchronously. */
28
+ async function settle(): Promise<void> {
29
+ await act(async () => {
30
+ await new Promise((resolve) => setTimeout(resolve, 0));
31
+ });
32
+ }
33
+
34
+ /** Browsers send hover to the wrapper, never to a disabled control inside it. */
35
+ function hover(trigger: HTMLElement): void {
36
+ const box = trigger.parentElement ?? trigger;
37
+ act(() => {
38
+ box.dispatchEvent(new PointerEvent("pointerenter", { bubbles: false, composed: true }));
39
+ box.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false, composed: true }));
40
+ box.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, composed: true }));
41
+ });
42
+ }
43
+
44
+ function bubble(): HTMLElement | null {
45
+ return document.querySelector<HTMLElement>('[role="tooltip"]');
46
+ }
47
+
48
+ it("opens on focus and describes its trigger", async () => {
49
+ const trigger = mount();
50
+
51
+ expect(bubble()).toBeNull();
52
+ act(() => trigger.focus());
53
+ await settle();
54
+
55
+ const tip = bubble();
56
+ expect(tip?.textContent).toBe("Selection tool (V)");
57
+ expect(trigger.getAttribute("aria-describedby")).toBe(tip?.id);
58
+ });
59
+
60
+ it("opens on pointer hover", async () => {
61
+ const trigger = mount();
62
+
63
+ hover(trigger);
64
+ await settle();
65
+
66
+ expect(bubble()?.textContent).toBe("Selection tool (V)");
67
+ });
68
+
69
+ it("closes on Escape and drops the description", async () => {
70
+ const trigger = mount();
71
+ act(() => trigger.focus());
72
+ await settle();
73
+ expect(bubble()).not.toBeNull();
74
+
75
+ act(() => {
76
+ document.dispatchEvent(
77
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true, composed: true }),
78
+ );
79
+ });
80
+ await settle();
81
+
82
+ expect(bubble()).toBeNull();
83
+ expect(trigger.getAttribute("aria-describedby")).toBeNull();
84
+ });
85
+
86
+ it("opens on hover over a disabled child", async () => {
87
+ const host = mountHost(
88
+ <Tooltip label="Rendering" delay={0}>
89
+ <button type="button" disabled data-testid="off">
90
+ Export
91
+ </button>
92
+ </Tooltip>,
93
+ );
94
+ const off = host.querySelector<HTMLElement>('[data-testid="off"]');
95
+ if (!off) throw new Error("trigger not rendered");
96
+
97
+ hover(off);
98
+ await settle();
99
+
100
+ expect(bubble()?.textContent).toBe("Rendering");
101
+ });
102
+
103
+ it("wraps its child in a box-less span so a disabled child still gets hover", () => {
104
+ const trigger = mount();
105
+
106
+ expect(trigger.tagName).toBe("BUTTON");
107
+ expect(trigger.parentElement?.tagName).toBe("SPAN");
108
+ expect(trigger.parentElement?.className).toBe("contents");
109
+ expect(trigger.parentElement?.parentElement?.parentElement).toBe(document.body);
110
+ });
@@ -1,127 +1,53 @@
1
- import {
2
- useState,
3
- useRef,
4
- useCallback,
5
- useEffect,
6
- useLayoutEffect,
7
- useId,
8
- type ReactNode,
9
- } from "react";
10
- import { createPortal } from "react-dom";
11
- import { clampCentredLeft } from "../editor/floatingPanel";
1
+ /**
2
+ * Tooltip: Base UI's tooltip in Studio tokens. The trigger is a box-less
3
+ * `display: contents` wrapper so a disabled control still gets hover.
4
+ */
5
+
6
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
7
+ import { cloneElement, useId, useRef, useState, type ReactElement } from "react";
12
8
 
13
9
  interface TooltipProps {
14
10
  label: string;
15
- children: ReactNode;
11
+ /** A single element, wrapped in a box-less span so a disabled one still gets hover. */
12
+ children: ReactElement<{ "aria-describedby"?: string }>;
13
+ /** Hover delay in ms. */
16
14
  delay?: number;
17
- side?: "top" | "bottom";
15
+ side?: "top" | "bottom" | "left" | "right";
18
16
  }
19
17
 
20
- // Rough bubble height (padding + one text line) used to decide flipping
21
- // before the bubble has rendered; exact height isn't needed for the guard.
22
- const APPROX_BUBBLE_H = 28;
18
+ /** Matches the old bubble's gap from its trigger, and its viewport margin. */
19
+ const SIDE_OFFSET = 6;
23
20
  const VIEWPORT_MARGIN = 8;
24
21
 
25
22
  export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipProps) {
26
- const [visible, setVisible] = useState(false);
27
- const [pos, setPos] = useState({ x: 0, y: 0 });
28
- const [resolvedSide, setResolvedSide] = useState<"top" | "bottom">(side);
29
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
30
- const triggerRef = useRef<HTMLSpanElement>(null);
31
- const bubbleRef = useRef<HTMLDivElement>(null);
32
- const [bubbleWidth, setBubbleWidth] = useState(0);
33
- // WCAG 4.1.2: programmatically associate the bubble with its trigger.
23
+ const [open, setOpen] = useState(false);
24
+ const boxRef = useRef<HTMLSpanElement>(null);
34
25
  const tooltipId = useId();
35
26
 
36
- const show = useCallback(() => {
37
- if (timerRef.current) clearTimeout(timerRef.current);
38
- timerRef.current = setTimeout(() => {
39
- const el = triggerRef.current;
40
- if (!el) return;
41
- const child = el.firstElementChild as HTMLElement | null;
42
- const rect = (child ?? el).getBoundingClientRect();
43
- if (rect.width === 0 && rect.height === 0) return;
44
- // Flip when the preferred side would clip the viewport edge.
45
- let nextSide = side;
46
- if (side === "top" && rect.top - APPROX_BUBBLE_H - 6 < VIEWPORT_MARGIN) {
47
- nextSide = "bottom";
48
- } else if (
49
- side === "bottom" &&
50
- rect.bottom + APPROX_BUBBLE_H + 6 > window.innerHeight - VIEWPORT_MARGIN
51
- ) {
52
- nextSide = "top";
53
- }
54
- setResolvedSide(nextSide);
55
- setPos({
56
- // Raw trigger centre; clamped to the viewport at render, once the
57
- // bubble's own width is known (see clampedX).
58
- x: rect.left + rect.width / 2,
59
- y: nextSide === "top" ? rect.top - 6 : rect.bottom + 6,
60
- });
61
- setVisible(true);
62
- }, delay);
63
- }, [delay, side]);
64
-
65
- const hide = useCallback(() => {
66
- if (timerRef.current) {
67
- clearTimeout(timerRef.current);
68
- timerRef.current = null;
69
- }
70
- setVisible(false);
71
- }, []);
72
-
73
- // Measure before paint so a wide bubble near a viewport edge is clamped in
74
- // the same commit it appears in (no visible jump).
75
- useLayoutEffect(() => {
76
- setBubbleWidth(visible ? (bubbleRef.current?.offsetWidth ?? 0) : 0);
77
- }, [visible, label]);
78
-
79
- // WCAG 1.4.13: tooltip content must be dismissible with Escape.
80
- useEffect(() => {
81
- if (!visible) return;
82
- const onKeyDown = (e: KeyboardEvent) => {
83
- if (e.key === "Escape") hide();
84
- };
85
- document.addEventListener("keydown", onKeyDown);
86
- return () => document.removeEventListener("keydown", onKeyDown);
87
- }, [visible, hide]);
88
-
89
- const clampedX = clampCentredLeft(pos.x, bubbleWidth, window.innerWidth, VIEWPORT_MARGIN);
90
-
91
27
  return (
92
- <>
93
- <span
94
- ref={triggerRef}
95
- onPointerEnter={show}
96
- onPointerLeave={hide}
97
- onFocus={show}
98
- onBlur={hide}
99
- aria-describedby={visible ? tooltipId : undefined}
100
- className="contents"
101
- >
102
- {children}
103
- </span>
104
- {visible &&
105
- createPortal(
106
- <div
107
- className="fixed z-[200] pointer-events-none"
108
- style={{
109
- left: clampedX,
110
- top: pos.y,
111
- transform: resolvedSide === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
112
- }}
28
+ <BaseTooltip.Root open={open} onOpenChange={setOpen}>
29
+ <BaseTooltip.Trigger delay={delay} render={<span ref={boxRef} className="contents" />}>
30
+ {cloneElement(children, {
31
+ "aria-describedby": open ? tooltipId : children.props["aria-describedby"],
32
+ })}
33
+ </BaseTooltip.Trigger>
34
+ <BaseTooltip.Portal>
35
+ <BaseTooltip.Positioner
36
+ anchor={() => boxRef.current?.firstElementChild ?? null}
37
+ side={side}
38
+ sideOffset={SIDE_OFFSET}
39
+ collisionPadding={VIEWPORT_MARGIN}
40
+ className="z-200"
41
+ >
42
+ <BaseTooltip.Popup
43
+ id={tooltipId}
44
+ role="tooltip"
45
+ className="pointer-events-none rounded-md border border-border-input bg-surface px-2 py-1 text-step-10 font-medium text-text-1 whitespace-nowrap shadow-menu"
113
46
  >
114
- <div
115
- ref={bubbleRef}
116
- role="tooltip"
117
- id={tooltipId}
118
- className="px-2 py-1 rounded-md bg-neutral-800 border border-neutral-700/50 text-[10px] font-medium text-neutral-200 whitespace-nowrap shadow-lg"
119
- >
120
- {label}
121
- </div>
122
- </div>,
123
- document.body,
124
- )}
125
- </>
47
+ {label}
48
+ </BaseTooltip.Popup>
49
+ </BaseTooltip.Positioner>
50
+ </BaseTooltip.Portal>
51
+ </BaseTooltip.Root>
126
52
  );
127
53
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The two things `cn` must do that a bare `clsx` cannot: drop a losing class
3
+ * from the same group, and know the two class families `theme.css` invents.
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { cn } from "./cn";
7
+
8
+ describe("cn", () => {
9
+ it("keeps only the last class from a group", () => {
10
+ expect(cn("text-sm", "text-step-11")).toBe("text-step-11");
11
+ expect(cn("text-step-11", "text-sm")).toBe("text-sm");
12
+ });
13
+
14
+ it("takes clsx-style conditionals", () => {
15
+ const cond = true;
16
+ expect(cn("px-3", cond && "px-4")).toBe("px-4");
17
+ expect(cn("px-3", !cond && "px-4")).toBe("px-3");
18
+ });
19
+
20
+ it("merges the control heights against each other", () => {
21
+ expect(cn("h-ctl", "h-ctl-lg")).toBe("h-ctl-lg");
22
+ expect(cn("h-ctl-lg", "h-8")).toBe("h-8");
23
+ expect(cn("size-ctl", "size-ctl-sm")).toBe("size-ctl-sm");
24
+ });
25
+
26
+ it("keeps a type step and a text colour apart", () => {
27
+ // Both are `text-*`; only one is a font size.
28
+ expect(cn("text-step-11", "text-text-2")).toBe("text-step-11 text-text-2");
29
+ });
30
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Studio's tailwind-merge config: teaches it the two scales `theme.css`
3
+ * invents (`text-step-*`, `ctl`/`ctl-sm`/`ctl-lg`) so overrides replace, not stack.
4
+ */
5
+
6
+ import { createCn } from "cn/config";
7
+
8
+ /** `text-step-11`, `text-step-9`, … — the type scale from `theme.css`. */
9
+ const isTypeStep = (value: string) => /^step-\d+$/.test(value);
10
+
11
+ /** `ctl`, `ctl-sm`, `ctl-lg` — the control heights from `theme.css`. */
12
+ const isControlSize = (value: string) => /^ctl(-sm|-lg)?$/.test(value);
13
+
14
+ export const cn = createCn({
15
+ extend: {
16
+ classGroups: {
17
+ "font-size": [{ text: [isTypeStep] }],
18
+ h: [{ h: [isControlSize] }],
19
+ w: [{ w: [isControlSize] }],
20
+ size: [{ size: [isControlSize] }],
21
+ "min-h": [{ "min-h": [isControlSize] }],
22
+ "min-w": [{ "min-w": [isControlSize] }],
23
+ },
24
+ },
25
+ });
@@ -1,5 +1,32 @@
1
- // Minimal UI primitives for studio canvas components
2
- export { Button, IconButton } from "./Button";
1
+ // Studio's shared UI primitives. Every control in the app is meant to come
2
+ // from here, so a look or a keyboard behaviour is decided once.
3
+ export { cn } from "./cn";
4
+ export { Button, buttonBase, buttonSizes, buttonVariants } from "./Button";
5
+ export type { ButtonSize, ButtonVariant, PreviewState } from "./Button";
6
+ export { IconButton } from "./IconButton";
7
+ export { Tab, TabPanel, Tabs, TabsList } from "./Tabs";
3
8
  export { HyperframesLoader, StatusFrame } from "./HyperframesLoader";
4
9
  export type { HyperframesLoaderProps } from "./HyperframesLoader";
5
10
  export { Tooltip } from "./Tooltip";
11
+ export {
12
+ ContextMenu,
13
+ Menu,
14
+ MenuItem,
15
+ MenuRadioGroup,
16
+ MenuRadioItem,
17
+ MenuSeparator,
18
+ MenuShortcut,
19
+ popupSurface,
20
+ } from "./Menu";
21
+ export type { MenuItemTone, PopupPreviewState } from "./Menu";
22
+ export { Popover } from "./Popover";
23
+ export { Input, fieldBase, fieldText } from "./Input";
24
+ export type { InputProps } from "./Input";
25
+ export { NumberField } from "./NumberField";
26
+ export type { NumberFieldProps } from "./NumberField";
27
+ export { Select } from "./Select";
28
+ export type { SelectOption, SelectProps } from "./Select";
29
+ export { Slider } from "./Slider";
30
+ export type { SliderProps } from "./Slider";
31
+ export { Toggle } from "./Toggle";
32
+ export type { ToggleProps } from "./Toggle";
@@ -0,0 +1,24 @@
1
+ import { act, type ReactElement } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+
4
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
5
+
6
+ let mounted: { root: Root; host: HTMLElement } | null = null;
7
+
8
+ /** Renders into a fresh host attached to the body; `cleanupMounted` tears it down. */
9
+ export function mountHost(element: ReactElement): HTMLElement {
10
+ const host = document.createElement("div");
11
+ document.body.append(host);
12
+ const root = createRoot(host);
13
+ mounted = { root, host };
14
+ act(() => root.render(element));
15
+ return host;
16
+ }
17
+
18
+ export function cleanupMounted(): void {
19
+ if (!mounted) return;
20
+ const { root, host } = mounted;
21
+ mounted = null;
22
+ act(() => root.unmount());
23
+ host.remove();
24
+ }