@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.
- package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-PTEnAHhg.js} +3 -3
- package/dist/assets/{index-DUAIWxkI.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Dt3qBI-9.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +825 -24
- package/dist/index.html +2 -2
- package/dist/index.js +8911 -9853
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +13 -12
- package/src/App.tsx +145 -163
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +59 -62
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +118 -221
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +8 -12
- package/src/components/dock/Dock.test.tsx +274 -0
- package/src/components/dock/Dock.tsx +291 -0
- package/src/components/dock/dock.css +28 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +105 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +108 -0
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +7 -5
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +7 -5
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +4 -3
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +3 -3
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +3 -3
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -4
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +7 -7
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.test.tsx +53 -0
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +252 -0
- package/src/components/ui/Button.tsx +89 -126
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +21 -84
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -3
- package/src/player/components/ClipContextMenu.tsx +3 -3
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +3 -2
- package/src/player/components/TimelineCanvas.tsx +21 -14
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +32 -0
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +2 -2
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +2 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +61 -1
- package/src/player/components/timelineDragDrop.ts +57 -15
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/lib/describeClips.ts +3 -54
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +270 -0
- package/src/styles/theme.test.ts +183 -0
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +7 -16
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/StudioLeftSidebar.tsx +0 -204
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -334
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useInspectorSplitResize.ts +0 -51
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
- package/src/utils/fitPanels.test.ts +0 -164
- package/src/utils/fitPanels.ts +0 -151
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu and ContextMenu on Base UI. Dismissal is plain Base UI (capture-phase listeners survive the overlay's
|
|
3
|
+
* `stopPropagation`); `trigger` is rendered, not wrapped; `container` exists for shadow roots.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
|
|
7
|
+
import { Menu as BaseMenu } from "@base-ui/react/menu";
|
|
8
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactElement, ReactNode } from "react";
|
|
9
|
+
import { cn } from "./cn";
|
|
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
|
+
/** Forces the settled open look for a gallery shot; CSS-only, open state stays controlled. */
|
|
20
|
+
export type PopupPreviewState = "open";
|
|
21
|
+
|
|
22
|
+
/** Where the portal puts the popup. `null` keeps it inline, next to its trigger. */
|
|
23
|
+
type PortalContainer = ComponentPropsWithoutRef<typeof BaseMenu.Portal>["container"];
|
|
24
|
+
|
|
25
|
+
/** Matches Tooltip's gap from its trigger, and its viewport margin. */
|
|
26
|
+
const SIDE_OFFSET = 6;
|
|
27
|
+
const VIEWPORT_MARGIN = 8;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Chrome shared by every floating panel (`Popover` too); callers add their own shadow token.
|
|
31
|
+
* `data-starting-style`/`data-ending-style` are Base UI's transition attributes.
|
|
32
|
+
*/
|
|
33
|
+
export const popupSurface = cn(
|
|
34
|
+
"rounded-lg border border-border-input bg-surface",
|
|
35
|
+
"origin-[var(--transform-origin)] outline-hidden",
|
|
36
|
+
"transition-[opacity,transform] ease-out-quint duration-open",
|
|
37
|
+
"data-[starting-style]:opacity-0 data-[starting-style]:scale-95",
|
|
38
|
+
"data-[ending-style]:opacity-0 data-[ending-style]:scale-95",
|
|
39
|
+
"data-[preview-state=open]:opacity-100 data-[preview-state=open]:scale-100",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/** The popup's own layer: menus sit above panel chrome and below a modal. */
|
|
43
|
+
const POPUP_LAYER = "z-200";
|
|
44
|
+
|
|
45
|
+
const menuPopup = cn(popupSurface, "min-w-36 p-1 shadow-menu");
|
|
46
|
+
|
|
47
|
+
/** One row. `data-highlighted` is set by keyboard and pointer alike, so the seen row is the Enter row. */
|
|
48
|
+
const itemBase = cn(
|
|
49
|
+
"flex cursor-default select-none items-center justify-between gap-6 rounded-sm px-2 py-1.5",
|
|
50
|
+
"text-step-11 whitespace-nowrap text-text-1",
|
|
51
|
+
"outline-hidden transition-colors ease-out-quint duration-hover",
|
|
52
|
+
"data-[highlighted]:bg-hover data-[highlighted]:text-text-0",
|
|
53
|
+
"data-[preview-state=hover]:bg-hover data-[preview-state=hover]:text-text-0",
|
|
54
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-40",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const itemDanger = cn("text-danger data-[highlighted]:bg-danger/15 data-[highlighted]:text-danger");
|
|
58
|
+
|
|
59
|
+
export type MenuItemTone = "default" | "danger";
|
|
60
|
+
|
|
61
|
+
interface PositionedProps {
|
|
62
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
63
|
+
align?: "start" | "center" | "end";
|
|
64
|
+
sideOffset?: number;
|
|
65
|
+
/** Portal target. Pass the shadow root when the trigger lives in one. */
|
|
66
|
+
container?: PortalContainer;
|
|
67
|
+
/** Names the popup for assistive tech. A menu with no name is unlabelled. */
|
|
68
|
+
"aria-label"?: string;
|
|
69
|
+
className?: string;
|
|
70
|
+
"data-preview-state"?: PopupPreviewState;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface MenuProps
|
|
74
|
+
extends PositionedProps, Omit<ComponentPropsWithoutRef<typeof BaseMenu.Root>, "children"> {
|
|
75
|
+
/** A single element. It becomes the trigger; no wrapper is added around it. */
|
|
76
|
+
trigger: ReactElement;
|
|
77
|
+
/** The items: `MenuItem`, `MenuRadioGroup`, `MenuSeparator`. */
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A menu opened by its trigger. Uncontrolled by default; pass `open` and
|
|
83
|
+
* `onOpenChange` for a menu whose state lives in a store.
|
|
84
|
+
*/
|
|
85
|
+
export function Menu({
|
|
86
|
+
trigger,
|
|
87
|
+
children,
|
|
88
|
+
side = "bottom",
|
|
89
|
+
align = "start",
|
|
90
|
+
sideOffset = SIDE_OFFSET,
|
|
91
|
+
container,
|
|
92
|
+
className,
|
|
93
|
+
"aria-label": ariaLabel,
|
|
94
|
+
"data-preview-state": previewState,
|
|
95
|
+
...root
|
|
96
|
+
}: MenuProps) {
|
|
97
|
+
return (
|
|
98
|
+
<BaseMenu.Root {...root}>
|
|
99
|
+
<BaseMenu.Trigger render={trigger} />
|
|
100
|
+
<BaseMenu.Portal container={container}>
|
|
101
|
+
<BaseMenu.Positioner
|
|
102
|
+
side={side}
|
|
103
|
+
align={align}
|
|
104
|
+
sideOffset={sideOffset}
|
|
105
|
+
collisionPadding={VIEWPORT_MARGIN}
|
|
106
|
+
className={POPUP_LAYER}
|
|
107
|
+
>
|
|
108
|
+
<BaseMenu.Popup
|
|
109
|
+
aria-label={ariaLabel}
|
|
110
|
+
data-preview-state={previewState}
|
|
111
|
+
className={cn(menuPopup, className)}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</BaseMenu.Popup>
|
|
115
|
+
</BaseMenu.Positioner>
|
|
116
|
+
</BaseMenu.Portal>
|
|
117
|
+
</BaseMenu.Root>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ContextMenuProps
|
|
122
|
+
extends PositionedProps, Omit<ComponentPropsWithoutRef<typeof BaseContextMenu.Root>, "children"> {
|
|
123
|
+
/**
|
|
124
|
+
* The right-clickable area, as the caller's own element. Its children are
|
|
125
|
+
* kept: the trigger renders that element rather than wrapping it.
|
|
126
|
+
*/
|
|
127
|
+
trigger: ReactElement;
|
|
128
|
+
children: ReactNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Opened by right click or long press at the pointer, so `side` and `align` are not offered. */
|
|
132
|
+
export function ContextMenu({
|
|
133
|
+
trigger,
|
|
134
|
+
children,
|
|
135
|
+
container,
|
|
136
|
+
className,
|
|
137
|
+
"aria-label": ariaLabel,
|
|
138
|
+
"data-preview-state": previewState,
|
|
139
|
+
...root
|
|
140
|
+
}: Omit<ContextMenuProps, "side" | "align" | "sideOffset">) {
|
|
141
|
+
return (
|
|
142
|
+
<BaseContextMenu.Root {...root}>
|
|
143
|
+
<BaseContextMenu.Trigger render={trigger} />
|
|
144
|
+
<BaseContextMenu.Portal container={container}>
|
|
145
|
+
<BaseContextMenu.Positioner collisionPadding={VIEWPORT_MARGIN} className={POPUP_LAYER}>
|
|
146
|
+
<BaseContextMenu.Popup
|
|
147
|
+
aria-label={ariaLabel}
|
|
148
|
+
data-preview-state={previewState}
|
|
149
|
+
className={cn(menuPopup, className)}
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</BaseContextMenu.Popup>
|
|
153
|
+
</BaseContextMenu.Positioner>
|
|
154
|
+
</BaseContextMenu.Portal>
|
|
155
|
+
</BaseContextMenu.Root>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Keyboard shortcut hint, mono and tabular; exported for hints that are not a plain string. */
|
|
160
|
+
export function MenuShortcut({ className, ...props }: StyledProps<"span">) {
|
|
161
|
+
return (
|
|
162
|
+
<span
|
|
163
|
+
className={cn("shrink-0 font-mono text-step-10 tabular-nums text-text-4", className)}
|
|
164
|
+
aria-hidden="true"
|
|
165
|
+
{...props}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface MenuItemProps extends StyledProps<typeof BaseMenu.Item> {
|
|
171
|
+
/** Rendered as a dim mono hint on the trailing edge. Decorative. */
|
|
172
|
+
shortcut?: string;
|
|
173
|
+
/** `danger` for a destructive action (Delete, Remove). */
|
|
174
|
+
tone?: MenuItemTone;
|
|
175
|
+
"data-preview-state"?: "hover";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** One action. `disabled` items are skipped by the arrow keys, not just dimmed. */
|
|
179
|
+
export function MenuItem({ shortcut, tone, className, children, ...props }: MenuItemProps) {
|
|
180
|
+
return (
|
|
181
|
+
<BaseMenu.Item className={cn(itemBase, tone === "danger" && itemDanger, className)} {...props}>
|
|
182
|
+
<span className="truncate">{children}</span>
|
|
183
|
+
{shortcut ? <MenuShortcut>{shortcut}</MenuShortcut> : null}
|
|
184
|
+
</BaseMenu.Item>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Single-choice group; selection is `aria-checked` on each item, which the dot indicator draws from. */
|
|
189
|
+
export function MenuRadioGroup(props: ComponentPropsWithoutRef<typeof BaseMenu.RadioGroup>) {
|
|
190
|
+
return <BaseMenu.RadioGroup {...props} />;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface MenuRadioItemProps extends StyledProps<typeof BaseMenu.RadioItem> {
|
|
194
|
+
"data-preview-state"?: "hover";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One choice in a `MenuRadioGroup`. The dot renders only while it is checked. */
|
|
198
|
+
export function MenuRadioItem({ className, children, ...props }: MenuRadioItemProps) {
|
|
199
|
+
return (
|
|
200
|
+
<BaseMenu.RadioItem className={cn(itemBase, className)} {...props}>
|
|
201
|
+
<span className="truncate">{children}</span>
|
|
202
|
+
{/* Fixed box so a checked and an unchecked row keep the same width. */}
|
|
203
|
+
<span className="flex size-3 shrink-0 items-center justify-center">
|
|
204
|
+
<BaseMenu.RadioItemIndicator className="size-1.5 rounded-full bg-accent" />
|
|
205
|
+
</span>
|
|
206
|
+
</BaseMenu.RadioItem>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A hairline between two groups of items. */
|
|
211
|
+
export function MenuSeparator({ className, ...props }: StyledProps<typeof BaseMenu.Separator>) {
|
|
212
|
+
return <BaseMenu.Separator className={cn("my-1 h-px bg-hairline", className)} {...props} />;
|
|
213
|
+
}
|
|
@@ -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="
|
|
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-
|
|
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
|
+
}
|