@hyperframes/studio 0.8.50 → 0.8.52

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 (280) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
  2. package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
  3. package/dist/assets/index-CxAu_Okg.css +1 -0
  4. package/dist/assets/index-DocJlW2v.js +376 -0
  5. package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
  6. package/dist/index.d.ts +419 -20
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7496 -8288
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +13 -12
  12. package/src/App.tsx +131 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  15. package/src/captions/components/CaptionOverlay.tsx +8 -4
  16. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  17. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  18. package/src/captions/components/CaptionTimeline.tsx +1 -1
  19. package/src/captions/components/shared.tsx +2 -2
  20. package/src/components/AskAgentModal.tsx +5 -5
  21. package/src/components/EditorShell.tsx +8 -7
  22. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  23. package/src/components/LintModal.tsx +6 -16
  24. package/src/components/MediaPreview.tsx +2 -2
  25. package/src/components/RightPanelTabs.test.tsx +151 -0
  26. package/src/components/RightPanelTabs.tsx +59 -0
  27. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  28. package/src/components/StudioHeader.dom.test.tsx +181 -0
  29. package/src/components/StudioHeader.tsx +110 -210
  30. package/src/components/StudioLeftSidebar.tsx +7 -4
  31. package/src/components/StudioOverlays.tsx +1 -1
  32. package/src/components/StudioRightPanel.tsx +51 -39
  33. package/src/components/StudioToast.tsx +2 -2
  34. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  35. package/src/components/TimelineHistoryButtons.tsx +54 -0
  36. package/src/components/TimelineToolbar.test.tsx +8 -0
  37. package/src/components/TimelineToolbar.tsx +8 -12
  38. package/src/components/dock/Dock.test.tsx +260 -0
  39. package/src/components/dock/Dock.tsx +281 -0
  40. package/src/components/dock/dock.css +28 -0
  41. package/src/components/dock/dockAccessibility.test.ts +166 -0
  42. package/src/components/dock/dockAccessibility.ts +98 -0
  43. package/src/components/dock/dockLayout.test.ts +134 -0
  44. package/src/components/dock/dockLayout.ts +105 -0
  45. package/src/components/dock/dockLayoutSchema.ts +66 -0
  46. package/src/components/dock/dockLayoutStore.ts +91 -0
  47. package/src/components/dock/dockTestHarness.ts +11 -0
  48. package/src/components/dock/panelRegistry.ts +67 -0
  49. package/src/components/editor/AnimationCard.tsx +4 -4
  50. package/src/components/editor/AnimationCardParts.tsx +6 -6
  51. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  52. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  53. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  54. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  55. package/src/components/editor/DomEditOverlay.tsx +1 -1
  56. package/src/components/editor/EaseCurveSection.tsx +1 -1
  57. package/src/components/editor/EaseParamFields.tsx +5 -5
  58. package/src/components/editor/FileTree.tsx +4 -4
  59. package/src/components/editor/FileTreeIcons.tsx +1 -1
  60. package/src/components/editor/FileTreeNodes.tsx +13 -13
  61. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  62. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  63. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  64. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  65. package/src/components/editor/LayersPanel.test.ts +97 -1
  66. package/src/components/editor/LayersPanel.tsx +7 -5
  67. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  68. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  69. package/src/components/editor/PreviewGuides.tsx +1 -1
  70. package/src/components/editor/PromotableControl.tsx +2 -2
  71. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  72. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  73. package/src/components/editor/SnapToolbar.tsx +2 -2
  74. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  75. package/src/components/editor/TopologyLens.test.tsx +28 -0
  76. package/src/components/editor/TopologyLens.tsx +7 -5
  77. package/src/components/editor/Transform3DCube.tsx +2 -2
  78. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  79. package/src/components/editor/domEditing.test.ts +1 -1
  80. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  81. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  82. package/src/components/editor/propertyPanelColor.tsx +11 -11
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  87. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  88. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  89. package/src/components/editor/propertyPanelFill.tsx +4 -4
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  94. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  95. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  96. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  97. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  98. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  99. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.tsx +5 -5
  101. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  102. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  106. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  107. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  108. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  109. package/src/components/editor/propertyPanelSections.tsx +8 -8
  110. package/src/components/editor/useColorGradingController.test.ts +38 -0
  111. package/src/components/editor/useColorGradingController.ts +9 -1
  112. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  113. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  114. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  115. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  116. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  117. package/src/components/feedback/feedbackTrigger.ts +0 -1
  118. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  119. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  120. package/src/components/nle/NLEPreview.tsx +3 -3
  121. package/src/components/nle/PreviewOverlays.tsx +6 -6
  122. package/src/components/nle/PreviewPane.tsx +3 -3
  123. package/src/components/nle/TimelinePane.tsx +3 -3
  124. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  125. package/src/components/panels/SlideshowPanel.tsx +2 -2
  126. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  127. package/src/components/panels/VariablesBindElement.tsx +2 -2
  128. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  129. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  130. package/src/components/panels/VariablesPanel.tsx +3 -3
  131. package/src/components/panels/VariablesValueControls.tsx +2 -2
  132. package/src/components/pointerTargetSize.test.tsx +8 -0
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  134. package/src/components/renders/RenderQueue.test.tsx +7 -0
  135. package/src/components/renders/RenderQueue.tsx +7 -7
  136. package/src/components/renders/RenderQueueItem.tsx +6 -6
  137. package/src/components/sidebar/AssetCard.tsx +8 -8
  138. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  139. package/src/components/sidebar/AssetsTab.tsx +2 -2
  140. package/src/components/sidebar/AudioRow.tsx +6 -6
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  143. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  144. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  145. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/timelineToolbarStyles.ts +5 -0
  148. package/src/components/ui/Button.test.tsx +252 -0
  149. package/src/components/ui/Button.tsx +89 -126
  150. package/src/components/ui/IconButton.tsx +38 -0
  151. package/src/components/ui/Input.tsx +115 -0
  152. package/src/components/ui/Menu.test.tsx +374 -0
  153. package/src/components/ui/Menu.tsx +213 -0
  154. package/src/components/ui/NumberField.tsx +122 -0
  155. package/src/components/ui/Popover.tsx +74 -0
  156. package/src/components/ui/SearchInput.tsx +2 -2
  157. package/src/components/ui/Select.tsx +100 -0
  158. package/src/components/ui/Slider.tsx +153 -0
  159. package/src/components/ui/Tabs.test.tsx +98 -0
  160. package/src/components/ui/Tabs.tsx +71 -0
  161. package/src/components/ui/Toggle.tsx +63 -0
  162. package/src/components/ui/Tooltip.test.tsx +110 -0
  163. package/src/components/ui/Tooltip.tsx +38 -112
  164. package/src/components/ui/cn.test.ts +30 -0
  165. package/src/components/ui/cn.ts +25 -0
  166. package/src/components/ui/index.ts +29 -2
  167. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  168. package/src/components/ui/valueControls.test.tsx +524 -0
  169. package/src/hooks/useAppHotkeys.ts +21 -80
  170. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  171. package/src/hooks/useCaptionDetection.ts +3 -1
  172. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  173. package/src/hooks/useEditHistoryActions.ts +106 -0
  174. package/src/hooks/useLintModal.test.tsx +65 -0
  175. package/src/hooks/useLintModal.ts +3 -0
  176. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  177. package/src/hooks/useSdkSession.ts +43 -10
  178. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  179. package/src/hooks/useServerConnection.ts +43 -10
  180. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  181. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  182. package/src/index.ts +51 -0
  183. package/src/main.tsx +9 -3
  184. package/src/player/components/AudioWaveform.tsx +1 -1
  185. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  186. package/src/player/components/ClipContextMenu.tsx +3 -3
  187. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  188. package/src/player/components/CompositionThumbnail.tsx +3 -4
  189. package/src/player/components/EditModal.tsx +1 -1
  190. package/src/player/components/ImageThumbnail.tsx +1 -1
  191. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  192. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  193. package/src/player/components/PlayerControls.tsx +1 -1
  194. package/src/player/components/ShortcutsPanel.tsx +9 -9
  195. package/src/player/components/SpeedMenu.tsx +3 -5
  196. package/src/player/components/Timeline.tsx +2 -1
  197. package/src/player/components/TimelineCanvas.tsx +21 -14
  198. package/src/player/components/TimelineClip.tsx +0 -1
  199. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  200. package/src/player/components/TimelineEmptyState.tsx +5 -5
  201. package/src/player/components/TimelineFxButton.tsx +6 -6
  202. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  203. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  204. package/src/player/components/TimelineGroupRow.tsx +1 -1
  205. package/src/player/components/TimelineLanes.test.tsx +32 -0
  206. package/src/player/components/TimelineLanes.tsx +2 -5
  207. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  208. package/src/player/components/TimelineRuler.tsx +1 -1
  209. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  210. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  211. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  212. package/src/player/components/TimelineTypes.ts +2 -1
  213. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  214. package/src/player/components/VideoThumbnail.tsx +4 -2
  215. package/src/player/components/VolumeControl.tsx +2 -2
  216. package/src/player/components/timelineCallbacks.ts +8 -8
  217. package/src/player/components/timelineClipDragPreview.ts +51 -27
  218. package/src/player/components/timelineClipDragTypes.ts +3 -0
  219. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  220. package/src/player/components/timelineDragDrop.ts +57 -15
  221. package/src/player/components/timelineLaneProps.ts +3 -0
  222. package/src/player/components/timelineSnapping.test.ts +27 -0
  223. package/src/player/components/timelineSnapping.ts +14 -1
  224. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  225. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  226. package/src/player/components/useTimelineClipDrag.ts +30 -3
  227. package/src/player/lib/describeClips.ts +3 -54
  228. package/src/player/lib/playbackShortcuts.ts +2 -0
  229. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  230. package/src/player/store/previewIframeStore.test.tsx +29 -0
  231. package/src/player/store/previewIframeStore.ts +25 -0
  232. package/src/renderQueueExports.test.tsx +58 -0
  233. package/src/styles/classCandidates.test.ts +105 -0
  234. package/src/styles/classCandidates.ts +281 -0
  235. package/src/styles/hex-baseline.json +84 -0
  236. package/src/styles/hexRatchet.test.ts +201 -0
  237. package/src/styles/iconTokens.ts +20 -0
  238. package/src/styles/palette-reset.css +6 -0
  239. package/src/styles/studio.css +24 -21
  240. package/src/styles/styleSources.test.ts +20 -0
  241. package/src/styles/styleSources.ts +47 -0
  242. package/src/styles/tailwind-preset.shared.js +3 -0
  243. package/src/styles/theme.css +243 -0
  244. package/src/styles/theme.test.ts +183 -0
  245. package/src/styles/tokenGate.test.ts +109 -0
  246. package/src/utils/sourcePatcher.ts +1 -1
  247. package/src/utils/studioHelpers.test.ts +18 -0
  248. package/src/utils/studioHelpers.ts +11 -1
  249. package/src/utils/studioUiPreferences.ts +7 -0
  250. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  251. package/src/utils/timelineDropTrackInsert.ts +83 -0
  252. package/src/utils/typingTarget.ts +4 -0
  253. package/dist/assets/index-n2ml4tUn.js +0 -442
  254. package/dist/assets/index-pO6p0H27.css +0 -1
  255. package/src/components/PanelTabButton.tsx +0 -31
  256. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  257. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  258. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  259. package/src/components/storyboard/FramePoster.tsx +0 -86
  260. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  261. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  262. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  263. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  264. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  265. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  266. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  267. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  268. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  269. package/src/components/storyboard/StoryboardView.tsx +0 -169
  270. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  271. package/src/components/storyboard/frameComments.test.ts +0 -95
  272. package/src/components/storyboard/frameComments.ts +0 -115
  273. package/src/components/storyboard/frameStatus.ts +0 -36
  274. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  275. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  276. package/src/components/storyboard/useFrameComments.ts +0 -98
  277. package/src/contexts/ViewModeContext.tsx +0 -142
  278. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  279. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  280. package/src/hooks/useStoryboard.ts +0 -99
@@ -0,0 +1,122 @@
1
+ /**
2
+ * NumberField: the inspector's metric control. Commits only on blur, Enter, or a settled step (KTD11),
3
+ * never per keystroke. The unit is a sibling of the input; unparseable text shows a red boundary and commits nothing.
4
+ */
5
+
6
+ import { NumberField as BaseNumberField } from "@base-ui/react/number-field";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { cn } from "./cn";
9
+ import { fieldBase, fieldText } from "./Input";
10
+ import type { PreviewState } from "./Button";
11
+
12
+ export interface NumberFieldProps {
13
+ /** Accessible name. The inspector's row label is the usual one. */
14
+ label: string;
15
+ /** The committed value. */
16
+ value: number;
17
+ /** Called when a commit boundary produces a value different from `value`. */
18
+ onCommit: (next: number) => void;
19
+ /** Called once per committed change, for design-input telemetry. */
20
+ onTrack?: () => void;
21
+ /** Rendered beside the number: "px", "%", "deg". Never part of the text. */
22
+ unit?: string;
23
+ min?: number;
24
+ max?: number;
25
+ step?: number;
26
+ disabled?: boolean;
27
+ className?: string;
28
+ "data-preview-state"?: PreviewState;
29
+ }
30
+
31
+ export function NumberField({
32
+ label,
33
+ value,
34
+ onCommit,
35
+ onTrack,
36
+ unit,
37
+ min,
38
+ max,
39
+ step,
40
+ disabled,
41
+ className,
42
+ "data-preview-state": previewState,
43
+ }: NumberFieldProps) {
44
+ const [draft, setDraft] = useState<number | null>(value);
45
+ const [invalid, setInvalid] = useState(false);
46
+ const inputRef = useRef<HTMLInputElement>(null);
47
+ // What was last handed to `onCommit`, which is not the same as `value`: a
48
+ // blur can fire both Base UI's commit and ours in one turn, before the parent
49
+ // has re-rendered with the new prop, and the second must be a no-op.
50
+ const committedRef = useRef(value);
51
+ const valueRef = useRef(value);
52
+ valueRef.current = value;
53
+
54
+ useEffect(() => {
55
+ committedRef.current = value;
56
+ setDraft(value);
57
+ setInvalid(false);
58
+ }, [value]);
59
+
60
+ const commit = (next: number | null) => {
61
+ if (next === null || !Number.isFinite(next)) {
62
+ setInvalid(true);
63
+ return;
64
+ }
65
+ setInvalid(false);
66
+ if (next === committedRef.current) return;
67
+ committedRef.current = next;
68
+ onCommit(next);
69
+ onTrack?.();
70
+ };
71
+
72
+ /** Enter commits what is on screen, which Base UI has not parsed for us yet. */
73
+ const commitFromText = () => {
74
+ const text = inputRef.current?.value.trim() ?? "";
75
+ commit(text === "" ? null : Number(text));
76
+ };
77
+
78
+ return (
79
+ <BaseNumberField.Root
80
+ value={draft}
81
+ min={min}
82
+ max={max}
83
+ step={step}
84
+ disabled={disabled}
85
+ onValueChange={(next) => {
86
+ setDraft(next);
87
+ if (next !== null) setInvalid(false);
88
+ }}
89
+ onValueCommitted={(next) => commit(next)}
90
+ >
91
+ <BaseNumberField.Group
92
+ className={cn(fieldBase, className)}
93
+ aria-invalid={invalid || undefined}
94
+ data-preview-state={previewState}
95
+ >
96
+ <BaseNumberField.Input
97
+ ref={inputRef}
98
+ aria-label={label}
99
+ aria-invalid={invalid || undefined}
100
+ className={cn(fieldText, "tabular-nums")}
101
+ onKeyDown={(event) => {
102
+ if (event.key !== "Enter") return;
103
+ event.preventDefault();
104
+ commitFromText();
105
+ }}
106
+ onBlur={() => {
107
+ // Base UI's own blur handler returns without committing when the
108
+ // text does not parse. That is the only case left for us: a valid
109
+ // blur has already arrived through `onValueCommitted`.
110
+ const text = inputRef.current?.value.trim() ?? "";
111
+ if (text === "" || !Number.isFinite(Number(text))) setInvalid(true);
112
+ }}
113
+ />
114
+ {unit && (
115
+ <span className="shrink-0 select-none text-step-10 text-text-4" aria-hidden="true">
116
+ {unit}
117
+ </span>
118
+ )}
119
+ </BaseNumberField.Group>
120
+ </BaseNumberField.Root>
121
+ );
122
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Popover: `Menu`'s floating chrome (`popupSurface`) without item semantics, so arrow keys
3
+ * stay with whatever is focused inside (rename inputs, forms). Only the shadow differs.
4
+ */
5
+
6
+ import { Popover as BasePopover } from "@base-ui/react/popover";
7
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
8
+ import { cn } from "./cn";
9
+ import { popupSurface, type PopupPreviewState } from "./Menu";
10
+
11
+ type PortalContainer = ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
12
+
13
+ /** Matches Menu's gap from its trigger, and its viewport margin. */
14
+ const SIDE_OFFSET = 6;
15
+ const VIEWPORT_MARGIN = 8;
16
+
17
+ interface PopoverProps extends Omit<ComponentPropsWithoutRef<typeof BasePopover.Root>, "children"> {
18
+ /** A single element. It becomes the trigger; no wrapper is added around it. */
19
+ trigger: ReactElement;
20
+ /** Arbitrary content. The popover owns none of its keys. */
21
+ children: ReactNode;
22
+ side?: "top" | "bottom" | "left" | "right";
23
+ align?: "start" | "center" | "end";
24
+ sideOffset?: number;
25
+ /** Portal target. Pass the shadow root when the trigger lives in one. */
26
+ container?: PortalContainer;
27
+ /** Names the popup for assistive tech. */
28
+ "aria-label"?: string;
29
+ /**
30
+ * What to focus on open. Defaults to Base UI's behaviour (the first tabbable
31
+ * element), which is what a rename field wants.
32
+ */
33
+ initialFocus?: ComponentPropsWithoutRef<typeof BasePopover.Popup>["initialFocus"];
34
+ className?: string;
35
+ "data-preview-state"?: PopupPreviewState;
36
+ }
37
+
38
+ export function Popover({
39
+ trigger,
40
+ children,
41
+ side = "bottom",
42
+ align = "center",
43
+ sideOffset = SIDE_OFFSET,
44
+ container,
45
+ className,
46
+ initialFocus,
47
+ "aria-label": ariaLabel,
48
+ "data-preview-state": previewState,
49
+ ...root
50
+ }: PopoverProps) {
51
+ return (
52
+ <BasePopover.Root {...root}>
53
+ <BasePopover.Trigger render={trigger} />
54
+ <BasePopover.Portal container={container}>
55
+ <BasePopover.Positioner
56
+ side={side}
57
+ align={align}
58
+ sideOffset={sideOffset}
59
+ collisionPadding={VIEWPORT_MARGIN}
60
+ className="z-200"
61
+ >
62
+ <BasePopover.Popup
63
+ aria-label={ariaLabel}
64
+ initialFocus={initialFocus}
65
+ data-preview-state={previewState}
66
+ className={cn(popupSurface, "p-3 text-step-11 text-text-1 shadow-popover", className)}
67
+ >
68
+ {children}
69
+ </BasePopover.Popup>
70
+ </BasePopover.Positioner>
71
+ </BasePopover.Portal>
72
+ </BasePopover.Root>
73
+ );
74
+ }
@@ -21,7 +21,7 @@ export function SearchInput({ className = "", ...props }: SearchInputProps) {
21
21
  height="12"
22
22
  viewBox="0 0 256 256"
23
23
  fill="none"
24
- className="flex-shrink-0"
24
+ className="shrink-0"
25
25
  aria-hidden="true"
26
26
  >
27
27
  <circle
@@ -45,7 +45,7 @@ export function SearchInput({ className = "", ...props }: SearchInputProps) {
45
45
  </svg>
46
46
  <input
47
47
  type="text"
48
- className="min-w-0 w-full bg-transparent text-[11px] text-panel-text-1 outline-none placeholder:text-panel-text-5"
48
+ className="min-w-0 w-full bg-transparent text-[11px] text-panel-text-1 outline-hidden placeholder:text-panel-text-5"
49
49
  {...props}
50
50
  />
51
51
  </div>
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Select: Base UI select in Input's box. The trigger keeps `role="combobox"`, which both hotkey selector
3
+ * lists match. Options are data, not children, so typeahead and the value display can read labels.
4
+ */
5
+
6
+ import { Select as BaseSelect } from "@base-ui/react/select";
7
+ import { cn } from "./cn";
8
+ import { fieldBase } from "./Input";
9
+ import type { PreviewState } from "./Button";
10
+
11
+ export interface SelectOption {
12
+ label: string;
13
+ value: string;
14
+ }
15
+
16
+ export interface SelectProps {
17
+ /** Accessible name for the trigger. */
18
+ label: string;
19
+ value: string;
20
+ options: SelectOption[];
21
+ /** Called when a different option is chosen. */
22
+ onCommit: (next: string) => void;
23
+ /** Called once per committed change, for design-input telemetry. */
24
+ onTrack?: () => void;
25
+ disabled?: boolean;
26
+ className?: string;
27
+ "data-preview-state"?: PreviewState;
28
+ }
29
+
30
+ export function Select({
31
+ label,
32
+ value,
33
+ options,
34
+ onCommit,
35
+ onTrack,
36
+ disabled,
37
+ className,
38
+ "data-preview-state": previewState,
39
+ }: SelectProps) {
40
+ return (
41
+ <BaseSelect.Root
42
+ value={value}
43
+ disabled={disabled}
44
+ items={options}
45
+ onValueChange={(next) => {
46
+ const chosen = String(next);
47
+ if (chosen === value) return;
48
+ onCommit(chosen);
49
+ onTrack?.();
50
+ }}
51
+ >
52
+ <BaseSelect.Trigger
53
+ aria-label={label}
54
+ className={cn(
55
+ fieldBase,
56
+ "w-full cursor-pointer justify-between text-left",
57
+ "data-[popup-open]:border-border-strong",
58
+ className,
59
+ )}
60
+ data-preview-state={previewState}
61
+ >
62
+ <BaseSelect.Value className="truncate" />
63
+ <BaseSelect.Icon className="shrink-0 text-text-4" aria-hidden="true">
64
+ <svg width="8" height="5" viewBox="0 0 8 5" fill="none">
65
+ <path
66
+ d="M1 1L4 4L7 1"
67
+ stroke="currentColor"
68
+ strokeWidth="1.5"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ />
72
+ </svg>
73
+ </BaseSelect.Icon>
74
+ </BaseSelect.Trigger>
75
+
76
+ <BaseSelect.Portal>
77
+ <BaseSelect.Positioner sideOffset={4} alignItemWithTrigger={false}>
78
+ <BaseSelect.Popup className="min-w-[var(--anchor-width)] rounded-md border border-border bg-surface py-1 shadow-menu outline-hidden">
79
+ <BaseSelect.List>
80
+ {options.map((option) => (
81
+ <BaseSelect.Item
82
+ key={option.value}
83
+ value={option.value}
84
+ className={cn(
85
+ "flex h-ctl-sm cursor-pointer select-none items-center gap-2 px-2.5",
86
+ "text-step-11 text-text-2 outline-hidden",
87
+ "data-[highlighted]:bg-hover data-[highlighted]:text-text-0",
88
+ "data-[selected]:text-text-0",
89
+ )}
90
+ >
91
+ <BaseSelect.ItemText className="truncate">{option.label}</BaseSelect.ItemText>
92
+ </BaseSelect.Item>
93
+ ))}
94
+ </BaseSelect.List>
95
+ </BaseSelect.Popup>
96
+ </BaseSelect.Positioner>
97
+ </BaseSelect.Portal>
98
+ </BaseSelect.Root>
99
+ );
100
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Slider: `onPreview` fires while the thumb moves and writes nothing durable; `onCommit` (with `onTrack`, once)
3
+ * fires at a boundary. Right-click or Escape aborts a drag via `eventDetails.cancel()` and restores the start value.
4
+ */
5
+
6
+ import { Slider as BaseSlider } from "@base-ui/react/slider";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { cn } from "./cn";
9
+ import type { PreviewState } from "./Button";
10
+
11
+ export interface SliderProps {
12
+ /** Accessible name for the thumb. The inspector's row label is the usual one. */
13
+ label: string;
14
+ /** The committed value. */
15
+ value: number;
16
+ min: number;
17
+ max: number;
18
+ step?: number;
19
+ /** Every intermediate value while dragging or stepping. Not a commit. */
20
+ onPreview?: (next: number) => void;
21
+ /** Called at a commit boundary when the value really changed. */
22
+ onCommit: (next: number) => void;
23
+ /** Called once per commit boundary, for design-input telemetry. */
24
+ onTrack?: () => void;
25
+ disabled?: boolean;
26
+ className?: string;
27
+ "data-preview-state"?: PreviewState;
28
+ }
29
+
30
+ const first = (next: number | number[]): number => (Array.isArray(next) ? next[0] : next);
31
+
32
+ export function Slider({
33
+ label,
34
+ value,
35
+ min,
36
+ max,
37
+ step,
38
+ onPreview,
39
+ onCommit,
40
+ onTrack,
41
+ disabled,
42
+ className,
43
+ "data-preview-state": previewState,
44
+ }: SliderProps) {
45
+ const [draft, setDraft] = useState(value);
46
+ const draggingRef = useRef(false);
47
+ const abortedRef = useRef(false);
48
+ // The committed value the current gesture started from, so an abort has
49
+ // something to put back.
50
+ const dragStartRef = useRef(value);
51
+ // What was last handed to `onCommit`. A gesture's own commit lands before the
52
+ // parent re-renders, so deduping against the `value` prop would double-fire.
53
+ const committedRef = useRef(value);
54
+ const valueRef = useRef(value);
55
+ valueRef.current = value;
56
+
57
+ // An external write wins, except mid-drag: the echo is a frame behind, and
58
+ // applying it would snap the thumb backwards under the pointer.
59
+ useEffect(() => {
60
+ if (draggingRef.current) return;
61
+ committedRef.current = value;
62
+ setDraft(value);
63
+ }, [value]);
64
+
65
+ const commit = (next: number) => {
66
+ if (next === committedRef.current) return;
67
+ committedRef.current = next;
68
+ onCommit(next);
69
+ };
70
+
71
+ const abort = () => {
72
+ if (!draggingRef.current) return;
73
+ abortedRef.current = true;
74
+ const start = dragStartRef.current;
75
+ setDraft(start);
76
+ onPreview?.(start);
77
+ commit(start);
78
+ };
79
+
80
+ return (
81
+ <BaseSlider.Root
82
+ value={draft}
83
+ min={min}
84
+ max={max}
85
+ step={step}
86
+ disabled={disabled}
87
+ onValueChange={(next, details) => {
88
+ if (abortedRef.current) {
89
+ // The pointer is still down after a right-click abort. Base UI would
90
+ // otherwise keep applying moves to a gesture the user cancelled.
91
+ details.cancel();
92
+ return;
93
+ }
94
+ const n = first(next);
95
+ setDraft(n);
96
+ onPreview?.(n);
97
+ }}
98
+ onValueCommitted={(next) => {
99
+ draggingRef.current = false;
100
+ if (abortedRef.current) {
101
+ abortedRef.current = false;
102
+ return;
103
+ }
104
+ commit(first(next));
105
+ onTrack?.();
106
+ }}
107
+ className={cn("flex min-w-0 items-center", className)}
108
+ data-preview-state={previewState}
109
+ >
110
+ <BaseSlider.Control
111
+ // A stable hook for the drag tests and for the screenshot script. Base
112
+ // UI's own marker on this part only exists before hydration.
113
+ data-slider-control="true"
114
+ // h-6 is the 24x24 WCAG 2.2 (2.5.8) target: the visible track stays
115
+ // 2px, only the pointer box grows.
116
+ className="flex h-6 w-full min-w-0 touch-none select-none items-center"
117
+ onPointerDown={() => {
118
+ draggingRef.current = true;
119
+ abortedRef.current = false;
120
+ dragStartRef.current = valueRef.current;
121
+ }}
122
+ onContextMenu={(event) => {
123
+ if (!draggingRef.current) return;
124
+ event.preventDefault();
125
+ abort();
126
+ }}
127
+ onKeyDown={(event) => {
128
+ if (event.key !== "Escape" || !draggingRef.current) return;
129
+ event.preventDefault();
130
+ abort();
131
+ }}
132
+ >
133
+ <BaseSlider.Track className="h-0.5 w-full rounded-full bg-hover">
134
+ <BaseSlider.Indicator className="rounded-full bg-text-5" />
135
+ <BaseSlider.Thumb
136
+ aria-label={label}
137
+ className={cn(
138
+ // The ring separates the thumb from the track behind it. A drop
139
+ // shadow would need a raw colour literal, which R5 does not allow
140
+ // outside the token file, and no shadow token fits a 10px dot.
141
+ "size-2.5 rounded-full bg-text-0 ring-2 ring-bg-1",
142
+ "cursor-grab active:cursor-grabbing data-[preview-state=active]:cursor-grabbing",
143
+ "hover:scale-110 data-[preview-state=hover]:scale-110",
144
+ "transition-transform ease-standard duration-hover",
145
+ "has-[:focus-visible]:outline-solid has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-accent",
146
+ "data-[preview-state=focus]:outline-solid data-[preview-state=focus]:outline-2 data-[preview-state=focus]:outline-offset-2 data-[preview-state=focus]:outline-accent",
147
+ )}
148
+ />
149
+ </BaseSlider.Track>
150
+ </BaseSlider.Control>
151
+ </BaseSlider.Root>
152
+ );
153
+ }
@@ -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
+ }