@passui/react 0.1.36 → 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/segmented-control/segmented-control.d.ts +4 -1
- 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/tab-view/tab-view.d.ts +4 -1
- package/dist/types/src/components/toggle/toggle.d.ts +4 -1
- package/package.json +1 -1
- package/src/components/accordion/accordion.tsx +16 -3
- package/src/components/date-picker/date-picker.tsx +37 -6
- package/src/components/segmented-control/segmented-control.tsx +47 -6
- package/src/components/sheet/sheet.tsx +1 -1
- package/src/components/slider/slider.tsx +58 -9
- package/src/components/stepper/stepper.tsx +37 -11
- package/src/components/tab-view/tab-view.tsx +31 -4
- package/src/components/toggle/toggle.tsx +105 -20
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { type DisclosureGroupSize } from "../disclosure-group/disclosure-group";
|
|
3
|
+
export type AccordionSize = DisclosureGroupSize;
|
|
2
4
|
export interface AccordionItem {
|
|
3
5
|
/** Stable value used to identify the expanded item. */
|
|
4
6
|
value: string;
|
|
@@ -13,7 +15,9 @@ export interface AccordionProps {
|
|
|
13
15
|
value: string | null;
|
|
14
16
|
/** Selecting the open item reports `null`, so every item can collapse. */
|
|
15
17
|
onValueChange: (value: string | null) => void;
|
|
18
|
+
/** Density passed through to each DisclosureGroup. @default "md" */
|
|
19
|
+
size?: AccordionSize;
|
|
16
20
|
className?: string;
|
|
17
21
|
}
|
|
18
22
|
/** A single-open, collapsible accordion with labelled animated content regions. */
|
|
19
|
-
export declare function Accordion({ items, value, onValueChange, className }: AccordionProps): import("react").JSX.Element;
|
|
23
|
+
export declare function Accordion({ items, value, onValueChange, size, className }: AccordionProps): import("react").JSX.Element;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextFieldSize } from "../text-field/text-field";
|
|
1
2
|
type DatePickerProps = {
|
|
2
3
|
value: string;
|
|
3
4
|
onChange: (value: string) => void;
|
|
@@ -10,7 +11,9 @@ type DatePickerProps = {
|
|
|
10
11
|
};
|
|
11
12
|
locale: string;
|
|
12
13
|
max?: string;
|
|
14
|
+
/** Field scale passed to each TextField; matches TextField's xs|sm|md|lg. @default "md" */
|
|
15
|
+
size?: TextFieldSize;
|
|
13
16
|
};
|
|
14
17
|
/** Segmented date entry suited to birthdays, without a native calendar dialog. */
|
|
15
|
-
export declare function DatePicker({ value, onChange, label, labels, locale, max }: DatePickerProps): import("react").JSX.Element;
|
|
18
|
+
export declare function DatePicker({ value, onChange, label, labels, locale, max, size, }: DatePickerProps): import("react").JSX.Element;
|
|
16
19
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type SegmentedControlSize = "sm" | "md" | "lg";
|
|
1
2
|
export interface SegmentedControlProps<T extends string> {
|
|
2
3
|
/** Value of the selected segment. */
|
|
3
4
|
value: T;
|
|
@@ -12,6 +13,8 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
12
13
|
label: string;
|
|
13
14
|
/** Custom classes for the control container. */
|
|
14
15
|
className?: string;
|
|
16
|
+
/** Segment padding, label text scale and min-width. @default "md" */
|
|
17
|
+
size?: SegmentedControlSize;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Mutually exclusive choices with a sliding selection, as on Apple
|
|
@@ -19,4 +22,4 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
19
22
|
* translation, a narrow pane) it becomes a pop-up button instead of
|
|
20
23
|
* squeezing or spilling its text.
|
|
21
24
|
*/
|
|
22
|
-
export declare function SegmentedControl<T extends string>({ value, options, onChange, label, className, }: SegmentedControlProps<T>): import("react").JSX.Element;
|
|
25
|
+
export declare function SegmentedControl<T extends string>({ value, options, onChange, label, className, size, }: SegmentedControlProps<T>): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentProps } from "react";
|
|
2
|
-
export
|
|
2
|
+
export type SliderSize = "sm" | "md" | "lg";
|
|
3
|
+
export interface SliderProps extends Omit<ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange" | "min" | "max" | "step" | "size"> {
|
|
3
4
|
value: number;
|
|
4
5
|
onChange: (value: number) => void;
|
|
5
6
|
label: string;
|
|
@@ -9,6 +10,8 @@ export interface SliderProps extends Omit<ComponentProps<"input">, "type" | "val
|
|
|
9
10
|
description?: string;
|
|
10
11
|
/** Provides a localized, spoken description of the current value. */
|
|
11
12
|
formatValue?: (value: number) => string;
|
|
13
|
+
/** Track height, thumb size and label/description text scale. @default "md" */
|
|
14
|
+
size?: SliderSize;
|
|
12
15
|
}
|
|
13
16
|
/** Native keyboard, touch and pointer behavior with Passui's control treatment. */
|
|
14
|
-
export declare function Slider({ value, onChange, label, min, max, step, description, formatValue, id: suppliedId, className, disabled, ...props }: SliderProps): import("react").JSX.Element;
|
|
17
|
+
export declare function Slider({ value, onChange, label, min, max, step, description, formatValue, size, id: suppliedId, className, disabled, ...props }: SliderProps): import("react").JSX.Element;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type StepperSize = "sm" | "md" | "lg";
|
|
1
2
|
export interface StepperProps {
|
|
2
3
|
value: number;
|
|
3
4
|
onChange: (value: number) => void;
|
|
@@ -14,6 +15,8 @@ export interface StepperProps {
|
|
|
14
15
|
className?: string;
|
|
15
16
|
/** Formats the displayed value. */
|
|
16
17
|
formatValue?: (value: number) => string;
|
|
18
|
+
/** Label, output and +/- button scale. @default "md" */
|
|
19
|
+
size?: StepperSize;
|
|
17
20
|
}
|
|
18
21
|
/** Discrete increments with disabled boundary actions and a spoken value. */
|
|
19
|
-
export declare function Stepper({ value, onChange, label, labels, min, max, step, disabled, className, formatValue, }: StepperProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Stepper({ value, onChange, label, labels, min, max, step, disabled, className, formatValue, size, }: StepperProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
export type TabViewAlign = "start" | "center";
|
|
3
|
+
export type TabViewSize = "sm" | "md" | "lg";
|
|
3
4
|
export interface TabViewProps<Value extends string> {
|
|
4
5
|
/** Available tabs, in keyboard navigation order. */
|
|
5
6
|
options: readonly {
|
|
@@ -18,7 +19,9 @@ export interface TabViewProps<Value extends string> {
|
|
|
18
19
|
label: string;
|
|
19
20
|
/** Renders the selected tab's panel. */
|
|
20
21
|
children: (value: Value) => ReactNode;
|
|
22
|
+
/** Tablist height, tab text scale and underline offset. @default "md" */
|
|
23
|
+
size?: TabViewSize;
|
|
21
24
|
className?: string;
|
|
22
25
|
}
|
|
23
26
|
/** Content tabs with native buttons, roving focus and automatic keyboard activation. */
|
|
24
|
-
export declare function TabView<Value extends string>({ options, defaultValue, value: controlledValue, onValueChange, align, label, children, className, }: TabViewProps<Value>): import("react").JSX.Element | null;
|
|
27
|
+
export declare function TabView<Value extends string>({ options, defaultValue, value: controlledValue, onValueChange, align, label, children, size, className, }: TabViewProps<Value>): import("react").JSX.Element | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ToggleStyle = "switch" | "checkbox";
|
|
2
|
+
export type ToggleSize = "sm" | "md" | "lg";
|
|
2
3
|
export interface ToggleProps {
|
|
3
4
|
/** Current on or off state. */
|
|
4
5
|
checked: boolean;
|
|
@@ -13,7 +14,9 @@ export interface ToggleProps {
|
|
|
13
14
|
mixed?: boolean;
|
|
14
15
|
/** Keeps the accessible name while hiding the visible label. @default false */
|
|
15
16
|
labelHidden?: boolean;
|
|
17
|
+
/** Switch track size, thumb travel, checkbox box and label text scale. @default "md" */
|
|
18
|
+
size?: ToggleSize;
|
|
16
19
|
/** Custom classes for the toggle container. */
|
|
17
20
|
className?: string;
|
|
18
21
|
}
|
|
19
|
-
export declare function Toggle({ checked, onChange, label, disabled, style, mixed, labelHidden, className, }: ToggleProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Toggle({ checked, onChange, label, disabled, style, mixed, labelHidden, size, className, }: ToggleProps): import("react").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
|
-
import { DisclosureGroup } from "../disclosure-group/disclosure-group"
|
|
2
|
+
import { DisclosureGroup, type DisclosureGroupSize } from "../disclosure-group/disclosure-group"
|
|
3
3
|
import { cn } from "../../lib/class-names"
|
|
4
4
|
|
|
5
|
+
export type AccordionSize = DisclosureGroupSize
|
|
6
|
+
|
|
7
|
+
const containerRadius: Record<AccordionSize, string> = {
|
|
8
|
+
sm: "rounded-xl",
|
|
9
|
+
md: "rounded-2xl",
|
|
10
|
+
lg: "rounded-3xl",
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export interface AccordionItem {
|
|
6
14
|
/** Stable value used to identify the expanded item. */
|
|
7
15
|
value: string
|
|
@@ -17,15 +25,19 @@ export interface AccordionProps {
|
|
|
17
25
|
value: string | null
|
|
18
26
|
/** Selecting the open item reports `null`, so every item can collapse. */
|
|
19
27
|
onValueChange: (value: string | null) => void
|
|
28
|
+
/** Density passed through to each DisclosureGroup. @default "md" */
|
|
29
|
+
size?: AccordionSize
|
|
20
30
|
className?: string
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/** A single-open, collapsible accordion with labelled animated content regions. */
|
|
24
|
-
export function Accordion({ items, value, onValueChange, className }: AccordionProps) {
|
|
34
|
+
export function Accordion({ items, value, onValueChange, size = "md", className }: AccordionProps) {
|
|
25
35
|
return (
|
|
26
36
|
<div
|
|
27
37
|
className={cn(
|
|
28
|
-
"flex w-full min-w-0 flex-col overflow-hidden
|
|
38
|
+
"flex w-full min-w-0 flex-col overflow-hidden",
|
|
39
|
+
containerRadius[size],
|
|
40
|
+
"border border-separator/60 bg-elevated",
|
|
29
41
|
className
|
|
30
42
|
)}
|
|
31
43
|
>
|
|
@@ -36,6 +48,7 @@ export function Accordion({ items, value, onValueChange, className }: AccordionP
|
|
|
36
48
|
description={item.description}
|
|
37
49
|
open={value === item.value}
|
|
38
50
|
disabled={item.disabled}
|
|
51
|
+
size={size}
|
|
39
52
|
triggerClassName="rounded-none"
|
|
40
53
|
onOpenChange={(open) => onValueChange(open ? item.value : null)}
|
|
41
54
|
className={cn(index > 0 && "border-t border-separator/60")}
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { useState } from "react"
|
|
2
|
-
import { TextField } from "../text-field/text-field"
|
|
2
|
+
import { TextField, type TextFieldSize } from "../text-field/text-field"
|
|
3
|
+
import { cn } from "../../lib/class-names"
|
|
3
4
|
import { dateValue } from "./date-value"
|
|
4
5
|
|
|
6
|
+
const legendText: Record<TextFieldSize, string> = {
|
|
7
|
+
xs: "mb-2 text-caption font-medium",
|
|
8
|
+
sm: "mb-2 text-footnote font-medium",
|
|
9
|
+
md: "mb-3 text-subheadline font-medium",
|
|
10
|
+
lg: "mb-3 text-body font-medium",
|
|
11
|
+
}
|
|
12
|
+
const fieldsetGap: Record<TextFieldSize, string> = {
|
|
13
|
+
xs: "gap-2",
|
|
14
|
+
sm: "gap-2",
|
|
15
|
+
md: "gap-3",
|
|
16
|
+
lg: "gap-4",
|
|
17
|
+
}
|
|
18
|
+
const errorText: Record<TextFieldSize, string> = {
|
|
19
|
+
xs: "text-caption2 text-danger",
|
|
20
|
+
sm: "text-caption text-danger",
|
|
21
|
+
md: "text-footnote text-danger",
|
|
22
|
+
lg: "text-subheadline text-danger",
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
type DatePickerProps = {
|
|
6
26
|
value: string
|
|
7
27
|
onChange: (value: string) => void
|
|
@@ -14,10 +34,20 @@ type DatePickerProps = {
|
|
|
14
34
|
}
|
|
15
35
|
locale: string
|
|
16
36
|
max?: string
|
|
37
|
+
/** Field scale passed to each TextField; matches TextField's xs|sm|md|lg. @default "md" */
|
|
38
|
+
size?: TextFieldSize
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
/** Segmented date entry suited to birthdays, without a native calendar dialog. */
|
|
20
|
-
export function DatePicker({
|
|
42
|
+
export function DatePicker({
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
label,
|
|
46
|
+
labels,
|
|
47
|
+
locale,
|
|
48
|
+
max,
|
|
49
|
+
size = "md",
|
|
50
|
+
}: DatePickerProps) {
|
|
21
51
|
const [year, month, day] = value.split("-")
|
|
22
52
|
const [parts, setParts] = useState({
|
|
23
53
|
year: year ?? "",
|
|
@@ -41,9 +71,9 @@ export function DatePicker({ value, onChange, label, labels, locale, max }: Date
|
|
|
41
71
|
} => part.type === "year" || part.type === "month" || part.type === "day"
|
|
42
72
|
)
|
|
43
73
|
return (
|
|
44
|
-
<fieldset className="flex min-w-0 flex-col
|
|
45
|
-
<legend className=
|
|
46
|
-
<div className="grid grid-cols-3
|
|
74
|
+
<fieldset className={cn("flex min-w-0 flex-col", fieldsetGap[size])}>
|
|
75
|
+
<legend className={legendText[size]}>{label}</legend>
|
|
76
|
+
<div className={cn("grid grid-cols-3", fieldsetGap[size])}>
|
|
47
77
|
{order.map(({ type }) => (
|
|
48
78
|
<TextField
|
|
49
79
|
key={type}
|
|
@@ -52,6 +82,7 @@ export function DatePicker({ value, onChange, label, labels, locale, max }: Date
|
|
|
52
82
|
autoComplete={`bday-${type}`}
|
|
53
83
|
maxLength={type === "year" ? 4 : 2}
|
|
54
84
|
value={parts[type]}
|
|
85
|
+
size={size}
|
|
55
86
|
aria-invalid={invalid || undefined}
|
|
56
87
|
onChange={(event) => {
|
|
57
88
|
const next = {
|
|
@@ -65,7 +96,7 @@ export function DatePicker({ value, onChange, label, labels, locale, max }: Date
|
|
|
65
96
|
))}
|
|
66
97
|
</div>
|
|
67
98
|
{invalid && (
|
|
68
|
-
<p role="status" className=
|
|
99
|
+
<p role="status" className={errorText[size]}>
|
|
69
100
|
{labels.invalid}
|
|
70
101
|
</p>
|
|
71
102
|
)}
|
|
@@ -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>
|
|
@@ -76,7 +76,7 @@ function OpenSheet({
|
|
|
76
76
|
"fixed inset-0 z-[65] flex justify-center",
|
|
77
77
|
placement === "center"
|
|
78
78
|
? cn("items-center", outerPaddings[size])
|
|
79
|
-
: cn("items-end sm:items-center",
|
|
79
|
+
: cn("items-end sm:items-center", `sm:${outerPaddings[size]}`)
|
|
80
80
|
)}
|
|
81
81
|
style={
|
|
82
82
|
viewport
|
|
@@ -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
|
)}
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { useId } from "react"
|
|
2
|
-
import { Button } from "../button/button"
|
|
2
|
+
import { Button, type ButtonSize } from "../button/button"
|
|
3
3
|
import { cn } from "../../lib/class-names"
|
|
4
4
|
import { clampRange, stepRange } from "../../lib/range-value"
|
|
5
5
|
import { RollingDigit } from "./rolling-digit"
|
|
6
6
|
|
|
7
|
+
export type StepperSize = "sm" | "md" | "lg"
|
|
8
|
+
|
|
9
|
+
const labelText: Record<StepperSize, string> = {
|
|
10
|
+
sm: "min-w-0 text-footnote text-label",
|
|
11
|
+
md: "min-w-0 text-subheadline text-label",
|
|
12
|
+
lg: "min-w-0 text-body text-label",
|
|
13
|
+
}
|
|
14
|
+
const outputText: Record<StepperSize, string> = {
|
|
15
|
+
sm: "text-footnote tabular-nums text-label",
|
|
16
|
+
md: "text-subheadline tabular-nums text-label",
|
|
17
|
+
lg: "text-body tabular-nums text-label",
|
|
18
|
+
}
|
|
19
|
+
const buttonSizes: Record<StepperSize, { size: ButtonSize; cls: string }> = {
|
|
20
|
+
sm: {
|
|
21
|
+
size: "icon-sm",
|
|
22
|
+
cls: "size-7 rounded-none text-subheadline text-accent-text [@media(pointer:coarse)]:size-10",
|
|
23
|
+
},
|
|
24
|
+
md: {
|
|
25
|
+
size: "icon",
|
|
26
|
+
cls: "size-8 rounded-none text-title3 text-accent-text [@media(pointer:coarse)]:size-11",
|
|
27
|
+
},
|
|
28
|
+
lg: {
|
|
29
|
+
size: "icon-lg",
|
|
30
|
+
cls: "size-9 rounded-none text-title2 text-accent-text [@media(pointer:coarse)]:size-12",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
export interface StepperProps {
|
|
8
35
|
value: number
|
|
9
36
|
onChange: (value: number) => void
|
|
@@ -17,6 +44,8 @@ export interface StepperProps {
|
|
|
17
44
|
className?: string
|
|
18
45
|
/** Formats the displayed value. */
|
|
19
46
|
formatValue?: (value: number) => string
|
|
47
|
+
/** Label, output and +/- button scale. @default "md" */
|
|
48
|
+
size?: StepperSize
|
|
20
49
|
}
|
|
21
50
|
|
|
22
51
|
/** Discrete increments with disabled boundary actions and a spoken value. */
|
|
@@ -31,6 +60,7 @@ export function Stepper({
|
|
|
31
60
|
disabled = false,
|
|
32
61
|
className,
|
|
33
62
|
formatValue,
|
|
63
|
+
size = "md",
|
|
34
64
|
}: StepperProps) {
|
|
35
65
|
const id = useId()
|
|
36
66
|
const upper = Math.max(min, max)
|
|
@@ -43,15 +73,11 @@ export function Stepper({
|
|
|
43
73
|
|
|
44
74
|
return (
|
|
45
75
|
<div className={cn("flex min-w-0 items-center justify-between gap-3", className)}>
|
|
46
|
-
<span id={id} className=
|
|
76
|
+
<span id={id} className={labelText[size]}>
|
|
47
77
|
{label}
|
|
48
78
|
</span>
|
|
49
79
|
<div className="flex shrink-0 items-center gap-3">
|
|
50
|
-
<output
|
|
51
|
-
aria-live="polite"
|
|
52
|
-
aria-labelledby={id}
|
|
53
|
-
className="text-subheadline tabular-nums text-label"
|
|
54
|
-
>
|
|
80
|
+
<output aria-live="polite" aria-labelledby={id} className={outputText[size]}>
|
|
55
81
|
<span aria-hidden="true" className="inline-flex items-center">
|
|
56
82
|
{Array.from(displayed).map((character, index) => {
|
|
57
83
|
if (!/[0-9]/.test(character))
|
|
@@ -73,24 +99,24 @@ export function Stepper({
|
|
|
73
99
|
<Button
|
|
74
100
|
type="button"
|
|
75
101
|
variant="ghost"
|
|
76
|
-
size=
|
|
102
|
+
size={buttonSizes[size].size}
|
|
77
103
|
aria-label={labels.decrement}
|
|
78
104
|
aria-describedby={id}
|
|
79
105
|
disabled={disabled || decrement === current}
|
|
80
106
|
onClick={() => onChange(decrement)}
|
|
81
|
-
className=
|
|
107
|
+
className={buttonSizes[size].cls}
|
|
82
108
|
>
|
|
83
109
|
−
|
|
84
110
|
</Button>
|
|
85
111
|
<Button
|
|
86
112
|
type="button"
|
|
87
113
|
variant="ghost"
|
|
88
|
-
size=
|
|
114
|
+
size={buttonSizes[size].size}
|
|
89
115
|
aria-label={labels.increment}
|
|
90
116
|
aria-describedby={id}
|
|
91
117
|
disabled={disabled || increment === current}
|
|
92
118
|
onClick={() => onChange(increment)}
|
|
93
|
-
className=
|
|
119
|
+
className={buttonSizes[size].cls}
|
|
94
120
|
>
|
|
95
121
|
+
|
|
96
122
|
</Button>
|
|
@@ -2,6 +2,28 @@ import { useId, useRef, useState, type ReactNode } from "react"
|
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
|
|
4
4
|
export type TabViewAlign = "start" | "center"
|
|
5
|
+
export type TabViewSize = "sm" | "md" | "lg"
|
|
6
|
+
|
|
7
|
+
const containerGaps: Record<TabViewSize, string> = {
|
|
8
|
+
sm: "gap-4",
|
|
9
|
+
md: "gap-5",
|
|
10
|
+
lg: "gap-6",
|
|
11
|
+
}
|
|
12
|
+
const tablistClasses: Record<TabViewSize, string> = {
|
|
13
|
+
sm: "inline-flex h-7 w-fit max-w-full items-center gap-4 p-[2px]",
|
|
14
|
+
md: "inline-flex h-8 w-fit max-w-full items-center gap-5 p-[3px]",
|
|
15
|
+
lg: "inline-flex h-10 w-fit max-w-full items-center gap-6 p-[4px]",
|
|
16
|
+
}
|
|
17
|
+
const tabText: Record<TabViewSize, string> = {
|
|
18
|
+
sm: "text-footnote",
|
|
19
|
+
md: "text-subheadline",
|
|
20
|
+
lg: "text-body",
|
|
21
|
+
}
|
|
22
|
+
const underlineOffset: Record<TabViewSize, string> = {
|
|
23
|
+
sm: "after:bottom-[-4px] after:h-0.5",
|
|
24
|
+
md: "after:bottom-[-5px] after:h-0.5",
|
|
25
|
+
lg: "after:bottom-[-6px] after:h-1",
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
export interface TabViewProps<Value extends string> {
|
|
7
29
|
/** Available tabs, in keyboard navigation order. */
|
|
@@ -17,6 +39,8 @@ export interface TabViewProps<Value extends string> {
|
|
|
17
39
|
label: string
|
|
18
40
|
/** Renders the selected tab's panel. */
|
|
19
41
|
children: (value: Value) => ReactNode
|
|
42
|
+
/** Tablist height, tab text scale and underline offset. @default "md" */
|
|
43
|
+
size?: TabViewSize
|
|
20
44
|
className?: string
|
|
21
45
|
}
|
|
22
46
|
|
|
@@ -29,6 +53,7 @@ export function TabView<Value extends string>({
|
|
|
29
53
|
align = "center",
|
|
30
54
|
label,
|
|
31
55
|
children,
|
|
56
|
+
size = "md",
|
|
32
57
|
className,
|
|
33
58
|
}: TabViewProps<Value>) {
|
|
34
59
|
const id = useId()
|
|
@@ -44,12 +69,12 @@ export function TabView<Value extends string>({
|
|
|
44
69
|
if (value === undefined) return null
|
|
45
70
|
|
|
46
71
|
return (
|
|
47
|
-
<div className={cn("flex w-full flex-col
|
|
72
|
+
<div className={cn("flex w-full flex-col", containerGaps[size], className)}>
|
|
48
73
|
<div
|
|
49
74
|
role="tablist"
|
|
50
75
|
aria-label={label}
|
|
51
76
|
className={cn(
|
|
52
|
-
|
|
77
|
+
tablistClasses[size],
|
|
53
78
|
align === "start" ? "self-start" : "mx-auto justify-center"
|
|
54
79
|
)}
|
|
55
80
|
>
|
|
@@ -96,9 +121,11 @@ export function TabView<Value extends string>({
|
|
|
96
121
|
}
|
|
97
122
|
}}
|
|
98
123
|
className={cn(
|
|
99
|
-
"relative inline-flex h-[calc(100%-1px)] shrink-0 items-center justify-center border border-transparent p-0
|
|
124
|
+
"relative inline-flex h-[calc(100%-1px)] shrink-0 items-center justify-center border border-transparent p-0 font-medium whitespace-nowrap",
|
|
125
|
+
tabText[size],
|
|
100
126
|
"focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
101
|
-
"after:absolute after:inset-x-0 after:
|
|
127
|
+
"after:absolute after:inset-x-0 after:bg-current",
|
|
128
|
+
underlineOffset[size],
|
|
102
129
|
option.value === value
|
|
103
130
|
? "after:opacity-100"
|
|
104
131
|
: "opacity-60 hover:opacity-100 after:opacity-0"
|
|
@@ -13,6 +13,74 @@ import { haptic } from "../../lib/haptics"
|
|
|
13
13
|
import { spring } from "../../motion/transitions"
|
|
14
14
|
|
|
15
15
|
export type ToggleStyle = "switch" | "checkbox"
|
|
16
|
+
export type ToggleSize = "sm" | "md" | "lg"
|
|
17
|
+
|
|
18
|
+
const labelText: Record<ToggleSize, string> = {
|
|
19
|
+
sm: "text-footnote",
|
|
20
|
+
md: "text-subheadline",
|
|
21
|
+
lg: "text-body",
|
|
22
|
+
}
|
|
23
|
+
const labelGap: Record<ToggleSize, string> = {
|
|
24
|
+
sm: "gap-2",
|
|
25
|
+
md: "gap-3",
|
|
26
|
+
lg: "gap-3.5",
|
|
27
|
+
}
|
|
28
|
+
const checkboxBox: Record<ToggleSize, string> = {
|
|
29
|
+
sm: "size-4 rounded-sm",
|
|
30
|
+
md: "size-5 rounded-sm",
|
|
31
|
+
lg: "size-6 rounded-md",
|
|
32
|
+
}
|
|
33
|
+
const checkIconSize: Record<ToggleSize, number> = {
|
|
34
|
+
sm: 12,
|
|
35
|
+
md: 14,
|
|
36
|
+
lg: 16,
|
|
37
|
+
}
|
|
38
|
+
const mixedBar: Record<ToggleSize, string> = {
|
|
39
|
+
sm: "h-0.5 w-2",
|
|
40
|
+
md: "h-0.5 w-2.5",
|
|
41
|
+
lg: "h-0.5 w-3",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Switch physics: travel = track_width - thumb_width - 2*inset.
|
|
45
|
+
// md: 50 - 26 - 2 = 22 ✓ sm: 44 - 22 - 2 = 20 ✓ lg: 56 - 30 - 2 = 24 ✓
|
|
46
|
+
type SwitchGeo = {
|
|
47
|
+
travel: number
|
|
48
|
+
inset: number
|
|
49
|
+
dropOffset: number
|
|
50
|
+
track: string
|
|
51
|
+
thumb: string
|
|
52
|
+
drop: string
|
|
53
|
+
dropTop: string
|
|
54
|
+
}
|
|
55
|
+
const switchGeo: Record<ToggleSize, SwitchGeo> = {
|
|
56
|
+
sm: {
|
|
57
|
+
travel: 20,
|
|
58
|
+
inset: 2,
|
|
59
|
+
dropOffset: 4,
|
|
60
|
+
track: "h-[26px] w-[44px]",
|
|
61
|
+
thumb: "size-[22px]",
|
|
62
|
+
drop: "size-3.5",
|
|
63
|
+
dropTop: "top-[6px]",
|
|
64
|
+
},
|
|
65
|
+
md: {
|
|
66
|
+
travel: 22,
|
|
67
|
+
inset: 2,
|
|
68
|
+
dropOffset: 5,
|
|
69
|
+
track: "h-[30px] w-[50px]",
|
|
70
|
+
thumb: "size-[26px]",
|
|
71
|
+
drop: "size-4",
|
|
72
|
+
dropTop: "top-[7px]",
|
|
73
|
+
},
|
|
74
|
+
lg: {
|
|
75
|
+
travel: 24,
|
|
76
|
+
inset: 2,
|
|
77
|
+
dropOffset: 6,
|
|
78
|
+
track: "h-[34px] w-[56px]",
|
|
79
|
+
thumb: "size-[30px]",
|
|
80
|
+
drop: "size-5",
|
|
81
|
+
dropTop: "top-[8px]",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
16
84
|
|
|
17
85
|
export interface ToggleProps {
|
|
18
86
|
/** Current on or off state. */
|
|
@@ -28,6 +96,8 @@ export interface ToggleProps {
|
|
|
28
96
|
mixed?: boolean
|
|
29
97
|
/** Keeps the accessible name while hiding the visible label. @default false */
|
|
30
98
|
labelHidden?: boolean
|
|
99
|
+
/** Switch track size, thumb travel, checkbox box and label text scale. @default "md" */
|
|
100
|
+
size?: ToggleSize
|
|
31
101
|
/** Custom classes for the toggle container. */
|
|
32
102
|
className?: string
|
|
33
103
|
}
|
|
@@ -40,6 +110,7 @@ export function Toggle({
|
|
|
40
110
|
style = "switch",
|
|
41
111
|
mixed = false,
|
|
42
112
|
labelHidden = false,
|
|
113
|
+
size = "md",
|
|
43
114
|
className,
|
|
44
115
|
}: ToggleProps) {
|
|
45
116
|
const input = useRef<HTMLInputElement>(null)
|
|
@@ -50,12 +121,14 @@ export function Toggle({
|
|
|
50
121
|
return (
|
|
51
122
|
<label
|
|
52
123
|
className={cn(
|
|
53
|
-
"flex cursor-pointer items-center
|
|
124
|
+
"flex cursor-pointer items-center",
|
|
125
|
+
labelGap[size],
|
|
126
|
+
labelText[size],
|
|
54
127
|
disabled && "cursor-not-allowed opacity-40",
|
|
55
128
|
className
|
|
56
129
|
)}
|
|
57
130
|
>
|
|
58
|
-
<span className="relative inline-flex
|
|
131
|
+
<span className={cn("relative inline-flex shrink-0", checkboxBox[size])}>
|
|
59
132
|
<input
|
|
60
133
|
ref={input}
|
|
61
134
|
type="checkbox"
|
|
@@ -64,19 +137,23 @@ export function Toggle({
|
|
|
64
137
|
disabled={disabled}
|
|
65
138
|
onChange={(event) => onChange(event.target.checked)}
|
|
66
139
|
className={cn(
|
|
67
|
-
"peer
|
|
140
|
+
"peer cursor-inherit appearance-none border border-separator/80 bg-elevated transition-colors checked:border-transparent checked:bg-accent indeterminate:border-transparent indeterminate:bg-accent motion-reduce:transition-none",
|
|
141
|
+
checkboxBox[size],
|
|
68
142
|
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text"
|
|
69
143
|
)}
|
|
70
144
|
/>
|
|
71
145
|
{mixed ? (
|
|
72
146
|
<span
|
|
73
147
|
aria-hidden="true"
|
|
74
|
-
className=
|
|
148
|
+
className={cn(
|
|
149
|
+
"pointer-events-none absolute inset-0 m-auto rounded-full bg-on-accent",
|
|
150
|
+
mixedBar[size]
|
|
151
|
+
)}
|
|
75
152
|
/>
|
|
76
153
|
) : (
|
|
77
154
|
<CheckIcon
|
|
78
155
|
aria-hidden="true"
|
|
79
|
-
size={
|
|
156
|
+
size={checkIconSize[size]}
|
|
80
157
|
strokeWidth={2.5}
|
|
81
158
|
className="pointer-events-none absolute inset-0 m-auto text-on-accent opacity-0 peer-checked:opacity-100"
|
|
82
159
|
/>
|
|
@@ -92,6 +169,7 @@ export function Toggle({
|
|
|
92
169
|
onChange={onChange}
|
|
93
170
|
label={label}
|
|
94
171
|
disabled={disabled}
|
|
172
|
+
size={size}
|
|
95
173
|
className={className}
|
|
96
174
|
/>
|
|
97
175
|
)
|
|
@@ -102,30 +180,32 @@ function SwitchToggle({
|
|
|
102
180
|
onChange,
|
|
103
181
|
label,
|
|
104
182
|
disabled,
|
|
183
|
+
size = "md",
|
|
105
184
|
className,
|
|
106
|
-
}: Pick<ToggleProps, "checked" | "onChange" | "label" | "disabled" | "className">) {
|
|
185
|
+
}: Pick<ToggleProps, "checked" | "onChange" | "label" | "disabled" | "size" | "className">) {
|
|
107
186
|
const reducedMotion = useReducedMotion()
|
|
108
187
|
const id = `toggle-goo-${useId().replace(/:/g, "")}`
|
|
109
|
-
const
|
|
110
|
-
|
|
188
|
+
const geo = switchGeo[size]
|
|
189
|
+
const on = geo.travel
|
|
190
|
+
const off = geo.inset
|
|
191
|
+
const x = useMotionValue(checked ? on : off)
|
|
111
192
|
const chase = useSpring(x, spring.snappy)
|
|
112
|
-
const dropX = useTransform(chase, (value) => value +
|
|
193
|
+
const dropX = useTransform(chase, (value) => value + geo.dropOffset)
|
|
113
194
|
const gesture = useRef({ start: 0, x: 0, moved: false, skipClick: false, held: false })
|
|
114
195
|
useEffect(() => {
|
|
115
196
|
if (reducedMotion) {
|
|
116
|
-
x.set(checked ?
|
|
197
|
+
x.set(checked ? on : off)
|
|
117
198
|
return
|
|
118
199
|
}
|
|
119
|
-
const animation = animate(x, checked ?
|
|
200
|
+
const animation = animate(x, checked ? on : off, spring.snappy)
|
|
120
201
|
return () => animation.stop()
|
|
121
|
-
}, [checked, reducedMotion, x])
|
|
202
|
+
}, [checked, reducedMotion, x, on, off])
|
|
122
203
|
const choose = (next: boolean) => {
|
|
123
204
|
if (disabled) return
|
|
124
205
|
haptic("selection")
|
|
125
206
|
if (next !== checked) onChange(next)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
else animate(x, next ? 22 : 2, spring.snappy)
|
|
207
|
+
if (reducedMotion) x.set(next ? on : off)
|
|
208
|
+
else animate(x, next ? on : off, spring.snappy)
|
|
129
209
|
}
|
|
130
210
|
return (
|
|
131
211
|
<button
|
|
@@ -158,7 +238,7 @@ function SwitchToggle({
|
|
|
158
238
|
const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
|
|
159
239
|
const delta = (event.clientX - drag.start) * direction
|
|
160
240
|
if (Math.abs(delta) > 3) drag.moved = true
|
|
161
|
-
if (drag.moved) x.set(Math.max(
|
|
241
|
+
if (drag.moved) x.set(Math.max(off, Math.min(on, drag.x + delta)))
|
|
162
242
|
}}
|
|
163
243
|
onPointerUp={() => {
|
|
164
244
|
const drag = gesture.current
|
|
@@ -166,7 +246,7 @@ function SwitchToggle({
|
|
|
166
246
|
drag.held = false
|
|
167
247
|
if (!drag.moved) return
|
|
168
248
|
drag.skipClick = true
|
|
169
|
-
choose(x.get() >=
|
|
249
|
+
choose(x.get() >= (on + off) / 2)
|
|
170
250
|
}}
|
|
171
251
|
onPointerCancel={() => {
|
|
172
252
|
gesture.current.held = false
|
|
@@ -174,7 +254,8 @@ function SwitchToggle({
|
|
|
174
254
|
choose(checked)
|
|
175
255
|
}}
|
|
176
256
|
className={cn(
|
|
177
|
-
"relative
|
|
257
|
+
"relative shrink-0 touch-pan-y rounded-full transition-colors duration-normal motion-reduce:transition-none",
|
|
258
|
+
geo.track,
|
|
178
259
|
"shadow-[inset_0_1px_2px_rgb(0_0_0/0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:opacity-45",
|
|
179
260
|
checked ? "bg-accent" : "bg-separator/80",
|
|
180
261
|
className
|
|
@@ -206,12 +287,16 @@ function SwitchToggle({
|
|
|
206
287
|
>
|
|
207
288
|
{!reducedMotion && (
|
|
208
289
|
<motion.span
|
|
209
|
-
className=
|
|
290
|
+
className={cn(
|
|
291
|
+
"absolute left-0 rounded-full bg-white",
|
|
292
|
+
geo.dropTop,
|
|
293
|
+
geo.drop
|
|
294
|
+
)}
|
|
210
295
|
style={{ x: dropX }}
|
|
211
296
|
/>
|
|
212
297
|
)}
|
|
213
298
|
<motion.span
|
|
214
|
-
className="absolute top-0.5 left-0
|
|
299
|
+
className={cn("absolute top-0.5 left-0 rounded-full bg-white", geo.thumb)}
|
|
215
300
|
style={{ x }}
|
|
216
301
|
/>
|
|
217
302
|
</span>
|