@boyernick/standard-ui-react 0.1.1-canary.2 → 0.1.1-canary.22
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/package.json +5 -3
- package/src/accordion.tsx +1 -1
- package/src/alert-dialog.tsx +1 -1
- package/src/autocomplete.tsx +12 -11
- package/src/badge.tsx +73 -9
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +38 -20
- package/src/button.tsx +2 -2
- package/src/calendar.tsx +22 -11
- package/src/card.tsx +57 -14
- package/src/carousel.tsx +163 -6
- package/src/checkbox.tsx +35 -13
- package/src/collapsible.tsx +4 -2
- package/src/combobox.tsx +13 -12
- package/src/command.tsx +38 -14
- package/src/context-menu.tsx +6 -3
- package/src/date-picker.tsx +174 -0
- package/src/dialog.tsx +33 -10
- package/src/drawer.tsx +1 -1
- package/src/empty.tsx +4 -4
- package/src/field.tsx +1 -1
- package/src/filter-group.tsx +139 -0
- package/src/icons.tsx +39 -0
- package/src/illustrations.tsx +219 -141
- package/src/image-modal.tsx +440 -49
- package/src/index.ts +188 -0
- package/src/input.tsx +1 -1
- package/src/kbd.tsx +49 -0
- package/src/lib/motion.ts +128 -18
- package/src/lib/popup.ts +45 -0
- package/src/menu.tsx +7 -4
- package/src/menubar.tsx +5 -0
- package/src/meter.tsx +1 -1
- package/src/minimap.tsx +267 -0
- package/src/navigation-menu.tsx +336 -67
- package/src/number-field.tsx +327 -61
- package/src/pagination.tsx +258 -42
- package/src/popover.tsx +3 -1
- package/src/preview-card.tsx +3 -1
- package/src/progress.tsx +6 -1
- package/src/questionnaire.tsx +279 -0
- package/src/scroll-area.tsx +1 -1
- package/src/select.tsx +8 -6
- package/src/sidebar.tsx +109 -28
- package/src/slider.tsx +64 -6
- package/src/sounds.tsx +6 -10
- package/src/spinner.tsx +105 -32
- package/src/switch.tsx +2 -2
- package/src/table.tsx +82 -15
- package/src/tabs.tsx +184 -38
- package/src/text-animate.tsx +71 -28
- package/src/textarea.tsx +1 -1
- package/src/timeline.tsx +321 -0
- package/src/toast.tsx +156 -23
- package/src/tooltip.tsx +43 -3
- package/src/video-player.tsx +296 -120
package/src/carousel.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useCallback,
|
|
9
9
|
useContext,
|
|
10
10
|
useEffect,
|
|
11
|
+
useRef,
|
|
11
12
|
useSyncExternalStore,
|
|
12
13
|
type ComponentProps,
|
|
13
14
|
type KeyboardEvent,
|
|
@@ -21,6 +22,9 @@ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
|
21
22
|
type CarouselOptions = UseCarouselParameters[0]
|
|
22
23
|
type CarouselPlugin = UseCarouselParameters[1]
|
|
23
24
|
|
|
25
|
+
const TRACKPAD_SWIPE_THRESHOLD = 24
|
|
26
|
+
const TRACKPAD_GESTURE_END_DELAY = 140
|
|
27
|
+
|
|
24
28
|
type CarouselContextValue = {
|
|
25
29
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
26
30
|
api: CarouselApi
|
|
@@ -46,6 +50,8 @@ export type CarouselProps = ComponentProps<"div"> & {
|
|
|
46
50
|
plugins?: CarouselPlugin
|
|
47
51
|
orientation?: "horizontal" | "vertical"
|
|
48
52
|
setApi?: (api: CarouselApi) => void
|
|
53
|
+
/** Adds directional edge fades wherever more slides are available. */
|
|
54
|
+
fade?: boolean
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export type CarouselContentProps = ComponentProps<"div">
|
|
@@ -53,16 +59,54 @@ export type CarouselItemProps = ComponentProps<"div">
|
|
|
53
59
|
export type CarouselPreviousProps = ComponentProps<typeof Button>
|
|
54
60
|
export type CarouselNextProps = ComponentProps<typeof Button>
|
|
55
61
|
|
|
62
|
+
const CarouselEdgeFades = () => {
|
|
63
|
+
const { orientation, canScrollPrev, canScrollNext } = useCarousel()
|
|
64
|
+
const horizontal = orientation === "horizontal"
|
|
65
|
+
const sharedClassName =
|
|
66
|
+
"pointer-events-none absolute z-10 opacity-0 transition-opacity duration-[var(--duration-lg)] ease-enter data-[visible=true]:opacity-100 motion-reduce:transition-none"
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<div
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
data-slot={horizontal ? "carousel-fade-left" : "carousel-fade-top"}
|
|
73
|
+
data-visible={canScrollPrev}
|
|
74
|
+
className={cn(
|
|
75
|
+
sharedClassName,
|
|
76
|
+
horizontal
|
|
77
|
+
? "-top-px -bottom-px -left-px w-20 bg-linear-to-r from-surface via-surface/60 via-40% to-transparent"
|
|
78
|
+
: "-top-px -right-px -left-px h-20 bg-linear-to-b from-surface via-surface/60 via-40% to-transparent",
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
<div
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
data-slot={horizontal ? "carousel-fade-right" : "carousel-fade-bottom"}
|
|
84
|
+
data-visible={canScrollNext}
|
|
85
|
+
className={cn(
|
|
86
|
+
sharedClassName,
|
|
87
|
+
horizontal
|
|
88
|
+
? "-top-px -right-px -bottom-px w-20 bg-linear-to-l from-surface via-surface/60 via-40% to-transparent"
|
|
89
|
+
: "-right-px -bottom-px -left-px h-20 bg-linear-to-t from-surface via-surface/60 via-40% to-transparent",
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
</>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
export const Carousel = ({
|
|
57
97
|
orientation = "horizontal",
|
|
98
|
+
fade = false,
|
|
58
99
|
opts,
|
|
59
100
|
setApi,
|
|
60
101
|
plugins,
|
|
61
102
|
className,
|
|
62
103
|
children,
|
|
63
104
|
onKeyDownCapture,
|
|
105
|
+
tabIndex,
|
|
106
|
+
ref: forwardedRef,
|
|
64
107
|
...props
|
|
65
108
|
}: CarouselProps) => {
|
|
109
|
+
const carouselRootRef = useRef<HTMLDivElement | null>(null)
|
|
66
110
|
const [carouselRef, api] = useEmblaCarousel(
|
|
67
111
|
{
|
|
68
112
|
...opts,
|
|
@@ -107,6 +151,35 @@ export const Carousel = ({
|
|
|
107
151
|
api?.scrollNext()
|
|
108
152
|
}, [api])
|
|
109
153
|
|
|
154
|
+
const canScrollPrevRef = useRef(canScrollPrev)
|
|
155
|
+
const canScrollNextRef = useRef(canScrollNext)
|
|
156
|
+
const scrollPrevRef = useRef(scrollPrev)
|
|
157
|
+
const scrollNextRef = useRef(scrollNext)
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
canScrollPrevRef.current = canScrollPrev
|
|
161
|
+
canScrollNextRef.current = canScrollNext
|
|
162
|
+
scrollPrevRef.current = scrollPrev
|
|
163
|
+
scrollNextRef.current = scrollNext
|
|
164
|
+
}, [canScrollNext, canScrollPrev, scrollNext, scrollPrev])
|
|
165
|
+
|
|
166
|
+
const trackpadDelta = useRef(0)
|
|
167
|
+
const trackpadGestureHandled = useRef(false)
|
|
168
|
+
const trackpadGestureTimeout = useRef<ReturnType<typeof setTimeout> | null>(
|
|
169
|
+
null,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const scheduleTrackpadGestureEnd = useCallback(() => {
|
|
173
|
+
if (trackpadGestureTimeout.current) {
|
|
174
|
+
clearTimeout(trackpadGestureTimeout.current)
|
|
175
|
+
}
|
|
176
|
+
trackpadGestureTimeout.current = setTimeout(() => {
|
|
177
|
+
trackpadDelta.current = 0
|
|
178
|
+
trackpadGestureHandled.current = false
|
|
179
|
+
trackpadGestureTimeout.current = null
|
|
180
|
+
}, TRACKPAD_GESTURE_END_DELAY)
|
|
181
|
+
}, [])
|
|
182
|
+
|
|
110
183
|
const handleKeyDown = useCallback(
|
|
111
184
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
112
185
|
onKeyDownCapture?.(event)
|
|
@@ -122,6 +195,72 @@ export const Carousel = ({
|
|
|
122
195
|
[onKeyDownCapture, scrollNext, scrollPrev],
|
|
123
196
|
)
|
|
124
197
|
|
|
198
|
+
const handleWheel = useCallback(
|
|
199
|
+
(event: globalThis.WheelEvent) => {
|
|
200
|
+
if (event.defaultPrevented || orientation !== "horizontal") return
|
|
201
|
+
|
|
202
|
+
const deltaMultiplier =
|
|
203
|
+
event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 100 : 1
|
|
204
|
+
const horizontalDelta = event.deltaX * deltaMultiplier
|
|
205
|
+
const verticalDelta = event.deltaY * deltaMultiplier
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
Math.abs(horizontalDelta) <= Math.abs(verticalDelta) ||
|
|
209
|
+
Math.abs(horizontalDelta) < 1
|
|
210
|
+
) {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (trackpadGestureHandled.current) {
|
|
215
|
+
event.preventDefault()
|
|
216
|
+
scheduleTrackpadGestureEnd()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const canScrollInDirection =
|
|
221
|
+
horizontalDelta > 0
|
|
222
|
+
? canScrollNextRef.current
|
|
223
|
+
: canScrollPrevRef.current
|
|
224
|
+
if (!canScrollInDirection) return
|
|
225
|
+
|
|
226
|
+
event.preventDefault()
|
|
227
|
+
trackpadDelta.current += horizontalDelta
|
|
228
|
+
scheduleTrackpadGestureEnd()
|
|
229
|
+
|
|
230
|
+
if (Math.abs(trackpadDelta.current) < TRACKPAD_SWIPE_THRESHOLD) return
|
|
231
|
+
|
|
232
|
+
trackpadGestureHandled.current = true
|
|
233
|
+
trackpadDelta.current = 0
|
|
234
|
+
if (horizontalDelta > 0) scrollNextRef.current()
|
|
235
|
+
else scrollPrevRef.current()
|
|
236
|
+
},
|
|
237
|
+
[orientation, scheduleTrackpadGestureEnd],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const root = carouselRootRef.current
|
|
242
|
+
if (!root) return
|
|
243
|
+
|
|
244
|
+
root.addEventListener("wheel", handleWheel, { passive: false })
|
|
245
|
+
return () => {
|
|
246
|
+
root.removeEventListener("wheel", handleWheel)
|
|
247
|
+
if (trackpadGestureTimeout.current) {
|
|
248
|
+
clearTimeout(trackpadGestureTimeout.current)
|
|
249
|
+
}
|
|
250
|
+
trackpadDelta.current = 0
|
|
251
|
+
trackpadGestureHandled.current = false
|
|
252
|
+
}
|
|
253
|
+
}, [handleWheel])
|
|
254
|
+
|
|
255
|
+
const setCarouselRootRef = useCallback(
|
|
256
|
+
(node: HTMLDivElement | null) => {
|
|
257
|
+
carouselRootRef.current = node
|
|
258
|
+
if (typeof forwardedRef === "function") forwardedRef(node)
|
|
259
|
+
else if (forwardedRef) forwardedRef.current = node
|
|
260
|
+
},
|
|
261
|
+
[forwardedRef],
|
|
262
|
+
)
|
|
263
|
+
|
|
125
264
|
useEffect(() => {
|
|
126
265
|
if (!api || !setApi) return
|
|
127
266
|
setApi(api)
|
|
@@ -140,14 +279,23 @@ export const Carousel = ({
|
|
|
140
279
|
}}
|
|
141
280
|
>
|
|
142
281
|
<div
|
|
282
|
+
ref={setCarouselRootRef}
|
|
143
283
|
role="region"
|
|
144
284
|
aria-roledescription="carousel"
|
|
145
285
|
data-slot="carousel"
|
|
146
|
-
|
|
286
|
+
data-fade={fade || undefined}
|
|
287
|
+
className={cn(
|
|
288
|
+
"relative",
|
|
289
|
+
fade &&
|
|
290
|
+
"rounded-xl outline-none focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
291
|
+
className,
|
|
292
|
+
)}
|
|
147
293
|
onKeyDownCapture={handleKeyDown}
|
|
294
|
+
tabIndex={tabIndex ?? (fade ? 0 : undefined)}
|
|
148
295
|
{...props}
|
|
149
296
|
>
|
|
150
297
|
{children}
|
|
298
|
+
{fade ? <CarouselEdgeFades /> : null}
|
|
151
299
|
</div>
|
|
152
300
|
</CarouselContext.Provider>
|
|
153
301
|
)
|
|
@@ -160,7 +308,16 @@ export const CarouselContent = ({
|
|
|
160
308
|
const { carouselRef, orientation } = useCarousel()
|
|
161
309
|
|
|
162
310
|
return (
|
|
163
|
-
<div
|
|
311
|
+
<div
|
|
312
|
+
ref={carouselRef}
|
|
313
|
+
className={cn(
|
|
314
|
+
"overflow-hidden",
|
|
315
|
+
orientation === "horizontal"
|
|
316
|
+
? "touch-pan-y overscroll-x-contain"
|
|
317
|
+
: "touch-pan-x overscroll-y-contain",
|
|
318
|
+
)}
|
|
319
|
+
data-slot="carousel-viewport"
|
|
320
|
+
>
|
|
164
321
|
<div
|
|
165
322
|
data-slot="carousel-content"
|
|
166
323
|
className={cn(
|
|
@@ -194,7 +351,7 @@ export const CarouselItem = ({ className, ...props }: CarouselItemProps) => {
|
|
|
194
351
|
|
|
195
352
|
export const CarouselPrevious = ({
|
|
196
353
|
className,
|
|
197
|
-
variant = "
|
|
354
|
+
variant = "ghost",
|
|
198
355
|
size = "md",
|
|
199
356
|
...props
|
|
200
357
|
}: CarouselPreviousProps) => {
|
|
@@ -211,7 +368,7 @@ export const CarouselPrevious = ({
|
|
|
211
368
|
aria-label="Previous slide"
|
|
212
369
|
data-slot="carousel-previous"
|
|
213
370
|
className={cn(
|
|
214
|
-
"absolute size-9",
|
|
371
|
+
"absolute size-9 border-0 focus-visible:border-0",
|
|
215
372
|
orientation === "horizontal"
|
|
216
373
|
? "top-1/2 -left-12 -translate-y-1/2 active:!-translate-y-1/2"
|
|
217
374
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
|
|
@@ -227,7 +384,7 @@ export const CarouselPrevious = ({
|
|
|
227
384
|
|
|
228
385
|
export const CarouselNext = ({
|
|
229
386
|
className,
|
|
230
|
-
variant = "
|
|
387
|
+
variant = "ghost",
|
|
231
388
|
size = "md",
|
|
232
389
|
...props
|
|
233
390
|
}: CarouselNextProps) => {
|
|
@@ -244,7 +401,7 @@ export const CarouselNext = ({
|
|
|
244
401
|
aria-label="Next slide"
|
|
245
402
|
data-slot="carousel-next"
|
|
246
403
|
className={cn(
|
|
247
|
-
"absolute size-9",
|
|
404
|
+
"absolute size-9 border-0 focus-visible:border-0",
|
|
248
405
|
orientation === "horizontal"
|
|
249
406
|
? "top-1/2 -right-12 -translate-y-1/2 active:!-translate-y-1/2"
|
|
250
407
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
|
package/src/checkbox.tsx
CHANGED
|
@@ -4,28 +4,50 @@ import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox"
|
|
|
4
4
|
import type { ComponentProps } from "react"
|
|
5
5
|
import { IconCheckmark1, IconMinus } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
|
+
import { motion } from "./lib/motion"
|
|
7
8
|
|
|
8
9
|
export type CheckboxProps = ComponentProps<typeof BaseCheckbox.Root>
|
|
9
10
|
|
|
10
11
|
export const Checkbox = ({ className, ...props }: CheckboxProps) => (
|
|
11
12
|
<BaseCheckbox.Root
|
|
12
13
|
className={cn(
|
|
13
|
-
"group flex size-4 shrink-0 items-center justify-center rounded-sm border border-border-secondary bg-surface
|
|
14
|
+
"group flex size-4 shrink-0 items-center justify-center rounded-sm border border-border-secondary bg-surface outline-none",
|
|
15
|
+
// motion.all, not motion.colors: the filled state brings a box shadow
|
|
16
|
+
// with it, which has to fade in alongside the background.
|
|
17
|
+
motion.all,
|
|
18
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
19
|
+
// Ticked and mixed are both filled controls, so they carry the primary
|
|
20
|
+
// button's treatment: brand fill, the border token that pairs with it,
|
|
21
|
+
// and the top-edge highlight that makes a solid control read as raised.
|
|
22
|
+
"data-checked:border-brand-primary-border data-checked:bg-brand-primary data-checked:inset-shadow-solid-top",
|
|
23
|
+
"data-indeterminate:border-brand-primary-border data-indeterminate:bg-brand-primary data-indeterminate:inset-shadow-solid-top",
|
|
24
|
+
"data-checked:hover:bg-brand-primary-hover data-indeterminate:hover:bg-brand-primary-hover",
|
|
25
|
+
"data-checked:active:bg-brand-primary-active data-indeterminate:active:bg-brand-primary-active",
|
|
26
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
14
27
|
className,
|
|
15
28
|
)}
|
|
16
29
|
{...props}
|
|
17
30
|
>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
{/* Only the active glyph is mounted. Central Icons give every instance of
|
|
32
|
+
an icon the same mask id, so a CSS-hidden twin would claim that id for
|
|
33
|
+
the whole document; a `display: none` subtree renders no mask, leaving
|
|
34
|
+
the visible instance to paint its backing rect unmasked as a solid
|
|
35
|
+
block. Mounting one icon keeps every id in the document resolvable. */}
|
|
36
|
+
{/* No `data-unchecked:hidden` here. Base UI keeps the indicator mounted
|
|
37
|
+
through its exit and flags it with both `data-unchecked` and
|
|
38
|
+
`data-ending-style`; hiding on the former would cut the latter's
|
|
39
|
+
transition short and the mark would vanish instantly. */}
|
|
40
|
+
<BaseCheckbox.Indicator
|
|
41
|
+
className={cn("flex text-brand-foreground", motion.checkIndicator)}
|
|
42
|
+
render={(props, state) => (
|
|
43
|
+
<span {...props}>
|
|
44
|
+
{state.indeterminate ? (
|
|
45
|
+
<IconMinus size={12} className="size-3" aria-hidden />
|
|
46
|
+
) : (
|
|
47
|
+
<IconCheckmark1 size={12} className="size-3" aria-hidden />
|
|
48
|
+
)}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
30
52
|
</BaseCheckbox.Root>
|
|
31
53
|
)
|
package/src/collapsible.tsx
CHANGED
|
@@ -25,7 +25,7 @@ export const CollapsibleTrigger = ({
|
|
|
25
25
|
className={cn(
|
|
26
26
|
"group flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top outline-none",
|
|
27
27
|
motion.colors,
|
|
28
|
-
"
|
|
28
|
+
"outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -56,6 +56,8 @@ export const CollapsiblePanel = ({
|
|
|
56
56
|
)}
|
|
57
57
|
{...props}
|
|
58
58
|
>
|
|
59
|
-
|
|
59
|
+
{/* px-3 matches the trigger's own padding, so the panel copy lines up
|
|
60
|
+
under the trigger's label instead of starting at the panel edge. */}
|
|
61
|
+
<div className="px-3 pt-2 leading-relaxed">{children}</div>
|
|
60
62
|
</BaseCollapsible.Panel>
|
|
61
63
|
)
|
package/src/combobox.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type { ComponentProps, ReactElement } from "react"
|
|
|
5
5
|
import { IconChevronDownSmall, IconCrossSmall } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
7
|
import { motion } from "./lib/motion"
|
|
8
|
+
import { popupInset, popupItem, popupLabel, popupMessage, popupSurface } from "./lib/popup"
|
|
8
9
|
|
|
9
10
|
export type ComboboxProps<
|
|
10
11
|
Value,
|
|
@@ -77,7 +78,6 @@ export const ComboboxInputGroup = ({
|
|
|
77
78
|
<BaseCombobox.InputGroup
|
|
78
79
|
className={cn(
|
|
79
80
|
"relative flex min-h-9 w-full items-center rounded-md border border-border-secondary bg-surface inset-shadow-outline-top outline-none transition-[color,box-shadow]",
|
|
80
|
-
"has-[[data-popup-open]]:bg-background-tertiary/60",
|
|
81
81
|
"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-offset-1 focus-within:ring-offset-background-primary focus-within:ring-ring/20",
|
|
82
82
|
"aria-invalid:border-destructive aria-invalid:focus-within:border-destructive aria-invalid:focus-within:ring-destructive/20",
|
|
83
83
|
"has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:focus-within:border-destructive has-[[aria-invalid=true]]:focus-within:ring-destructive/20",
|
|
@@ -139,7 +139,7 @@ export const ComboboxClear = ({
|
|
|
139
139
|
{...props}
|
|
140
140
|
>
|
|
141
141
|
{children ?? (
|
|
142
|
-
<IconCrossSmall size={
|
|
142
|
+
<IconCrossSmall size={16} className="size-4" aria-hidden />
|
|
143
143
|
)}
|
|
144
144
|
</BaseCombobox.Clear>
|
|
145
145
|
)
|
|
@@ -160,7 +160,7 @@ export const ComboboxBackdrop = ({
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
export const ComboboxPositioner = ({
|
|
163
|
-
sideOffset =
|
|
163
|
+
sideOffset = 6,
|
|
164
164
|
className,
|
|
165
165
|
...props
|
|
166
166
|
}: ComboboxPositionerProps) => (
|
|
@@ -174,7 +174,8 @@ export const ComboboxPositioner = ({
|
|
|
174
174
|
export const ComboboxPopup = ({ className, ...props }: ComboboxPopupProps) => (
|
|
175
175
|
<BaseCombobox.Popup
|
|
176
176
|
className={cn(
|
|
177
|
-
"z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden
|
|
177
|
+
"z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden",
|
|
178
|
+
popupSurface,
|
|
178
179
|
motion.popupAnchor,
|
|
179
180
|
className,
|
|
180
181
|
)}
|
|
@@ -199,7 +200,7 @@ export const ComboboxArrow = ({ className, ...props }: ComboboxArrowProps) => (
|
|
|
199
200
|
|
|
200
201
|
export const ComboboxList = ({ className, ...props }: ComboboxListProps) => (
|
|
201
202
|
<BaseCombobox.List
|
|
202
|
-
className={cn("max-h-[inherit] overflow-y-auto
|
|
203
|
+
className={cn("max-h-[inherit] overflow-y-auto outline-none", popupInset, className)}
|
|
203
204
|
{...props}
|
|
204
205
|
/>
|
|
205
206
|
)
|
|
@@ -207,7 +208,7 @@ export const ComboboxList = ({ className, ...props }: ComboboxListProps) => (
|
|
|
207
208
|
export const ComboboxItem = ({ className, ...props }: ComboboxItemProps) => (
|
|
208
209
|
<BaseCombobox.Item
|
|
209
210
|
className={cn(
|
|
210
|
-
|
|
211
|
+
popupItem,
|
|
211
212
|
motion.colors,
|
|
212
213
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary data-highlighted:text-fg-primary",
|
|
213
214
|
className,
|
|
@@ -238,14 +239,14 @@ export const ComboboxItemIndicator = ({
|
|
|
238
239
|
|
|
239
240
|
export const ComboboxEmpty = ({ className, ...props }: ComboboxEmptyProps) => (
|
|
240
241
|
<BaseCombobox.Empty
|
|
241
|
-
className={cn("
|
|
242
|
+
className={cn(cn(popupMessage, "empty:hidden"), className)}
|
|
242
243
|
{...props}
|
|
243
244
|
/>
|
|
244
245
|
)
|
|
245
246
|
|
|
246
247
|
export const ComboboxStatus = ({ className, ...props }: ComboboxStatusProps) => (
|
|
247
248
|
<BaseCombobox.Status
|
|
248
|
-
className={cn(
|
|
249
|
+
className={cn(popupMessage, className)}
|
|
249
250
|
{...props}
|
|
250
251
|
/>
|
|
251
252
|
)
|
|
@@ -259,7 +260,7 @@ export const ComboboxGroupLabel = ({
|
|
|
259
260
|
...props
|
|
260
261
|
}: ComboboxGroupLabelProps) => (
|
|
261
262
|
<BaseCombobox.GroupLabel
|
|
262
|
-
className={cn(
|
|
263
|
+
className={cn(popupLabel, className)}
|
|
263
264
|
{...props}
|
|
264
265
|
/>
|
|
265
266
|
)
|
|
@@ -291,7 +292,7 @@ export const ComboboxChips = ({ className, ...props }: ComboboxChipsProps) => (
|
|
|
291
292
|
export const ComboboxChip = ({ className, ...props }: ComboboxChipProps) => (
|
|
292
293
|
<BaseCombobox.Chip
|
|
293
294
|
className={cn(
|
|
294
|
-
"inline-flex h-6 items-center gap-1 rounded-xs bg-background-tertiary px-1.5 text-xs text-fg-primary",
|
|
295
|
+
"inline-flex h-6 items-center gap-1 rounded-xs bg-background-tertiary px-1.5 text-xs-strong text-fg-primary",
|
|
295
296
|
className,
|
|
296
297
|
)}
|
|
297
298
|
{...props}
|
|
@@ -305,13 +306,13 @@ export const ComboboxChipRemove = ({
|
|
|
305
306
|
}: ComboboxChipRemoveProps) => (
|
|
306
307
|
<BaseCombobox.ChipRemove
|
|
307
308
|
className={cn(
|
|
308
|
-
"inline-flex size-3.5 cursor-pointer items-center justify-center text-fg-
|
|
309
|
+
"inline-flex size-3.5 cursor-pointer items-center justify-center text-fg-primary outline-none focus-visible:outline-none",
|
|
309
310
|
className,
|
|
310
311
|
)}
|
|
311
312
|
{...props}
|
|
312
313
|
>
|
|
313
314
|
{children ?? (
|
|
314
|
-
<IconCrossSmall size={
|
|
315
|
+
<IconCrossSmall size={14} className="size-3.5" aria-hidden />
|
|
315
316
|
)}
|
|
316
317
|
</BaseCombobox.ChipRemove>
|
|
317
318
|
)
|
package/src/command.tsx
CHANGED
|
@@ -4,8 +4,10 @@ import { Dialog as BaseDialog } from "@base-ui/react/dialog"
|
|
|
4
4
|
import {
|
|
5
5
|
createContext,
|
|
6
6
|
useContext,
|
|
7
|
+
useEffect,
|
|
7
8
|
useId,
|
|
8
9
|
useMemo,
|
|
10
|
+
useRef,
|
|
9
11
|
type ButtonHTMLAttributes,
|
|
10
12
|
type ComponentProps,
|
|
11
13
|
type HTMLAttributes,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
type ReactNode,
|
|
14
16
|
} from "react"
|
|
15
17
|
import { IconCrossSmall, IconMagnifyingGlass } from "./icons"
|
|
18
|
+
import { filterGroupVariants, filterItemVariants } from "./filter-group"
|
|
16
19
|
import { cn } from "./lib/cn"
|
|
17
20
|
import { motion } from "./lib/motion"
|
|
18
21
|
|
|
@@ -49,7 +52,9 @@ export type CommandClearProps = ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
49
52
|
export type CommandCloseProps = ComponentProps<typeof BaseDialog.Close>
|
|
50
53
|
export type CommandDividerProps = HTMLAttributes<HTMLDivElement>
|
|
51
54
|
export type CommandContentProps = HTMLAttributes<HTMLDivElement>
|
|
55
|
+
/** @deprecated Use FilterGroup for new filter controls. */
|
|
52
56
|
export type CommandFiltersProps = HTMLAttributes<HTMLUListElement>
|
|
57
|
+
/** @deprecated Use FilterItem for new filter controls. */
|
|
53
58
|
export type CommandFilterProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
54
59
|
selected?: boolean
|
|
55
60
|
}
|
|
@@ -108,7 +113,7 @@ export const CommandBackdrop = ({
|
|
|
108
113
|
export const CommandPopup = ({ className, ...props }: CommandPopupProps) => (
|
|
109
114
|
<BaseDialog.Popup
|
|
110
115
|
className={cn(
|
|
111
|
-
"fixed top-1/2 left-1/2 z-50 flex h-[min(80vh,32rem)] w-full max-w-
|
|
116
|
+
"fixed top-1/2 left-1/2 z-50 flex h-[min(80vh,32rem)] w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl bg-surface-raised shadow-ambient outline-none max-sm:max-w-[calc(100vw-2rem)]",
|
|
112
117
|
motion.popupCenter,
|
|
113
118
|
className,
|
|
114
119
|
)}
|
|
@@ -147,7 +152,6 @@ export const CommandInput = ({
|
|
|
147
152
|
aria-activedescendant={activeOptionId}
|
|
148
153
|
className={cn(
|
|
149
154
|
"h-9 min-w-0 flex-1 cursor-text rounded-md bg-transparent px-2.5 text-base text-fg-primary outline-none placeholder:text-fg-quaternary",
|
|
150
|
-
"focus-visible:bg-background-tertiary/60 focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
151
155
|
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none",
|
|
152
156
|
className,
|
|
153
157
|
)}
|
|
@@ -174,9 +178,9 @@ export const CommandClear = ({
|
|
|
174
178
|
<button
|
|
175
179
|
type="button"
|
|
176
180
|
className={cn(
|
|
177
|
-
"cursor-pointer rounded-full px-3
|
|
181
|
+
"inline-flex h-8 cursor-pointer items-center rounded-full border border-transparent px-3 text-sm text-fg-quaternary outline-none",
|
|
178
182
|
motion.colors,
|
|
179
|
-
"hover:bg-background-tertiary hover:text-fg-primary focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-[active=true]:text-fg-primary",
|
|
183
|
+
"hover:bg-background-tertiary hover:text-fg-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-[active=true]:text-fg-primary",
|
|
180
184
|
className,
|
|
181
185
|
)}
|
|
182
186
|
{...props}
|
|
@@ -203,9 +207,9 @@ export const CommandClose = ({
|
|
|
203
207
|
}: CommandCloseProps) => (
|
|
204
208
|
<BaseDialog.Close
|
|
205
209
|
className={cn(
|
|
206
|
-
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full text-fg-primary outline-none",
|
|
210
|
+
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-transparent text-fg-primary outline-none",
|
|
207
211
|
motion.colors,
|
|
208
|
-
"hover:bg-background-tertiary focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
212
|
+
"hover:bg-background-tertiary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
209
213
|
className,
|
|
210
214
|
)}
|
|
211
215
|
aria-label={props["aria-label"] ?? "Close"}
|
|
@@ -223,7 +227,7 @@ export const CommandContent = ({
|
|
|
223
227
|
}: CommandContentProps) => (
|
|
224
228
|
<div
|
|
225
229
|
className={cn(
|
|
226
|
-
"flex min-h-0 flex-1 flex-col gap-3 overflow-
|
|
230
|
+
"flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-4 pt-1 pb-5",
|
|
227
231
|
className,
|
|
228
232
|
)}
|
|
229
233
|
{...props}
|
|
@@ -237,7 +241,8 @@ export const CommandFilters = ({
|
|
|
237
241
|
<ul
|
|
238
242
|
role="list"
|
|
239
243
|
className={cn(
|
|
240
|
-
|
|
244
|
+
filterGroupVariants({ variant: "pill" }),
|
|
245
|
+
"w-full shrink-0 justify-start py-1.5",
|
|
241
246
|
className,
|
|
242
247
|
)}
|
|
243
248
|
{...props}
|
|
@@ -255,11 +260,10 @@ export const CommandFilter = ({
|
|
|
255
260
|
type="button"
|
|
256
261
|
aria-pressed={selected}
|
|
257
262
|
data-selected={selected || undefined}
|
|
263
|
+
data-pressed={selected || undefined}
|
|
258
264
|
className={cn(
|
|
259
|
-
|
|
265
|
+
filterItemVariants({ variant: "pill", size: "md" }),
|
|
260
266
|
motion.colors,
|
|
261
|
-
"hover:text-fg-primary focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
262
|
-
"data-selected:bg-background-tertiary data-selected:text-fg-primary",
|
|
263
267
|
className,
|
|
264
268
|
)}
|
|
265
269
|
{...props}
|
|
@@ -277,7 +281,7 @@ export const CommandList = ({ className, ...props }: CommandListProps) => {
|
|
|
277
281
|
id={listboxId}
|
|
278
282
|
role="listbox"
|
|
279
283
|
className={cn(
|
|
280
|
-
"grid min-h-0 flex-1 content-start gap-0 overflow-
|
|
284
|
+
"grid min-h-0 flex-1 content-start gap-0 overflow-y-auto overscroll-contain p-1",
|
|
281
285
|
className,
|
|
282
286
|
)}
|
|
283
287
|
{...props}
|
|
@@ -295,12 +299,21 @@ export const CommandItem = ({
|
|
|
295
299
|
onClick,
|
|
296
300
|
...props
|
|
297
301
|
}: CommandItemProps) => {
|
|
302
|
+
const ref = useRef<HTMLAnchorElement & HTMLButtonElement>(null)
|
|
303
|
+
|
|
304
|
+
// Keyboard selection moves the highlight without moving focus, so nothing
|
|
305
|
+
// scrolls the list on its own. `nearest` only acts when the row is actually
|
|
306
|
+
// out of view, which leaves hover-driven selection alone.
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (selected) ref.current?.scrollIntoView({ block: "nearest" })
|
|
309
|
+
}, [selected])
|
|
310
|
+
|
|
298
311
|
const itemClassName = cn(
|
|
299
|
-
"flex h-15 min-h-15 w-full cursor-pointer items-center justify-between gap-4 rounded-lg px-4 text-left text-sm font-medium text-fg-primary outline-none select-none",
|
|
312
|
+
"flex h-15 min-h-15 w-full cursor-pointer items-center justify-between gap-4 rounded-lg border border-transparent px-4 text-left text-sm font-medium text-fg-primary outline-none select-none",
|
|
300
313
|
motion.colors,
|
|
301
314
|
"hover:bg-background-tertiary data-selected:bg-background-tertiary",
|
|
302
315
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
303
|
-
"focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
316
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
|
|
304
317
|
className,
|
|
305
318
|
)
|
|
306
319
|
|
|
@@ -308,6 +321,7 @@ export const CommandItem = ({
|
|
|
308
321
|
<li role="presentation" className="m-0">
|
|
309
322
|
{href ? (
|
|
310
323
|
<a
|
|
324
|
+
ref={ref}
|
|
311
325
|
id={id}
|
|
312
326
|
href={disabled ? undefined : href}
|
|
313
327
|
role="option"
|
|
@@ -323,6 +337,7 @@ export const CommandItem = ({
|
|
|
323
337
|
</a>
|
|
324
338
|
) : (
|
|
325
339
|
<button
|
|
340
|
+
ref={ref}
|
|
326
341
|
id={id}
|
|
327
342
|
type="button"
|
|
328
343
|
role="option"
|
|
@@ -369,6 +384,15 @@ export const CommandTitle = ({ className, children }: CommandTitleProps) => (
|
|
|
369
384
|
<span className={cn("min-w-0 truncate", className)}>{children}</span>
|
|
370
385
|
)
|
|
371
386
|
|
|
387
|
+
export type CommandMetaProps = HTMLAttributes<HTMLSpanElement>
|
|
388
|
+
|
|
389
|
+
export const CommandMeta = ({ className, ...props }: CommandMetaProps) => (
|
|
390
|
+
<span
|
|
391
|
+
className={cn("shrink-0 text-xs font-normal text-fg-tertiary", className)}
|
|
392
|
+
{...props}
|
|
393
|
+
/>
|
|
394
|
+
)
|
|
395
|
+
|
|
372
396
|
export const CommandDialogTitle = ({
|
|
373
397
|
className,
|
|
374
398
|
...props
|
package/src/context-menu.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
|
|
|
5
5
|
import { IconCheckmark1, IconChevronRightSmall } from "./icons"
|
|
6
6
|
import { cn } from "./lib/cn"
|
|
7
7
|
import { motion } from "./lib/motion"
|
|
8
|
+
import { popupInset, popupItem, popupLabel, popupSurface } from "./lib/popup"
|
|
8
9
|
|
|
9
10
|
export type ContextMenuProps = ComponentProps<typeof BaseContextMenu.Root>
|
|
10
11
|
export type ContextMenuTriggerProps = ComponentProps<
|
|
@@ -53,7 +54,7 @@ export type ContextMenuSubmenuTriggerProps = ComponentProps<
|
|
|
53
54
|
>
|
|
54
55
|
|
|
55
56
|
const itemClassName = cn(
|
|
56
|
-
|
|
57
|
+
popupItem,
|
|
57
58
|
motion.colors,
|
|
58
59
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary",
|
|
59
60
|
)
|
|
@@ -100,7 +101,9 @@ export const ContextMenuPopup = ({
|
|
|
100
101
|
}: ContextMenuPopupProps) => (
|
|
101
102
|
<BaseContextMenu.Popup
|
|
102
103
|
className={cn(
|
|
103
|
-
"z-50 min-w-40 overflow-hidden
|
|
104
|
+
"z-50 min-w-40 overflow-hidden",
|
|
105
|
+
popupSurface,
|
|
106
|
+
popupInset,
|
|
104
107
|
motion.popupAnchor,
|
|
105
108
|
className,
|
|
106
109
|
)}
|
|
@@ -165,7 +168,7 @@ export const ContextMenuGroupLabel = ({
|
|
|
165
168
|
...props
|
|
166
169
|
}: ContextMenuGroupLabelProps) => (
|
|
167
170
|
<BaseContextMenu.GroupLabel
|
|
168
|
-
className={cn(
|
|
171
|
+
className={cn(popupLabel, className)}
|
|
169
172
|
{...props}
|
|
170
173
|
/>
|
|
171
174
|
)
|