@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
|
@@ -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>
|
|
@@ -14,6 +14,34 @@ export interface SwatchPickerOption<T extends string> {
|
|
|
14
14
|
ink: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type SwatchPickerSize = "sm" | "md" | "lg"
|
|
18
|
+
|
|
19
|
+
const swatchSizes: Record<SwatchPickerSize, string> = {
|
|
20
|
+
sm: "size-6",
|
|
21
|
+
md: "size-8",
|
|
22
|
+
lg: "size-10",
|
|
23
|
+
}
|
|
24
|
+
const checkSizes: Record<SwatchPickerSize, number> = {
|
|
25
|
+
sm: 13,
|
|
26
|
+
md: 16,
|
|
27
|
+
lg: 19,
|
|
28
|
+
}
|
|
29
|
+
const captionText: Record<SwatchPickerSize, string> = {
|
|
30
|
+
sm: "text-caption2",
|
|
31
|
+
md: "text-caption2",
|
|
32
|
+
lg: "text-caption",
|
|
33
|
+
}
|
|
34
|
+
const fieldsetGaps: Record<SwatchPickerSize, string> = {
|
|
35
|
+
sm: "gap-2",
|
|
36
|
+
md: "gap-3",
|
|
37
|
+
lg: "gap-4",
|
|
38
|
+
}
|
|
39
|
+
const labelGaps: Record<SwatchPickerSize, string> = {
|
|
40
|
+
sm: "gap-1",
|
|
41
|
+
md: "gap-1.5",
|
|
42
|
+
lg: "gap-2",
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
export interface SwatchPickerProps<T extends string> {
|
|
18
46
|
value: T
|
|
19
47
|
options: ReadonlyArray<SwatchPickerOption<T>>
|
|
@@ -22,6 +50,8 @@ export interface SwatchPickerProps<T extends string> {
|
|
|
22
50
|
label: string
|
|
23
51
|
/** Custom classes for the swatch container. */
|
|
24
52
|
className?: string
|
|
53
|
+
/** Swatch diameter, check icon and caption scale. @default "md" */
|
|
54
|
+
size?: SwatchPickerSize
|
|
25
55
|
}
|
|
26
56
|
|
|
27
57
|
/** Colour swatches with a check on the chosen one. */
|
|
@@ -31,17 +61,21 @@ export function SwatchPicker<T extends string>({
|
|
|
31
61
|
onChange,
|
|
32
62
|
label,
|
|
33
63
|
className,
|
|
64
|
+
size = "md",
|
|
34
65
|
}: SwatchPickerProps<T>) {
|
|
35
66
|
const name = useId()
|
|
36
67
|
return (
|
|
37
|
-
<fieldset className={cn("flex flex-wrap
|
|
68
|
+
<fieldset className={cn("flex flex-wrap", fieldsetGaps[size], className)}>
|
|
38
69
|
<legend className="sr-only">{label}</legend>
|
|
39
70
|
{options.map((option) => {
|
|
40
71
|
const selected = option.value === value
|
|
41
72
|
return (
|
|
42
73
|
<label
|
|
43
74
|
key={option.value}
|
|
44
|
-
className=
|
|
75
|
+
className={cn(
|
|
76
|
+
"group flex cursor-pointer flex-col items-center",
|
|
77
|
+
labelGaps[size]
|
|
78
|
+
)}
|
|
45
79
|
>
|
|
46
80
|
<input
|
|
47
81
|
type="radio"
|
|
@@ -56,7 +90,8 @@ export function SwatchPicker<T extends string>({
|
|
|
56
90
|
/>
|
|
57
91
|
<span
|
|
58
92
|
className={cn(
|
|
59
|
-
"flex
|
|
93
|
+
"flex items-center justify-center rounded-full ring-offset-1 ring-offset-elevated transition-[box-shadow,transform] duration-fast group-active:scale-95 peer-focus-visible:ring-2 peer-focus-visible:ring-accent",
|
|
94
|
+
swatchSizes[size],
|
|
60
95
|
selected
|
|
61
96
|
? "ring-[1.5px] ring-label/70"
|
|
62
97
|
: "ring-0 group-hover:ring-[1.5px] group-hover:ring-separator"
|
|
@@ -78,7 +113,7 @@ export function SwatchPicker<T extends string>({
|
|
|
78
113
|
transition={spring.bouncy}
|
|
79
114
|
>
|
|
80
115
|
<CheckIcon
|
|
81
|
-
size={
|
|
116
|
+
size={checkSizes[size]}
|
|
82
117
|
strokeWidth={2.6}
|
|
83
118
|
style={{
|
|
84
119
|
color: option.ink,
|
|
@@ -89,7 +124,7 @@ export function SwatchPicker<T extends string>({
|
|
|
89
124
|
</span>
|
|
90
125
|
<span
|
|
91
126
|
className={cn(
|
|
92
|
-
|
|
127
|
+
captionText[size],
|
|
93
128
|
selected ? "font-semibold text-label" : "text-label-tertiary"
|
|
94
129
|
)}
|
|
95
130
|
>
|
|
@@ -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>
|