@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
@@ -1,159 +1,122 @@
1
1
  /**
2
- * Button & IconButton — The most important primitive.
2
+ * Button — a native `<button>` wearing Studio's tokens.
3
3
  *
4
- * Absorbs: active state (scale 0.98), hit target (min 32px),
5
- * shadow anatomy (primary), focus ring, disabled state,
6
- * loading state, reduced motion, proper timing tokens.
4
+ * Everything visual comes from `theme.css`: the three control heights, the
5
+ * radius scale, the semantic colours, the motion durations. No value is decided
6
+ * here, so a button cannot drift from the rest of the system.
7
7
  *
8
- * Rules applied:
9
- * - physics-active-state: scale(0.98) on :active
10
- * - ux-fitts-target-size: min 32px hit target
11
- * - visual-button-shadow-anatomy: 6-layer shadow on primary
12
- * - duration-press-hover: 120ms press, 150ms hover
8
+ * Two conventions: classes merge through `cn`, so a caller's `className`
9
+ * always wins within its group; and every interactive look is written twice,
10
+ * once as the real state and once as `data-[preview-state=…]` (CSS-only, for
11
+ * screenshots). `Button.test.tsx` asserts the two stay in sync.
13
12
  */
14
13
 
15
14
  import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
15
+ import { cn } from "./cn";
16
16
 
17
- // -- Button --
17
+ export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
18
+ export type ButtonSize = "sm" | "md" | "lg";
18
19
 
19
- type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
20
- type ButtonSize = "sm" | "md" | "lg";
20
+ /** Forces one interactive look for a gallery shot. CSS-only; see the header. */
21
+ export type PreviewState = "hover" | "active" | "focus";
21
22
 
22
- interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
23
+ export interface ButtonBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
23
24
  variant?: ButtonVariant;
24
25
  size?: ButtonSize;
26
+ "data-preview-state"?: PreviewState;
27
+ }
28
+
29
+ interface ButtonProps extends ButtonBaseProps {
25
30
  loading?: boolean;
26
31
  icon?: ReactNode;
27
32
  }
28
33
 
29
- const variantStyles: Record<ButtonVariant, string> = {
30
- primary: [
31
- "bg-white text-neutral-950 font-medium",
32
- "shadow-btn-primary",
33
- "enabled:hover:bg-neutral-200",
34
- "enabled:active:scale-[0.97]",
35
- ].join(" "),
36
- secondary: [
37
- "bg-transparent text-neutral-300 font-medium",
38
- "border border-border",
39
- "enabled:hover:bg-surface-hover enabled:hover:text-white enabled:hover:border-border-strong",
40
- "enabled:active:scale-[0.98]",
41
- ].join(" "),
42
- danger: [
43
- "bg-accent-red text-white font-medium",
44
- "enabled:hover:bg-red-600",
45
- "enabled:active:scale-[0.97]",
46
- ].join(" "),
47
- ghost: [
48
- "bg-transparent text-neutral-400",
49
- "enabled:hover:bg-surface-hover enabled:hover:text-white",
50
- "enabled:active:scale-[0.98]",
51
- ].join(" "),
34
+ /**
35
+ * Shared by Button and IconButton. `disabled:` keeps pointer events alive so a
36
+ * wrapping Tooltip can still explain why the control is disabled.
37
+ */
38
+ export const buttonBase = cn(
39
+ "inline-flex items-center justify-center select-none cursor-pointer whitespace-nowrap",
40
+ "transition-[background-color,border-color,color,filter,transform] ease-out-quint duration-press",
41
+ "disabled:opacity-40 disabled:cursor-not-allowed",
42
+ "outline-hidden",
43
+ "focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent",
44
+ "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",
45
+ );
46
+
47
+ /**
48
+ * One entry per variant. The `data-[preview-state=…]` half of each string
49
+ * repeats the `hover:` and `active:` half exactly; the test pairs them.
50
+ */
51
+ export const buttonVariants: Record<ButtonVariant, string> = {
52
+ primary: cn(
53
+ "bg-accent text-bg-0 font-semibold",
54
+ "enabled:hover:brightness-110 data-[preview-state=hover]:brightness-110",
55
+ "enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
56
+ ),
57
+ secondary: cn(
58
+ "bg-transparent text-text-1 font-medium border border-border-strong",
59
+ "enabled:hover:bg-hover enabled:hover:text-text-0 data-[preview-state=hover]:bg-hover data-[preview-state=hover]:text-text-0",
60
+ "enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
61
+ ),
62
+ danger: cn(
63
+ "bg-danger text-text-0 font-medium",
64
+ "enabled:hover:brightness-110 data-[preview-state=hover]:brightness-110",
65
+ "enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
66
+ ),
67
+ ghost: cn(
68
+ "bg-transparent text-text-2 font-medium",
69
+ "enabled:hover:bg-hover enabled:hover:text-text-0 data-[preview-state=hover]:bg-hover data-[preview-state=hover]:text-text-0",
70
+ "enabled:active:scale-[0.98] data-[preview-state=active]:scale-[0.98]",
71
+ ),
52
72
  };
53
73
 
54
- const sizeStyles: Record<ButtonSize, string> = {
55
- sm: "h-7 px-2.5 text-xs gap-1.5 rounded-button",
56
- md: "h-8 px-3 text-sm gap-1.5 rounded-button",
57
- lg: "h-9 px-4 text-base gap-2 rounded-button",
74
+ /**
75
+ * The three control heights, 24 / 28 / 32 px, from `--spacing-ctl-*`.
76
+ * Exported so the Capture download `<a href>` can wear the same recipe as a Button.
77
+ */
78
+ export const buttonSizes: Record<ButtonSize, string> = {
79
+ sm: "h-ctl-sm px-2 gap-1 rounded-sm text-step-11",
80
+ md: "h-ctl px-3 gap-1.5 rounded-md text-step-12",
81
+ lg: "h-ctl-lg px-4 gap-2 rounded-md text-step-13",
58
82
  };
59
83
 
60
- // Imported by the shell/renders PRs later in this stack.
61
- // fallow-ignore-next-line unused-export
84
+ function Spinner() {
85
+ return (
86
+ <svg className="animate-spin size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
87
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
88
+ <path
89
+ className="opacity-75"
90
+ fill="currentColor"
91
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
92
+ />
93
+ </svg>
94
+ );
95
+ }
96
+
62
97
  export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
63
98
  (
64
- {
65
- variant = "secondary",
66
- size = "md",
67
- loading,
68
- icon,
69
- children,
70
- className = "",
71
- disabled,
72
- ...props
73
- },
99
+ { variant = "secondary", size = "md", loading, icon, children, className, disabled, ...props },
74
100
  ref,
75
101
  ) => {
102
+ const isDisabled = disabled || loading;
103
+ let leading: ReactNode = null;
104
+ if (loading) leading = <Spinner />;
105
+ else if (icon) leading = <span className="shrink-0">{icon}</span>;
76
106
  return (
77
107
  <button
78
108
  ref={ref}
79
- disabled={disabled || loading}
80
- className={[
81
- "inline-flex items-center justify-center",
82
- "transition-all duration-press ease-standard",
83
- // No pointer-events-none: disabled buttons must still receive hover
84
- // so a wrapping Tooltip can explain WHY they're disabled (A5).
85
- "disabled:opacity-40 disabled:cursor-not-allowed",
86
- "select-none cursor-pointer",
87
- "outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-studio-accent",
88
- variantStyles[variant],
89
- sizeStyles[size],
90
- className,
91
- ].join(" ")}
109
+ disabled={isDisabled}
110
+ // `aria-disabled` as well as `disabled`: assistive tech announces the
111
+ // state even where the native attribute is filtered out of the tree.
112
+ aria-disabled={isDisabled || undefined}
113
+ className={cn(buttonBase, buttonVariants[variant], buttonSizes[size], className)}
92
114
  {...props}
93
115
  >
94
- {loading ? (
95
- <svg className="animate-spin h-3.5 w-3.5" viewBox="0 0 24 24" fill="none">
96
- <circle
97
- className="opacity-25"
98
- cx="12"
99
- cy="12"
100
- r="10"
101
- stroke="currentColor"
102
- strokeWidth="4"
103
- />
104
- <path
105
- className="opacity-75"
106
- fill="currentColor"
107
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
108
- />
109
- </svg>
110
- ) : icon ? (
111
- <span className="flex-shrink-0">{icon}</span>
112
- ) : null}
113
- {children && <span>{children}</span>}
116
+ {leading}
117
+ {children}
114
118
  </button>
115
119
  );
116
120
  },
117
121
  );
118
122
  Button.displayName = "Button";
119
-
120
- // -- IconButton --
121
- // For icon-only buttons. Enforces min 32px hit target.
122
-
123
- interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
124
- icon: ReactNode;
125
- size?: ButtonSize;
126
- variant?: ButtonVariant;
127
- "aria-label": string; // REQUIRED for accessibility
128
- }
129
-
130
- const iconSizeStyles: Record<ButtonSize, string> = {
131
- sm: "min-w-7 min-h-7 rounded-button", // 28px
132
- md: "min-w-8 min-h-8 rounded-button", // 32px — minimum recommended
133
- lg: "min-w-9 min-h-9 rounded-button", // 36px
134
- };
135
-
136
- // fallow-ignore-next-line unused-export
137
- export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
138
- ({ icon, size = "md", variant = "ghost", className = "", ...props }, ref) => {
139
- return (
140
- <button
141
- ref={ref}
142
- className={[
143
- "inline-flex items-center justify-center",
144
- "transition-all duration-press ease-standard",
145
- "disabled:opacity-40 disabled:cursor-not-allowed",
146
- "select-none cursor-pointer",
147
- "outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-studio-accent",
148
- variantStyles[variant],
149
- iconSizeStyles[size],
150
- className,
151
- ].join(" ")}
152
- {...props}
153
- >
154
- {icon}
155
- </button>
156
- );
157
- },
158
- );
159
- IconButton.displayName = "IconButton";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * IconButton — Button's square sibling; shares its base/variant classes so
3
+ * the two never drift. `aria-label` is required for screen readers.
4
+ */
5
+
6
+ import { forwardRef } from "react";
7
+ import type { ReactNode } from "react";
8
+ import { buttonBase, buttonVariants, type ButtonBaseProps, type ButtonSize } from "./Button";
9
+ import { cn } from "./cn";
10
+
11
+ interface IconButtonProps extends ButtonBaseProps {
12
+ icon: ReactNode;
13
+ "aria-label": string;
14
+ }
15
+
16
+ /** Square boxes on the same 24 / 28 / 32 px control heights as Button. */
17
+ const iconSizeStyles: Record<ButtonSize, string> = {
18
+ sm: "size-ctl-sm rounded-sm",
19
+ md: "size-ctl rounded-md",
20
+ lg: "size-ctl-lg rounded-md",
21
+ };
22
+
23
+ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
24
+ ({ icon, size = "md", variant = "ghost", className, disabled, ...props }, ref) => {
25
+ return (
26
+ <button
27
+ ref={ref}
28
+ disabled={disabled}
29
+ aria-disabled={disabled || undefined}
30
+ className={cn(buttonBase, buttonVariants[variant], iconSizeStyles[size], className)}
31
+ {...props}
32
+ >
33
+ {icon}
34
+ </button>
35
+ );
36
+ },
37
+ );
38
+ IconButton.displayName = "IconButton";
@@ -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
+ }