@passui/react 0.1.44 → 0.1.46

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.
Files changed (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -398
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -322
@@ -1,305 +1,320 @@
1
- import { CheckIcon } from "../../icons/icons"
1
+ import { CheckIcon } from "../../icons/icons";
2
2
  import {
3
- animate,
4
- motion,
5
- useMotionValue,
6
- useSpring,
7
- useTransform,
8
- useReducedMotion,
9
- } from "motion/react"
10
- import { useEffect, useId, useRef } from "react"
11
- import { cn } from "../../lib/class-names"
12
- import { haptic } from "../../lib/haptics"
13
- import { spring } from "../../motion/transitions"
3
+ animate,
4
+ motion,
5
+ useMotionValue,
6
+ useSpring,
7
+ useTransform,
8
+ useReducedMotion,
9
+ } from "motion/react";
10
+ import { useEffect, useId, useRef } from "react";
11
+ import { cn } from "../../lib/class-names";
12
+ import { haptic } from "../../lib/haptics";
13
+ import { spring } from "../../motion/transitions";
14
14
 
15
- export type ToggleStyle = "switch" | "checkbox"
16
- export type ToggleSize = "sm" | "md" | "lg"
15
+ export type ToggleStyle = "switch" | "checkbox";
16
+ export type ToggleSize = "sm" | "md" | "lg";
17
17
 
18
18
  const labelText: Record<ToggleSize, string> = {
19
- sm: "text-footnote",
20
- md: "text-subheadline",
21
- lg: "text-body",
22
- }
19
+ sm: "text-footnote",
20
+ md: "text-subheadline",
21
+ lg: "text-body",
22
+ };
23
23
  const labelGap: Record<ToggleSize, string> = {
24
- sm: "gap-2",
25
- md: "gap-3",
26
- lg: "gap-3.5",
27
- }
24
+ sm: "gap-2",
25
+ md: "gap-3",
26
+ lg: "gap-3.5",
27
+ };
28
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
- }
29
+ sm: "size-4 rounded-sm",
30
+ md: "size-5 rounded-sm",
31
+ lg: "size-6 rounded-md",
32
+ };
33
33
  const checkIconSize: Record<ToggleSize, number> = {
34
- sm: 12,
35
- md: 14,
36
- lg: 16,
37
- }
34
+ sm: 12,
35
+ md: 14,
36
+ lg: 16,
37
+ };
38
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
- }
39
+ sm: "h-0.5 w-2",
40
+ md: "h-0.5 w-2.5",
41
+ lg: "h-0.5 w-3",
42
+ };
43
43
 
44
44
  // Switch physics: travel = track_width - thumb_width - 2*inset.
45
45
  // md: 50 - 26 - 2 = 22 ✓ sm: 44 - 22 - 2 = 20 ✓ lg: 56 - 30 - 2 = 24 ✓
46
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
- }
47
+ travel: number;
48
+ inset: number;
49
+ dropOffset: number;
50
+ track: string;
51
+ thumb: string;
52
+ drop: string;
53
+ dropTop: string;
54
+ };
55
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
- }
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
+ };
84
84
 
85
85
  export interface ToggleProps {
86
- /** Current on or off state. */
87
- checked: boolean
88
- /** Called with the next checked state. */
89
- onChange: (checked: boolean) => void
90
- /** Accessible name, including when the visible label is hidden. */
91
- label: string
92
- disabled?: boolean
93
- /** `switch` for a setting, `checkbox` for a selection. @default "switch" */
94
- style?: ToggleStyle
95
- /** Reports a mixed selection to assistive technology. @default false */
96
- mixed?: boolean
97
- /** Keeps the accessible name while hiding the visible label. @default false */
98
- labelHidden?: boolean
99
- /** Switch track size, thumb travel, checkbox box and label text scale. @default "md" */
100
- size?: ToggleSize
101
- /** Custom classes for the toggle container. */
102
- className?: string
86
+ /** Current on or off state. */
87
+ checked: boolean;
88
+ /** Called with the next checked state. */
89
+ onChange: (checked: boolean) => void;
90
+ /** Accessible name, including when the visible label is hidden. */
91
+ label: string;
92
+ disabled?: boolean;
93
+ /** `switch` for a setting, `checkbox` for a selection. @default "switch" */
94
+ style?: ToggleStyle;
95
+ /** Reports a mixed selection to assistive technology. @default false */
96
+ mixed?: boolean;
97
+ /** Keeps the accessible name while hiding the visible label. @default false */
98
+ labelHidden?: boolean;
99
+ /** Switch track size, thumb travel, checkbox box and label text scale. @default "md" */
100
+ size?: ToggleSize;
101
+ /** Custom classes for the toggle container. */
102
+ className?: string;
103
103
  }
104
104
 
105
105
  export function Toggle({
106
- checked,
107
- onChange,
108
- label,
109
- disabled,
110
- style = "switch",
111
- mixed = false,
112
- labelHidden = false,
113
- size = "md",
114
- className,
106
+ checked,
107
+ onChange,
108
+ label,
109
+ disabled,
110
+ style = "switch",
111
+ mixed = false,
112
+ labelHidden = false,
113
+ size = "md",
114
+ className,
115
115
  }: ToggleProps) {
116
- const input = useRef<HTMLInputElement>(null)
117
- useEffect(() => {
118
- if (input.current) input.current.indeterminate = mixed
119
- }, [mixed])
120
- if (style === "checkbox") {
121
- return (
122
- <label
123
- className={cn(
124
- "flex cursor-pointer items-center",
125
- labelGap[size],
126
- labelText[size],
127
- disabled && "cursor-not-allowed opacity-40",
128
- className
129
- )}
130
- >
131
- <span className={cn("relative inline-flex shrink-0", checkboxBox[size])}>
132
- <input
133
- ref={input}
134
- type="checkbox"
135
- aria-checked={mixed ? "mixed" : checked}
136
- checked={checked}
137
- disabled={disabled}
138
- onChange={(event) => onChange(event.target.checked)}
139
- className={cn(
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],
142
- "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text"
143
- )}
144
- />
145
- {mixed ? (
146
- <span
147
- aria-hidden="true"
148
- className={cn(
149
- "pointer-events-none absolute inset-0 m-auto rounded-full bg-on-accent",
150
- mixedBar[size]
151
- )}
152
- />
153
- ) : (
154
- <CheckIcon
155
- aria-hidden="true"
156
- size={checkIconSize[size]}
157
- strokeWidth={2.5}
158
- className="pointer-events-none absolute inset-0 m-auto text-on-accent opacity-0 peer-checked:opacity-100"
159
- />
160
- )}
161
- </span>
162
- <span className={labelHidden ? "sr-only" : undefined}>{label}</span>
163
- </label>
164
- )
165
- }
166
- return (
167
- <SwitchToggle
168
- checked={checked}
169
- onChange={onChange}
170
- label={label}
171
- disabled={disabled}
172
- size={size}
173
- className={className}
174
- />
175
- )
116
+ const input = useRef<HTMLInputElement>(null);
117
+ useEffect(() => {
118
+ if (input.current) input.current.indeterminate = mixed;
119
+ }, [mixed]);
120
+ if (style === "checkbox") {
121
+ return (
122
+ <label
123
+ className={cn(
124
+ "flex cursor-pointer items-center",
125
+ labelGap[size],
126
+ labelText[size],
127
+ disabled && "cursor-not-allowed opacity-40",
128
+ className,
129
+ )}
130
+ >
131
+ <span
132
+ className={cn("relative inline-flex shrink-0", checkboxBox[size])}
133
+ >
134
+ <input
135
+ ref={input}
136
+ type="checkbox"
137
+ aria-checked={mixed ? "mixed" : checked}
138
+ checked={checked}
139
+ disabled={disabled}
140
+ onChange={(event) => onChange(event.target.checked)}
141
+ className={cn(
142
+ "peer cursor-inherit border-separator/80 bg-elevated checked:bg-accent indeterminate:bg-accent appearance-none border transition-colors checked:border-transparent indeterminate:border-transparent motion-reduce:transition-none",
143
+ checkboxBox[size],
144
+ "focus-visible:outline-accent-text focus-visible:outline-2 focus-visible:outline-offset-2",
145
+ )}
146
+ />
147
+ {mixed ? (
148
+ <span
149
+ aria-hidden="true"
150
+ className={cn(
151
+ "bg-on-accent pointer-events-none absolute inset-0 m-auto rounded-full",
152
+ mixedBar[size],
153
+ )}
154
+ />
155
+ ) : (
156
+ <CheckIcon
157
+ aria-hidden="true"
158
+ size={checkIconSize[size]}
159
+ strokeWidth={2.5}
160
+ className="text-on-accent pointer-events-none absolute inset-0 m-auto opacity-0 peer-checked:opacity-100"
161
+ />
162
+ )}
163
+ </span>
164
+ <span className={labelHidden ? "sr-only" : undefined}>{label}</span>
165
+ </label>
166
+ );
167
+ }
168
+ return (
169
+ <SwitchToggle
170
+ checked={checked}
171
+ onChange={onChange}
172
+ label={label}
173
+ disabled={disabled}
174
+ size={size}
175
+ className={className}
176
+ />
177
+ );
176
178
  }
177
179
 
178
180
  function SwitchToggle({
179
- checked,
180
- onChange,
181
- label,
182
- disabled,
183
- size = "md",
184
- className,
185
- }: Pick<ToggleProps, "checked" | "onChange" | "label" | "disabled" | "size" | "className">) {
186
- const reducedMotion = useReducedMotion()
187
- const id = `toggle-goo-${useId().replace(/:/g, "")}`
188
- const geo = switchGeo[size]
189
- const on = geo.travel
190
- const off = geo.inset
191
- const x = useMotionValue(checked ? on : off)
192
- const chase = useSpring(x, spring.snappy)
193
- const dropX = useTransform(chase, (value) => value + geo.dropOffset)
194
- const gesture = useRef({ start: 0, x: 0, moved: false, skipClick: false, held: false })
195
- useEffect(() => {
196
- if (reducedMotion) {
197
- x.set(checked ? on : off)
198
- return
199
- }
200
- const animation = animate(x, checked ? on : off, spring.snappy)
201
- return () => animation.stop()
202
- }, [checked, reducedMotion, x, on, off])
203
- const choose = (next: boolean) => {
204
- if (disabled) return
205
- haptic("selection")
206
- if (next !== checked) onChange(next)
207
- if (reducedMotion) x.set(next ? on : off)
208
- else animate(x, next ? on : off, spring.snappy)
209
- }
210
- return (
211
- <button
212
- type="button"
213
- role="switch"
214
- aria-checked={checked}
215
- aria-label={label}
216
- disabled={disabled}
217
- onClick={() => {
218
- if (gesture.current.skipClick) {
219
- gesture.current.skipClick = false
220
- return
221
- }
222
- choose(!checked)
223
- }}
224
- onPointerDown={(event) => {
225
- if (event.button !== 0 || disabled) return
226
- event.currentTarget.setPointerCapture(event.pointerId)
227
- gesture.current = {
228
- start: event.clientX,
229
- x: x.get(),
230
- moved: false,
231
- skipClick: false,
232
- held: true,
233
- }
234
- }}
235
- onPointerMove={(event) => {
236
- const drag = gesture.current
237
- if (!drag.held || disabled) return
238
- const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
239
- const delta = (event.clientX - drag.start) * direction
240
- if (Math.abs(delta) > 3) drag.moved = true
241
- if (drag.moved) x.set(Math.max(off, Math.min(on, drag.x + delta)))
242
- }}
243
- onPointerUp={() => {
244
- const drag = gesture.current
245
- if (!drag.held) return
246
- drag.held = false
247
- if (!drag.moved) return
248
- drag.skipClick = true
249
- choose(x.get() >= (on + off) / 2)
250
- }}
251
- onPointerCancel={() => {
252
- gesture.current.held = false
253
- gesture.current.skipClick = false
254
- choose(checked)
255
- }}
256
- className={cn(
257
- "relative shrink-0 touch-pan-y rounded-full transition-colors duration-normal motion-reduce:transition-none",
258
- geo.track,
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",
260
- checked ? "bg-accent" : "bg-separator/80",
261
- className
262
- )}
263
- >
264
- <svg aria-hidden="true" width="0" height="0" className="absolute">
265
- <defs>
266
- <filter
267
- id={id}
268
- x="-50%"
269
- y="-100%"
270
- width="200%"
271
- height="300%"
272
- colorInterpolationFilters="sRGB"
273
- >
274
- <feGaussianBlur stdDeviation="2.5" result="smear" />
275
- <feColorMatrix
276
- in="smear"
277
- type="matrix"
278
- values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
279
- />
280
- </filter>
281
- </defs>
282
- </svg>
283
- <span
284
- aria-hidden="true"
285
- className="pointer-events-none absolute inset-0 rtl:-scale-x-100"
286
- style={{ filter: reducedMotion ? undefined : `url(#${id})` }}
287
- >
288
- {!reducedMotion && (
289
- <motion.span
290
- className={cn(
291
- "absolute left-0 rounded-full bg-white",
292
- geo.dropTop,
293
- geo.drop
294
- )}
295
- style={{ x: dropX }}
296
- />
297
- )}
298
- <motion.span
299
- className={cn("absolute top-0.5 left-0 rounded-full bg-white", geo.thumb)}
300
- style={{ x }}
301
- />
302
- </span>
303
- </button>
304
- )
181
+ checked,
182
+ onChange,
183
+ label,
184
+ disabled,
185
+ size = "md",
186
+ className,
187
+ }: Pick<
188
+ ToggleProps,
189
+ "checked" | "onChange" | "label" | "disabled" | "size" | "className"
190
+ >) {
191
+ const reducedMotion = useReducedMotion();
192
+ const id = `toggle-goo-${useId().replace(/:/g, "")}`;
193
+ const geo = switchGeo[size];
194
+ const on = geo.travel;
195
+ const off = geo.inset;
196
+ const x = useMotionValue(checked ? on : off);
197
+ const chase = useSpring(x, spring.snappy);
198
+ const dropX = useTransform(chase, (value) => value + geo.dropOffset);
199
+ const gesture = useRef({
200
+ start: 0,
201
+ x: 0,
202
+ moved: false,
203
+ skipClick: false,
204
+ held: false,
205
+ });
206
+ useEffect(() => {
207
+ if (reducedMotion) {
208
+ x.set(checked ? on : off);
209
+ return;
210
+ }
211
+ const animation = animate(x, checked ? on : off, spring.snappy);
212
+ return () => animation.stop();
213
+ }, [checked, reducedMotion, x, on, off]);
214
+ const choose = (next: boolean) => {
215
+ if (disabled) return;
216
+ haptic("selection");
217
+ if (next !== checked) onChange(next);
218
+ if (reducedMotion) x.set(next ? on : off);
219
+ else animate(x, next ? on : off, spring.snappy);
220
+ };
221
+ return (
222
+ <button
223
+ type="button"
224
+ role="switch"
225
+ aria-checked={checked}
226
+ aria-label={label}
227
+ disabled={disabled}
228
+ onClick={() => {
229
+ if (gesture.current.skipClick) {
230
+ gesture.current.skipClick = false;
231
+ return;
232
+ }
233
+ choose(!checked);
234
+ }}
235
+ onPointerDown={(event) => {
236
+ if (event.button !== 0 || disabled) return;
237
+ event.currentTarget.setPointerCapture(event.pointerId);
238
+ gesture.current = {
239
+ start: event.clientX,
240
+ x: x.get(),
241
+ moved: false,
242
+ skipClick: false,
243
+ held: true,
244
+ };
245
+ }}
246
+ onPointerMove={(event) => {
247
+ const drag = gesture.current;
248
+ if (!drag.held || disabled) return;
249
+ const direction =
250
+ getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
251
+ const delta = (event.clientX - drag.start) * direction;
252
+ if (Math.abs(delta) > 3) drag.moved = true;
253
+ if (drag.moved) x.set(Math.max(off, Math.min(on, drag.x + delta)));
254
+ }}
255
+ onPointerUp={() => {
256
+ const drag = gesture.current;
257
+ if (!drag.held) return;
258
+ drag.held = false;
259
+ if (!drag.moved) return;
260
+ drag.skipClick = true;
261
+ choose(x.get() >= (on + off) / 2);
262
+ }}
263
+ onPointerCancel={() => {
264
+ gesture.current.held = false;
265
+ gesture.current.skipClick = false;
266
+ choose(checked);
267
+ }}
268
+ className={cn(
269
+ "duration-normal relative shrink-0 touch-pan-y rounded-full transition-colors motion-reduce:transition-none",
270
+ geo.track,
271
+ "focus-visible:ring-accent/70 focus-visible:ring-offset-background shadow-[inset_0_1px_2px_rgb(0_0_0/0.10)] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none disabled:opacity-45",
272
+ checked ? "bg-accent" : "bg-separator/80",
273
+ className,
274
+ )}
275
+ >
276
+ <svg aria-hidden="true" width="0" height="0" className="absolute">
277
+ <defs>
278
+ <filter
279
+ id={id}
280
+ x="-50%"
281
+ y="-100%"
282
+ width="200%"
283
+ height="300%"
284
+ colorInterpolationFilters="sRGB"
285
+ >
286
+ <feGaussianBlur stdDeviation="2.5" result="smear" />
287
+ <feColorMatrix
288
+ in="smear"
289
+ type="matrix"
290
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
291
+ />
292
+ </filter>
293
+ </defs>
294
+ </svg>
295
+ <span
296
+ aria-hidden="true"
297
+ className="pointer-events-none absolute inset-0 rtl:-scale-x-100"
298
+ style={{ filter: reducedMotion ? undefined : `url(#${id})` }}
299
+ >
300
+ {!reducedMotion && (
301
+ <motion.span
302
+ className={cn(
303
+ "absolute left-0 rounded-full bg-white",
304
+ geo.dropTop,
305
+ geo.drop,
306
+ )}
307
+ style={{ x: dropX }}
308
+ />
309
+ )}
310
+ <motion.span
311
+ className={cn(
312
+ "absolute top-0.5 left-0 rounded-full bg-white",
313
+ geo.thumb,
314
+ )}
315
+ style={{ x }}
316
+ />
317
+ </span>
318
+ </button>
319
+ );
305
320
  }