@passui/react 0.1.35 → 0.1.37
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/types/src/components/accordion/accordion.d.ts +5 -1
- package/dist/types/src/components/date-picker/date-picker.d.ts +4 -1
- package/dist/types/src/components/dialog/dialog.d.ts +4 -1
- package/dist/types/src/components/disclosure-group/disclosure-group.d.ts +4 -1
- package/dist/types/src/components/morph/index.d.ts +2 -0
- package/dist/types/src/components/morph/morph.d.ts +29 -0
- package/dist/types/src/components/morph/use-morph-disclosure.d.ts +13 -0
- package/dist/types/src/components/radio-group/radio-group.d.ts +4 -1
- package/dist/types/src/components/segmented-control/segmented-control.d.ts +4 -1
- package/dist/types/src/components/sheet/sheet.d.ts +3 -0
- package/dist/types/src/components/slider/slider.d.ts +5 -2
- package/dist/types/src/components/stepper/stepper.d.ts +4 -1
- package/dist/types/src/components/swatch-picker/swatch-picker.d.ts +4 -1
- package/dist/types/src/components/tab-view/tab-view.d.ts +4 -1
- package/dist/types/src/components/toggle/toggle.d.ts +4 -1
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +5 -1
- package/src/components/accordion/accordion.tsx +16 -3
- package/src/components/date-picker/date-picker.tsx +37 -6
- package/src/components/dialog/dialog.tsx +48 -5
- package/src/components/disclosure-group/disclosure-group.tsx +31 -9
- package/src/components/morph/index.ts +2 -0
- package/src/components/morph/morph.tsx +210 -0
- package/src/components/morph/use-morph-disclosure.ts +27 -0
- package/src/components/radio-group/radio-group.tsx +30 -10
- package/src/components/segmented-control/segmented-control.tsx +47 -6
- package/src/components/sheet/sheet.tsx +20 -2
- package/src/components/slider/slider.tsx +58 -9
- package/src/components/stepper/stepper.tsx +37 -11
- package/src/components/swatch-picker/swatch-picker.tsx +40 -5
- package/src/components/tab-view/tab-view.tsx +31 -4
- package/src/components/toggle/toggle.tsx +105 -20
- package/src/index.ts +2 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion } from "motion/react"
|
|
2
|
+
import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"
|
|
3
|
+
import { concealMenuSource } from "../context-menu/conceal-menu-source"
|
|
4
|
+
import { menuSurfaceExit, menuSurfaceReveal, type MenuRect } from "../context-menu/menu-reveal"
|
|
5
|
+
import { useSheetFocus } from "../sheet/use-sheet-focus"
|
|
6
|
+
import { SheetPortal } from "../sheet/sheet-portal"
|
|
7
|
+
import { cn } from "../../lib/class-names"
|
|
8
|
+
import { contentCrossfade, fluidCollapse, fluidOvershoot } from "../../motion/transitions"
|
|
9
|
+
|
|
10
|
+
export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
11
|
+
/** Controls whether the overlay is presented. */
|
|
12
|
+
open: boolean
|
|
13
|
+
/** Called on Escape or outside press. */
|
|
14
|
+
onClose: () => void
|
|
15
|
+
/** The element whose rect the overlay grows from and collapses back to. */
|
|
16
|
+
sourceRef: RefObject<HTMLElement | null>
|
|
17
|
+
/** Accessible name (used when there is no labelledBy). */
|
|
18
|
+
label: string
|
|
19
|
+
/** ID of a visible heading inside the overlay. */
|
|
20
|
+
labelledBy?: string
|
|
21
|
+
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
22
|
+
dismissOnOutsidePress?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
children?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A modal overlay that morphs its glass material from a source element's
|
|
29
|
+
* rect to its own resting rect, then collapses back — Apple's
|
|
30
|
+
* matched-geometry effect. The source is concealed while the overlay is
|
|
31
|
+
* mounted so the material reads as the source itself springing open.
|
|
32
|
+
*
|
|
33
|
+
* Reuses `SheetPortal` (theme inheritance), `useSheetFocus` (focus trap,
|
|
34
|
+
* Escape, modal environment) and the same `menuSurfaceReveal` FLIP
|
|
35
|
+
* primitive that powers anchored `Popover` and `ContextMenu`.
|
|
36
|
+
*/
|
|
37
|
+
export function Morph({ open, ...props }: MorphProps) {
|
|
38
|
+
return <AnimatePresence>{open && <OpenMorph {...props} />}</AnimatePresence>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function OpenMorph({
|
|
42
|
+
onClose,
|
|
43
|
+
sourceRef,
|
|
44
|
+
label,
|
|
45
|
+
labelledBy,
|
|
46
|
+
dismissOnOutsidePress = true,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: Omit<MorphProps, "open">) {
|
|
51
|
+
const panel = useRef<HTMLDivElement>(null)
|
|
52
|
+
const reducedMotion = useReducedMotion()
|
|
53
|
+
const [settled, setSettled] = useState(false)
|
|
54
|
+
const [target, setTarget] = useState<{ width: number; height: number; radius: number } | null>(
|
|
55
|
+
null
|
|
56
|
+
)
|
|
57
|
+
const [reveal, setReveal] = useState<ReturnType<typeof menuSurfaceReveal> | null>(null)
|
|
58
|
+
useSheetFocus(panel, onClose)
|
|
59
|
+
|
|
60
|
+
// Measure the panel's resting rect and the source's rect, then compute
|
|
61
|
+
// the reveal (initial material state) from menuSurfaceReveal. Re-measures
|
|
62
|
+
// on resize/scroll so the morph stays aligned if either moves.
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const surface = panel.current
|
|
65
|
+
const source = sourceRef.current
|
|
66
|
+
if (!surface) return
|
|
67
|
+
const measure = () => {
|
|
68
|
+
const panelRect = surface.getBoundingClientRect()
|
|
69
|
+
const radius = Number.parseFloat(getComputedStyle(surface).borderTopLeftRadius) || 0
|
|
70
|
+
setTarget((current) =>
|
|
71
|
+
current &&
|
|
72
|
+
current.width === panelRect.width &&
|
|
73
|
+
current.height === panelRect.height &&
|
|
74
|
+
current.radius === radius
|
|
75
|
+
? current
|
|
76
|
+
: { width: panelRect.width, height: panelRect.height, radius }
|
|
77
|
+
)
|
|
78
|
+
const sourceRect = source?.getBoundingClientRect()
|
|
79
|
+
const sourceArg: MenuRect | undefined =
|
|
80
|
+
sourceRect && sourceRect.width > 0 && sourceRect.height > 0
|
|
81
|
+
? {
|
|
82
|
+
left: sourceRect.left,
|
|
83
|
+
top: sourceRect.top,
|
|
84
|
+
width: sourceRect.width,
|
|
85
|
+
height: sourceRect.height,
|
|
86
|
+
}
|
|
87
|
+
: undefined
|
|
88
|
+
setReveal(
|
|
89
|
+
menuSurfaceReveal(sourceArg, {
|
|
90
|
+
left: panelRect.left,
|
|
91
|
+
top: panelRect.top,
|
|
92
|
+
width: panelRect.width,
|
|
93
|
+
height: panelRect.height,
|
|
94
|
+
radius,
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
measure()
|
|
99
|
+
let frame = 0
|
|
100
|
+
const schedule = () => {
|
|
101
|
+
cancelAnimationFrame(frame)
|
|
102
|
+
frame = requestAnimationFrame(measure)
|
|
103
|
+
}
|
|
104
|
+
const observer = new ResizeObserver(schedule)
|
|
105
|
+
observer.observe(surface)
|
|
106
|
+
if (source) observer.observe(source)
|
|
107
|
+
const viewport = window.visualViewport
|
|
108
|
+
viewport?.addEventListener("resize", schedule)
|
|
109
|
+
viewport?.addEventListener("scroll", schedule)
|
|
110
|
+
window.addEventListener("resize", schedule)
|
|
111
|
+
window.addEventListener("scroll", schedule, true)
|
|
112
|
+
return () => {
|
|
113
|
+
observer.disconnect()
|
|
114
|
+
cancelAnimationFrame(frame)
|
|
115
|
+
viewport?.removeEventListener("resize", schedule)
|
|
116
|
+
viewport?.removeEventListener("scroll", schedule)
|
|
117
|
+
window.removeEventListener("resize", schedule)
|
|
118
|
+
window.removeEventListener("scroll", schedule, true)
|
|
119
|
+
}
|
|
120
|
+
}, [sourceRef])
|
|
121
|
+
|
|
122
|
+
// Conceal the source while the morph is mounted; restore on unmount.
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
const source = sourceRef.current
|
|
125
|
+
if (!source) return
|
|
126
|
+
const restore = concealMenuSource(source, false)
|
|
127
|
+
return () => {
|
|
128
|
+
restore()
|
|
129
|
+
}
|
|
130
|
+
}, [sourceRef])
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<SheetPortal>
|
|
134
|
+
<div className="fixed inset-0 z-[75] flex items-center justify-center p-6">
|
|
135
|
+
<motion.div
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className="absolute inset-0 bg-black/30 backdrop-blur-md"
|
|
138
|
+
initial={{ opacity: 0 }}
|
|
139
|
+
animate={{ opacity: 1 }}
|
|
140
|
+
exit={{ opacity: 0 }}
|
|
141
|
+
transition={reducedMotion ? { duration: 0 } : { duration: 0.18 }}
|
|
142
|
+
onClick={dismissOnOutsidePress ? onClose : undefined}
|
|
143
|
+
/>
|
|
144
|
+
<motion.div
|
|
145
|
+
ref={panel}
|
|
146
|
+
role="dialog"
|
|
147
|
+
aria-modal="true"
|
|
148
|
+
aria-label={labelledBy ? undefined : label}
|
|
149
|
+
aria-labelledby={labelledBy}
|
|
150
|
+
tabIndex={-1}
|
|
151
|
+
className={cn(
|
|
152
|
+
"relative isolate flex w-full flex-col rounded-5xl outline-none",
|
|
153
|
+
"max-h-[92%] max-w-[720px]",
|
|
154
|
+
settled && "overflow-hidden",
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
{/* The morphing material — mirrors Popover's data-popover-surface
|
|
160
|
+
pattern. Starts at the source's rect (offset relative to the
|
|
161
|
+
panel) and grows to fill the panel, with the fluid overshoot
|
|
162
|
+
bounce. Border radius interpolates from the source's
|
|
163
|
+
"pillified" radius to the panel's authored radius. */}
|
|
164
|
+
<motion.div
|
|
165
|
+
key={reveal ? "surface" : "measure"}
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
data-morph-surface=""
|
|
168
|
+
onAnimationComplete={() => {
|
|
169
|
+
if (reveal) setSettled(true)
|
|
170
|
+
}}
|
|
171
|
+
className="glass-menu pointer-events-none absolute top-0 left-0 -z-10 rounded-5xl"
|
|
172
|
+
style={{ width: "100%", height: "100%" }}
|
|
173
|
+
initial={reducedMotion || !reveal ? false : reveal}
|
|
174
|
+
animate={
|
|
175
|
+
target
|
|
176
|
+
? {
|
|
177
|
+
x: 0,
|
|
178
|
+
y: 0,
|
|
179
|
+
width: target.width,
|
|
180
|
+
height: target.height,
|
|
181
|
+
opacity: 1,
|
|
182
|
+
borderRadius: target.radius,
|
|
183
|
+
}
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
186
|
+
exit={
|
|
187
|
+
reducedMotion || !reveal
|
|
188
|
+
? { opacity: 0 }
|
|
189
|
+
: { ...menuSurfaceExit(reveal), transition: fluidCollapse }
|
|
190
|
+
}
|
|
191
|
+
transition={
|
|
192
|
+
reducedMotion
|
|
193
|
+
? { duration: 0 }
|
|
194
|
+
: { ...fluidOvershoot, opacity: { duration: 0.18 } }
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
<motion.div
|
|
198
|
+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
199
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
200
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
201
|
+
exit={reducedMotion ? undefined : contentCrossfade.exit}
|
|
202
|
+
transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
</motion.div>
|
|
206
|
+
</motion.div>
|
|
207
|
+
</div>
|
|
208
|
+
</SheetPortal>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Disclosure state that keeps the overlay mounted through its exit animation,
|
|
5
|
+
* mirroring `useAnchoredDisclosure`. Pair with a `useRef<HTMLElement>(null)`
|
|
6
|
+
* for the source.
|
|
7
|
+
*/
|
|
8
|
+
export function useMorphDisclosure(disabled = false) {
|
|
9
|
+
const [open, setOpen] = useState(false)
|
|
10
|
+
const [mounted, setMounted] = useState(false)
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (disabled) setOpen(false)
|
|
13
|
+
}, [disabled])
|
|
14
|
+
const show = useCallback(() => {
|
|
15
|
+
setMounted(true)
|
|
16
|
+
setOpen(true)
|
|
17
|
+
}, [])
|
|
18
|
+
const toggle = useCallback(() => {
|
|
19
|
+
setMounted(true)
|
|
20
|
+
setOpen((value) => !value)
|
|
21
|
+
}, [])
|
|
22
|
+
const close = useCallback(() => setOpen(false), [])
|
|
23
|
+
const onExited = useCallback(() => {
|
|
24
|
+
if (!open || disabled) setMounted(false)
|
|
25
|
+
}, [open, disabled])
|
|
26
|
+
return { open: open && !disabled, mounted, show, toggle, close, onExited }
|
|
27
|
+
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { useId } from "react"
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
|
|
4
|
+
export type RadioGroupSize = "sm" | "md" | "lg"
|
|
5
|
+
|
|
6
|
+
const legendSizes: Record<RadioGroupSize, string> = {
|
|
7
|
+
sm: "mb-1.5 text-caption2 font-medium text-label-secondary",
|
|
8
|
+
md: "mb-2 text-footnote font-medium text-label-secondary",
|
|
9
|
+
lg: "mb-2.5 text-subheadline font-medium text-label-secondary",
|
|
10
|
+
}
|
|
11
|
+
const rowSizes: Record<RadioGroupSize, string> = {
|
|
12
|
+
sm: "flex min-h-7 items-start gap-1.5 text-footnote text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-10",
|
|
13
|
+
md: "flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11",
|
|
14
|
+
lg: "flex min-h-10 items-start gap-2.5 text-body text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-12",
|
|
15
|
+
}
|
|
16
|
+
const radioSizes: Record<RadioGroupSize, string> = {
|
|
17
|
+
sm: "mt-0.5 size-3.5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
18
|
+
md: "mt-1 size-4 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
19
|
+
lg: "mt-1 size-5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
20
|
+
}
|
|
21
|
+
const descriptionSizes: Record<RadioGroupSize, string> = {
|
|
22
|
+
sm: "block text-caption2 text-label-secondary",
|
|
23
|
+
md: "block text-footnote text-label-secondary",
|
|
24
|
+
lg: "block text-subheadline text-label-secondary",
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
export interface RadioGroupOption<T extends string> {
|
|
5
28
|
value: T
|
|
6
29
|
label: string
|
|
@@ -15,6 +38,8 @@ export interface RadioGroupProps<T extends string> {
|
|
|
15
38
|
disabled?: boolean
|
|
16
39
|
name?: string
|
|
17
40
|
className?: string
|
|
41
|
+
/** Legend, row, radio glyph and description density. @default "md" */
|
|
42
|
+
size?: RadioGroupSize
|
|
18
43
|
}
|
|
19
44
|
/** A labelled choice group using the browser's native radio keyboard behavior. */
|
|
20
45
|
export function RadioGroup<T extends string>({
|
|
@@ -25,16 +50,14 @@ export function RadioGroup<T extends string>({
|
|
|
25
50
|
disabled,
|
|
26
51
|
name,
|
|
27
52
|
className,
|
|
53
|
+
size = "md",
|
|
28
54
|
}: RadioGroupProps<T>) {
|
|
29
55
|
const id = useId()
|
|
30
56
|
return (
|
|
31
57
|
<fieldset disabled={disabled} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
32
|
-
<legend className=
|
|
58
|
+
<legend className={legendSizes[size]}>{label}</legend>
|
|
33
59
|
{options.map((option, index) => (
|
|
34
|
-
<label
|
|
35
|
-
key={option.value}
|
|
36
|
-
className="flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11"
|
|
37
|
-
>
|
|
60
|
+
<label key={option.value} className={rowSizes[size]}>
|
|
38
61
|
<input
|
|
39
62
|
type="radio"
|
|
40
63
|
name={name ?? id}
|
|
@@ -43,15 +66,12 @@ export function RadioGroup<T extends string>({
|
|
|
43
66
|
disabled={option.disabled}
|
|
44
67
|
onChange={() => onChange(option.value)}
|
|
45
68
|
aria-describedby={option.description ? `${id}-${index}` : undefined}
|
|
46
|
-
className=
|
|
69
|
+
className={radioSizes[size]}
|
|
47
70
|
/>
|
|
48
71
|
<span>
|
|
49
72
|
{option.label}
|
|
50
73
|
{option.description && (
|
|
51
|
-
<span
|
|
52
|
-
id={`${id}-${index}`}
|
|
53
|
-
className="block text-footnote text-label-secondary"
|
|
54
|
-
>
|
|
74
|
+
<span id={`${id}-${index}`} className={descriptionSizes[size]}>
|
|
55
75
|
{option.description}
|
|
56
76
|
</span>
|
|
57
77
|
)}
|
|
@@ -5,6 +5,29 @@ import { haptic } from "../../lib/haptics"
|
|
|
5
5
|
import { spring } from "../../motion/transitions"
|
|
6
6
|
import { Picker } from "../picker/picker"
|
|
7
7
|
|
|
8
|
+
export type SegmentedControlSize = "sm" | "md" | "lg"
|
|
9
|
+
|
|
10
|
+
const segmentPaddings: Record<SegmentedControlSize, string> = {
|
|
11
|
+
sm: "p-[2px]",
|
|
12
|
+
md: "p-[3px]",
|
|
13
|
+
lg: "p-[4px]",
|
|
14
|
+
}
|
|
15
|
+
const segmentLabelClasses: Record<SegmentedControlSize, string> = {
|
|
16
|
+
sm: "min-w-[52px] px-2 py-0.5 text-caption2 whitespace-nowrap",
|
|
17
|
+
md: "min-w-[64px] px-3 py-1 text-footnote whitespace-nowrap",
|
|
18
|
+
lg: "min-w-[80px] px-4 py-1.5 text-subheadline whitespace-nowrap",
|
|
19
|
+
}
|
|
20
|
+
const segmentRadius: Record<SegmentedControlSize, string> = {
|
|
21
|
+
sm: "rounded-sm",
|
|
22
|
+
md: "rounded-md",
|
|
23
|
+
lg: "rounded-lg",
|
|
24
|
+
}
|
|
25
|
+
const fallbackPickerSizes: Record<SegmentedControlSize, string> = {
|
|
26
|
+
sm: "rounded-md py-1 text-caption2 font-semibold",
|
|
27
|
+
md: "rounded-lg py-1.5 text-footnote font-semibold",
|
|
28
|
+
lg: "rounded-lg py-2 text-subheadline font-semibold",
|
|
29
|
+
}
|
|
30
|
+
|
|
8
31
|
export interface SegmentedControlProps<T extends string> {
|
|
9
32
|
/** Value of the selected segment. */
|
|
10
33
|
value: T
|
|
@@ -16,6 +39,8 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
16
39
|
label: string
|
|
17
40
|
/** Custom classes for the control container. */
|
|
18
41
|
className?: string
|
|
42
|
+
/** Segment padding, label text scale and min-width. @default "md" */
|
|
43
|
+
size?: SegmentedControlSize
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
@@ -30,6 +55,7 @@ export function SegmentedControl<T extends string>({
|
|
|
30
55
|
onChange,
|
|
31
56
|
label,
|
|
32
57
|
className,
|
|
58
|
+
size = "md",
|
|
33
59
|
}: SegmentedControlProps<T>) {
|
|
34
60
|
const id = useId()
|
|
35
61
|
const reducedMotion = useReducedMotion()
|
|
@@ -55,16 +81,25 @@ export function SegmentedControl<T extends string>({
|
|
|
55
81
|
<div
|
|
56
82
|
ref={measure}
|
|
57
83
|
aria-hidden="true"
|
|
58
|
-
className=
|
|
84
|
+
className={cn(
|
|
85
|
+
"pointer-events-none invisible absolute top-0 flex w-max font-semibold",
|
|
86
|
+
segmentPaddings[size],
|
|
87
|
+
segmentLabelClasses[size].replace(/min-w-\[[^\]]+\]\s*/, "")
|
|
88
|
+
)}
|
|
59
89
|
>
|
|
60
90
|
{options.map((option) => (
|
|
61
|
-
<span key={option.value} className=
|
|
91
|
+
<span key={option.value} className={segmentLabelClasses[size]}>
|
|
62
92
|
{option.label}
|
|
63
93
|
</span>
|
|
64
94
|
))}
|
|
65
95
|
</div>
|
|
66
96
|
{fits ? (
|
|
67
|
-
<fieldset
|
|
97
|
+
<fieldset
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex w-full rounded-lg bg-fill shadow-[inset_0_1px_2px_rgb(40_30_0/0.06)]",
|
|
100
|
+
segmentPaddings[size]
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
68
103
|
<legend className="sr-only">{label}</legend>
|
|
69
104
|
{options.map((option) => {
|
|
70
105
|
const selected = option.value === value
|
|
@@ -72,7 +107,9 @@ export function SegmentedControl<T extends string>({
|
|
|
72
107
|
<label
|
|
73
108
|
key={option.value}
|
|
74
109
|
className={cn(
|
|
75
|
-
"relative isolate
|
|
110
|
+
"relative isolate flex-1 cursor-pointer text-center whitespace-nowrap transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-accent/60",
|
|
111
|
+
segmentLabelClasses[size].replace("whitespace-nowrap", ""),
|
|
112
|
+
segmentRadius[size],
|
|
76
113
|
selected
|
|
77
114
|
? "font-semibold text-label"
|
|
78
115
|
: "font-medium text-label-secondary hover:text-label"
|
|
@@ -92,7 +129,10 @@ export function SegmentedControl<T extends string>({
|
|
|
92
129
|
{selected && (
|
|
93
130
|
<motion.span
|
|
94
131
|
layoutId={`segment-${id}`}
|
|
95
|
-
className=
|
|
132
|
+
className={cn(
|
|
133
|
+
"absolute inset-0 -z-10 bg-elevated shadow-[0_0_0_0.5px_rgb(0_0_0/0.06),0_1px_2px_rgb(0_0_0/0.08),0_3px_8px_-2px_rgb(0_0_0/0.10)]",
|
|
134
|
+
segmentRadius[size]
|
|
135
|
+
)}
|
|
96
136
|
transition={reducedMotion ? { duration: 0 } : spring.snappy}
|
|
97
137
|
/>
|
|
98
138
|
)}
|
|
@@ -108,7 +148,8 @@ export function SegmentedControl<T extends string>({
|
|
|
108
148
|
options={options}
|
|
109
149
|
onChange={onChange}
|
|
110
150
|
searchable={false}
|
|
111
|
-
className=
|
|
151
|
+
className={fallbackPickerSizes[size]}
|
|
152
|
+
size={size === "sm" ? "sm" : size === "lg" ? "lg" : "md"}
|
|
112
153
|
/>
|
|
113
154
|
)}
|
|
114
155
|
</div>
|
|
@@ -7,6 +7,18 @@ import { SheetPortal } from "./sheet-portal"
|
|
|
7
7
|
import { useSheetFocus } from "./use-sheet-focus"
|
|
8
8
|
|
|
9
9
|
export type SheetPlacement = "adaptive" | "center"
|
|
10
|
+
export type SheetSize = "sm" | "md" | "lg"
|
|
11
|
+
|
|
12
|
+
const outerPaddings: Record<SheetSize, string> = {
|
|
13
|
+
sm: "p-4",
|
|
14
|
+
md: "p-6",
|
|
15
|
+
lg: "p-8",
|
|
16
|
+
}
|
|
17
|
+
const panelMaxWidths: Record<SheetSize, string> = {
|
|
18
|
+
sm: "max-w-sm",
|
|
19
|
+
md: "max-w-[560px]",
|
|
20
|
+
lg: "max-w-2xl",
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
export interface SheetProps {
|
|
12
24
|
/** Controls whether the sheet is presented. */
|
|
@@ -21,6 +33,8 @@ export interface SheetProps {
|
|
|
21
33
|
labelledBy?: string
|
|
22
34
|
/** `adaptive` rises from the bottom on phones; `center` always floats. @default "adaptive" */
|
|
23
35
|
placement?: SheetPlacement
|
|
36
|
+
/** Outer padding and panel max-width. @default "md" */
|
|
37
|
+
size?: SheetSize
|
|
24
38
|
className?: string
|
|
25
39
|
children: ReactNode
|
|
26
40
|
}
|
|
@@ -47,6 +61,7 @@ function OpenSheet({
|
|
|
47
61
|
label,
|
|
48
62
|
labelledBy,
|
|
49
63
|
placement = "adaptive",
|
|
64
|
+
size = "md",
|
|
50
65
|
className,
|
|
51
66
|
children,
|
|
52
67
|
}: Omit<SheetProps, "open">) {
|
|
@@ -59,7 +74,9 @@ function OpenSheet({
|
|
|
59
74
|
<div
|
|
60
75
|
className={cn(
|
|
61
76
|
"fixed inset-0 z-[65] flex justify-center",
|
|
62
|
-
placement === "center"
|
|
77
|
+
placement === "center"
|
|
78
|
+
? cn("items-center", outerPaddings[size])
|
|
79
|
+
: cn("items-end sm:items-center", `sm:${outerPaddings[size]}`)
|
|
63
80
|
)}
|
|
64
81
|
style={
|
|
65
82
|
viewport
|
|
@@ -91,7 +108,8 @@ function OpenSheet({
|
|
|
91
108
|
tabIndex={-1}
|
|
92
109
|
className={cn(
|
|
93
110
|
"glass-menu relative flex w-full flex-col overflow-hidden",
|
|
94
|
-
"max-h-[92%]
|
|
111
|
+
"max-h-[92%]",
|
|
112
|
+
panelMaxWidths[size],
|
|
95
113
|
placement === "center" ? "rounded-sheet" : "rounded-t-sheet sm:rounded-sheet",
|
|
96
114
|
className
|
|
97
115
|
)}
|
|
@@ -2,10 +2,55 @@ import { type ComponentProps, useId } from "react"
|
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
import { clampRange } from "../../lib/range-value"
|
|
4
4
|
|
|
5
|
+
export type SliderSize = "sm" | "md" | "lg"
|
|
6
|
+
|
|
7
|
+
const labelSizes: Record<SliderSize, string> = {
|
|
8
|
+
sm: "text-footnote font-semibold text-label",
|
|
9
|
+
md: "text-subheadline font-semibold text-label",
|
|
10
|
+
lg: "text-body font-semibold text-label",
|
|
11
|
+
}
|
|
12
|
+
const inputHeights: Record<SliderSize, string> = {
|
|
13
|
+
sm: "h-9",
|
|
14
|
+
md: "h-11",
|
|
15
|
+
lg: "h-13",
|
|
16
|
+
}
|
|
17
|
+
const descriptionSizes: Record<SliderSize, string> = {
|
|
18
|
+
sm: "text-caption2 text-label-secondary",
|
|
19
|
+
md: "text-footnote text-label-secondary",
|
|
20
|
+
lg: "text-subheadline text-label-secondary",
|
|
21
|
+
}
|
|
22
|
+
// Track height, thumb size, and the webkit thumb's -mt offset (derived from
|
|
23
|
+
// (thumb - track) / 2: md (20-6)/2=7, sm (16-4)/2=6, lg (24-8)/2=8).
|
|
24
|
+
const webkitTrack: Record<SliderSize, string> = {
|
|
25
|
+
sm: "[&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-separator",
|
|
26
|
+
md: "[&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-separator",
|
|
27
|
+
lg: "[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-separator",
|
|
28
|
+
}
|
|
29
|
+
const webkitThumb: Record<SliderSize, string> = {
|
|
30
|
+
sm: "[&::-webkit-slider-thumb]:-mt-[6px] [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-accent [&::-webkit-slider-thumb]:bg-elevated [&::-webkit-slider-thumb]:shadow-sm",
|
|
31
|
+
md: "[&::-webkit-slider-thumb]:-mt-[7px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-accent [&::-webkit-slider-thumb]:bg-elevated [&::-webkit-slider-thumb]:shadow-sm",
|
|
32
|
+
lg: "[&::-webkit-slider-thumb]:-mt-[8px] [&::-webkit-slider-thumb]:size-6 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-accent [&::-webkit-slider-thumb]:bg-elevated [&::-webkit-slider-thumb]:shadow-sm",
|
|
33
|
+
}
|
|
34
|
+
const mozTrack: Record<SliderSize, string> = {
|
|
35
|
+
sm: "[&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-separator",
|
|
36
|
+
md: "[&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-separator",
|
|
37
|
+
lg: "[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-separator",
|
|
38
|
+
}
|
|
39
|
+
const mozProgress: Record<SliderSize, string> = {
|
|
40
|
+
sm: "[&::-moz-range-progress]:h-1 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-accent",
|
|
41
|
+
md: "[&::-moz-range-progress]:h-1.5 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-accent",
|
|
42
|
+
lg: "[&::-moz-range-progress]:h-2 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-accent",
|
|
43
|
+
}
|
|
44
|
+
const mozThumb: Record<SliderSize, string> = {
|
|
45
|
+
sm: "[&::-moz-range-thumb]:size-3 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-accent [&::-moz-range-thumb]:bg-elevated",
|
|
46
|
+
md: "[&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-accent [&::-moz-range-thumb]:bg-elevated",
|
|
47
|
+
lg: "[&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-accent [&::-moz-range-thumb]:bg-elevated",
|
|
48
|
+
}
|
|
49
|
+
|
|
5
50
|
export interface SliderProps
|
|
6
51
|
extends Omit<
|
|
7
52
|
ComponentProps<"input">,
|
|
8
|
-
"type" | "value" | "defaultValue" | "onChange" | "min" | "max" | "step"
|
|
53
|
+
"type" | "value" | "defaultValue" | "onChange" | "min" | "max" | "step" | "size"
|
|
9
54
|
> {
|
|
10
55
|
value: number
|
|
11
56
|
onChange: (value: number) => void
|
|
@@ -16,6 +61,8 @@ export interface SliderProps
|
|
|
16
61
|
description?: string
|
|
17
62
|
/** Provides a localized, spoken description of the current value. */
|
|
18
63
|
formatValue?: (value: number) => string
|
|
64
|
+
/** Track height, thumb size and label/description text scale. @default "md" */
|
|
65
|
+
size?: SliderSize
|
|
19
66
|
}
|
|
20
67
|
|
|
21
68
|
/** Native keyboard, touch and pointer behavior with Passui's control treatment. */
|
|
@@ -28,6 +75,7 @@ export function Slider({
|
|
|
28
75
|
step = 1,
|
|
29
76
|
description,
|
|
30
77
|
formatValue,
|
|
78
|
+
size = "md",
|
|
31
79
|
id: suppliedId,
|
|
32
80
|
className,
|
|
33
81
|
disabled,
|
|
@@ -40,7 +88,7 @@ export function Slider({
|
|
|
40
88
|
|
|
41
89
|
return (
|
|
42
90
|
<div className={cn("flex min-w-0 flex-col gap-2", disabled && "opacity-45")}>
|
|
43
|
-
<label htmlFor={id} className=
|
|
91
|
+
<label htmlFor={id} className={labelSizes[size]}>
|
|
44
92
|
{label}
|
|
45
93
|
</label>
|
|
46
94
|
<input
|
|
@@ -56,18 +104,19 @@ export function Slider({
|
|
|
56
104
|
aria-describedby={description ? `${id}-description` : undefined}
|
|
57
105
|
onChange={(event) => onChange(Number(event.currentTarget.value))}
|
|
58
106
|
className={cn(
|
|
59
|
-
"
|
|
107
|
+
"w-full min-w-0 cursor-pointer appearance-none bg-transparent accent-accent",
|
|
108
|
+
inputHeights[size],
|
|
60
109
|
"focus-visible:rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
webkitTrack[size],
|
|
111
|
+
webkitThumb[size],
|
|
112
|
+
mozTrack[size],
|
|
113
|
+
mozProgress[size],
|
|
114
|
+
mozThumb[size],
|
|
66
115
|
className
|
|
67
116
|
)}
|
|
68
117
|
/>
|
|
69
118
|
{description && (
|
|
70
|
-
<p id={`${id}-description`} className=
|
|
119
|
+
<p id={`${id}-description`} className={descriptionSizes[size]}>
|
|
71
120
|
{description}
|
|
72
121
|
</p>
|
|
73
122
|
)}
|