@passui/react 0.1.45 → 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 -402
  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 -334
@@ -1,53 +1,60 @@
1
- import { AnimatePresence, motion, useReducedMotion } from "motion/react"
2
- import { useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react"
3
- import { CheckIcon, CloseIcon } from "../../icons/icons"
4
- import { cn } from "../../lib/class-names"
5
- import { spring } from "../../motion/transitions"
1
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
6
2
  import {
7
- getToasts,
8
- subscribeToasts,
9
- type Toast,
10
- type ToastSize,
11
- type ToastTone,
12
- toast as toastApi,
13
- toastLimits,
14
- } from "./toast-store"
3
+ useEffect,
4
+ useId,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from "react";
10
+ import { CheckIcon, CloseIcon } from "../../icons/icons";
11
+ import { cn } from "../../lib/class-names";
12
+ import { spring } from "../../motion/transitions";
13
+ import {
14
+ getToasts,
15
+ subscribeToasts,
16
+ type Toast,
17
+ type ToastSize,
18
+ type ToastTone,
19
+ toast as toastApi,
20
+ toastLimits,
21
+ } from "./toast-store";
15
22
 
16
- const PILL: Record<ToastSize, number> = { sm: 32, md: 40, lg: 48 }
23
+ const PILL: Record<ToastSize, number> = { sm: 32, md: 40, lg: 48 };
17
24
 
18
25
  const contentClasses: Record<ToastSize, string> = {
19
- sm: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2 py-2 pr-3 pl-2 text-on-inverse",
20
- md: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2.5 py-[10px] pr-4 pl-[10px] text-on-inverse",
21
- lg: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-3 py-3 pr-5 pl-3 text-on-inverse",
22
- }
26
+ sm: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2 py-2 pr-3 pl-2 text-on-inverse",
27
+ md: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2.5 py-[10px] pr-4 pl-[10px] text-on-inverse",
28
+ lg: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-3 py-3 pr-5 pl-3 text-on-inverse",
29
+ };
23
30
  const titleClasses: Record<ToastSize, string> = {
24
- sm: "text-footnote leading-4 font-semibold",
25
- md: "text-subheadline leading-5 font-semibold",
26
- lg: "text-body leading-6 font-semibold",
27
- }
31
+ sm: "text-footnote leading-4 font-semibold",
32
+ md: "text-subheadline leading-5 font-semibold",
33
+ lg: "text-body leading-6 font-semibold",
34
+ };
28
35
  const descriptionClasses: Record<ToastSize, string> = {
29
- sm: "text-caption2 leading-snug text-on-inverse/70",
30
- md: "text-footnote leading-snug text-on-inverse/70",
31
- lg: "text-subheadline leading-snug text-on-inverse/70",
32
- }
36
+ sm: "text-caption2 leading-snug text-on-inverse/70",
37
+ md: "text-footnote leading-snug text-on-inverse/70",
38
+ lg: "text-subheadline leading-snug text-on-inverse/70",
39
+ };
33
40
  const actionClasses: Record<ToastSize, string> = {
34
- sm: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-2.5 py-0.5 text-caption2 font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
35
- md: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3 py-1 text-footnote font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
36
- lg: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3.5 py-1.5 text-subheadline font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
37
- }
41
+ sm: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-2.5 py-0.5 text-caption2 font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
42
+ md: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3 py-1 text-footnote font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
43
+ lg: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3.5 py-1.5 text-subheadline font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
44
+ };
38
45
  const toneIconClasses: Record<ToastSize, string> = {
39
- sm: "flex size-4 shrink-0 items-center justify-center rounded-full",
40
- md: "flex size-5 shrink-0 items-center justify-center rounded-full",
41
- lg: "flex size-6 shrink-0 items-center justify-center rounded-full",
42
- }
43
- const EMPTY: Toast[] = []
46
+ sm: "flex size-4 shrink-0 items-center justify-center rounded-full",
47
+ md: "flex size-5 shrink-0 items-center justify-center rounded-full",
48
+ lg: "flex size-6 shrink-0 items-center justify-center rounded-full",
49
+ };
50
+ const EMPTY: Toast[] = [];
44
51
  /** A little loose, so the body overshoots and settles like a drop of honey. */
45
52
  const BODY_SPRING = {
46
- type: "spring",
47
- stiffness: 260,
48
- damping: 19,
49
- mass: 0.9,
50
- } as const
53
+ type: "spring",
54
+ stiffness: 260,
55
+ damping: 19,
56
+ mass: 0.9,
57
+ } as const;
51
58
 
52
59
  /**
53
60
  * Toasts drop from the top centre like an island: a small pill carrying the
@@ -56,338 +63,345 @@ const BODY_SPRING = {
56
63
  * states and shrink away. Text is drawn above the filter to stay crisp.
57
64
  */
58
65
  export function Toaster({
59
- className,
60
- label = "Notifications",
66
+ className,
67
+ label = "Notifications",
61
68
  }: {
62
- className?: string
63
- label?: string
69
+ className?: string;
70
+ label?: string;
64
71
  }) {
65
- const toasts = useSyncExternalStore(subscribeToasts, getToasts, () => EMPTY)
66
- const filterId = useId()
67
- const reducedMotion = useReducedMotion()
68
- const [hovered, setHovered] = useState(false)
69
- const visible = toasts.slice(0, toastLimits.visible)
72
+ const toasts = useSyncExternalStore(subscribeToasts, getToasts, () => EMPTY);
73
+ const filterId = useId();
74
+ const reducedMotion = useReducedMotion();
75
+ const [hovered, setHovered] = useState(false);
76
+ const visible = toasts.slice(0, toastLimits.visible);
70
77
 
71
- return (
72
- <>
73
- <GooFilter id={filterId} />
74
- <section
75
- aria-label={label}
76
- aria-live="polite"
77
- className={cn(
78
- "pointer-events-none fixed inset-x-0 top-[max(10px,env(safe-area-inset-top))] z-[60] flex justify-center px-3",
79
- className
80
- )}
81
- >
82
- <ol
83
- className="relative flex w-full max-w-[420px] flex-col items-center"
84
- onMouseEnter={() => setHovered(true)}
85
- onMouseLeave={() => setHovered(false)}
86
- >
87
- <AnimatePresence initial={false}>
88
- {visible.map((item, index) => (
89
- <ToastItem
90
- key={item.id}
91
- toast={item}
92
- index={index}
93
- expanded={hovered}
94
- paused={hovered}
95
- filterId={filterId}
96
- reducedMotion={Boolean(reducedMotion)}
97
- />
98
- ))}
99
- </AnimatePresence>
100
- </ol>
101
- </section>
102
- </>
103
- )
78
+ return (
79
+ <>
80
+ <GooFilter id={filterId} />
81
+ <section
82
+ aria-label={label}
83
+ aria-live="polite"
84
+ className={cn(
85
+ "pointer-events-none fixed inset-x-0 top-[max(10px,env(safe-area-inset-top))] z-[60] flex justify-center px-3",
86
+ className,
87
+ )}
88
+ >
89
+ <ol
90
+ className="relative flex w-full max-w-[420px] flex-col items-center"
91
+ onMouseEnter={() => setHovered(true)}
92
+ onMouseLeave={() => setHovered(false)}
93
+ >
94
+ <AnimatePresence initial={false}>
95
+ {visible.map((item, index) => (
96
+ <ToastItem
97
+ key={item.id}
98
+ toast={item}
99
+ index={index}
100
+ expanded={hovered}
101
+ paused={hovered}
102
+ filterId={filterId}
103
+ reducedMotion={Boolean(reducedMotion)}
104
+ />
105
+ ))}
106
+ </AnimatePresence>
107
+ </ol>
108
+ </section>
109
+ </>
110
+ );
104
111
  }
105
112
 
106
113
  function GooFilter({ id }: { id: string }) {
107
- return (
108
- <svg aria-hidden="true" className="pointer-events-none absolute size-0">
109
- <defs>
110
- <filter id={id}>
111
- <feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur" />
112
- <feColorMatrix
113
- in="blur"
114
- mode="matrix"
115
- values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -10"
116
- result="goo"
117
- />
118
- <feComposite in="SourceGraphic" in2="goo" operator="atop" />
119
- </filter>
120
- </defs>
121
- </svg>
122
- )
114
+ return (
115
+ <svg aria-hidden="true" className="pointer-events-none absolute size-0">
116
+ <defs>
117
+ <filter id={id}>
118
+ <feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur" />
119
+ <feColorMatrix
120
+ in="blur"
121
+ mode="matrix"
122
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -10"
123
+ result="goo"
124
+ />
125
+ <feComposite in="SourceGraphic" in2="goo" operator="atop" />
126
+ </filter>
127
+ </defs>
128
+ </svg>
129
+ );
123
130
  }
124
131
 
125
132
  function ToastItem({
126
- toast,
127
- index,
128
- expanded,
129
- paused,
130
- filterId,
131
- reducedMotion,
133
+ toast,
134
+ index,
135
+ expanded,
136
+ paused,
137
+ filterId,
138
+ reducedMotion,
132
139
  }: {
133
- toast: Toast
134
- index: number
135
- expanded: boolean
136
- paused: boolean
137
- filterId: string
138
- reducedMotion: boolean
140
+ toast: Toast;
141
+ index: number;
142
+ expanded: boolean;
143
+ paused: boolean;
144
+ filterId: string;
145
+ reducedMotion: boolean;
139
146
  }) {
140
- const pill = PILL[toast.size]
141
- const content = useRef<HTMLDivElement>(null)
142
- const [size, setSize] = useState({
143
- width: pill,
144
- height: pill,
145
- })
146
- const [open, setOpen] = useState(false)
147
+ const pill = PILL[toast.size];
148
+ const content = useRef<HTMLDivElement>(null);
149
+ const [size, setSize] = useState({
150
+ width: pill,
151
+ height: pill,
152
+ });
153
+ const [open, setOpen] = useState(false);
147
154
 
148
- // The body flows out of the pill a beat after it lands.
149
- useEffect(() => {
150
- const timer = setTimeout(() => setOpen(true), reducedMotion ? 0 : 140)
151
- return () => clearTimeout(timer)
152
- }, [reducedMotion])
155
+ // The body flows out of the pill a beat after it lands.
156
+ useEffect(() => {
157
+ const timer = setTimeout(() => setOpen(true), reducedMotion ? 0 : 140);
158
+ return () => clearTimeout(timer);
159
+ }, [reducedMotion]);
153
160
 
154
- useLayoutEffect(() => {
155
- const element = content.current
156
- if (!element) return
157
- const measure = () =>
158
- setSize({
159
- width: Math.ceil(element.offsetWidth),
160
- height: Math.ceil(element.offsetHeight),
161
- })
162
- measure()
163
- const observer = new ResizeObserver(measure)
164
- observer.observe(element)
165
- return () => observer.disconnect()
166
- }, [])
161
+ useLayoutEffect(() => {
162
+ const element = content.current;
163
+ if (!element) return;
164
+ const measure = () =>
165
+ setSize({
166
+ width: Math.ceil(element.offsetWidth),
167
+ height: Math.ceil(element.offsetHeight),
168
+ });
169
+ measure();
170
+ const observer = new ResizeObserver(measure);
171
+ observer.observe(element);
172
+ return () => observer.disconnect();
173
+ }, []);
167
174
 
168
- useEffect(() => {
169
- if (toast.duration === null || paused) return
170
- const timer = setTimeout(() => toastApi.dismiss(toast.id), toast.duration)
171
- return () => clearTimeout(timer)
172
- }, [toast.id, toast.duration, paused])
175
+ useEffect(() => {
176
+ if (toast.duration === null || paused) return;
177
+ const timer = setTimeout(() => toastApi.dismiss(toast.id), toast.duration);
178
+ return () => clearTimeout(timer);
179
+ }, [toast.id, toast.duration, paused]);
173
180
 
174
- const width = open ? size.width : pill
175
- const height = open ? size.height : pill
176
- // Older toasts tuck in behind the newest, and fan out on hover.
177
- const stacked = !expanded && index > 0
181
+ const width = open ? size.width : pill;
182
+ const height = open ? size.height : pill;
183
+ // Older toasts tuck in behind the newest, and fan out on hover.
184
+ const stacked = !expanded && index > 0;
178
185
 
179
- return (
180
- <motion.li
181
- layout={!reducedMotion}
182
- className="pointer-events-none absolute inset-x-0 top-0 flex justify-center"
183
- style={{ zIndex: 10 - index }}
184
- initial={reducedMotion ? false : { opacity: 0, y: -24, scale: 0.6 }}
185
- animate={{
186
- opacity: stacked ? 1 - index * 0.25 : 1,
187
- y: stacked ? index * 9 : index * (pill + 14) + index * 6,
188
- scale: stacked ? 1 - index * 0.05 : 1,
189
- }}
190
- exit={
191
- reducedMotion
192
- ? { opacity: 0 }
193
- : {
194
- opacity: 0,
195
- y: -18,
196
- scale: 0.6,
197
- transition: spring.snappy,
198
- }
199
- }
200
- transition={reducedMotion ? { duration: 0 } : spring.bouncy}
201
- drag={reducedMotion ? false : "y"}
202
- dragConstraints={{ top: 0, bottom: 0 }}
203
- dragElastic={{ top: 0.6, bottom: 0.1 }}
204
- onDragEnd={(_, info) => {
205
- if (info.offset.y < -30 || info.velocity.y < -300) toastApi.dismiss(toast.id)
206
- }}
207
- >
208
- <motion.div
209
- role="status"
210
- className="pointer-events-auto relative"
211
- initial={{ width: pill, height: pill }}
212
- animate={{ width, height }}
213
- transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
214
- >
215
- {/* Shapes, under the goo filter; padded so the blur isn't clipped. */}
216
- <div
217
- aria-hidden="true"
218
- className="absolute -inset-3"
219
- style={{
220
- filter: reducedMotion ? undefined : `url(#${filterId})`,
221
- }}
222
- >
223
- <motion.span
224
- className="absolute top-3 left-3 rounded-full bg-inverse shadow-xl"
225
- style={{ width: pill, height: pill }}
226
- animate={{ scale: open ? 1 : 1.06 }}
227
- transition={reducedMotion ? { duration: 0 } : spring.bouncy}
228
- />
229
- <motion.span
230
- className="absolute top-3 left-3 bg-inverse"
231
- initial={{
232
- width: pill,
233
- height: pill,
234
- borderRadius: pill / 2,
235
- }}
236
- animate={{
237
- width,
238
- height,
239
- borderRadius: Math.min(pill / 2, height / 2, 22),
240
- }}
241
- transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
242
- />
243
- </div>
186
+ return (
187
+ <motion.li
188
+ layout={!reducedMotion}
189
+ className="pointer-events-none absolute inset-x-0 top-0 flex justify-center"
190
+ style={{ zIndex: 10 - index }}
191
+ initial={reducedMotion ? false : { opacity: 0, y: -24, scale: 0.6 }}
192
+ animate={{
193
+ opacity: stacked ? 1 - index * 0.25 : 1,
194
+ y: stacked ? index * 9 : index * (pill + 14) + index * 6,
195
+ scale: stacked ? 1 - index * 0.05 : 1,
196
+ }}
197
+ exit={
198
+ reducedMotion
199
+ ? { opacity: 0 }
200
+ : {
201
+ opacity: 0,
202
+ y: -18,
203
+ scale: 0.6,
204
+ transition: spring.snappy,
205
+ }
206
+ }
207
+ transition={reducedMotion ? { duration: 0 } : spring.bouncy}
208
+ drag={reducedMotion ? false : "y"}
209
+ dragConstraints={{ top: 0, bottom: 0 }}
210
+ dragElastic={{ top: 0.6, bottom: 0.1 }}
211
+ onDragEnd={(_, info) => {
212
+ if (info.offset.y < -30 || info.velocity.y < -300)
213
+ toastApi.dismiss(toast.id);
214
+ }}
215
+ >
216
+ <motion.div
217
+ role="status"
218
+ className="pointer-events-auto relative"
219
+ initial={{ width: pill, height: pill }}
220
+ animate={{ width, height }}
221
+ transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
222
+ >
223
+ {/* Shapes, under the goo filter; padded so the blur isn't clipped. */}
224
+ <div
225
+ aria-hidden="true"
226
+ className="absolute -inset-3"
227
+ style={{
228
+ filter: reducedMotion ? undefined : `url(#${filterId})`,
229
+ }}
230
+ >
231
+ <motion.span
232
+ className="bg-inverse absolute top-3 left-3 rounded-full shadow-xl"
233
+ style={{ width: pill, height: pill }}
234
+ animate={{ scale: open ? 1 : 1.06 }}
235
+ transition={reducedMotion ? { duration: 0 } : spring.bouncy}
236
+ />
237
+ <motion.span
238
+ className="bg-inverse absolute top-3 left-3"
239
+ initial={{
240
+ width: pill,
241
+ height: pill,
242
+ borderRadius: pill / 2,
243
+ }}
244
+ animate={{
245
+ width,
246
+ height,
247
+ borderRadius: Math.min(pill / 2, height / 2, 22),
248
+ }}
249
+ transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
250
+ />
251
+ </div>
244
252
 
245
- {/* Content, crisp above the shapes. */}
246
- <motion.div
247
- ref={content}
248
- className={contentClasses[toast.size]}
249
- style={{ minHeight: pill }}
250
- animate={{ opacity: open ? 1 : 0 }}
251
- transition={
252
- reducedMotion
253
- ? { duration: 0 }
254
- : {
255
- duration: open ? 0.22 : 0.1,
256
- delay: open ? 0.08 : 0,
257
- }
258
- }
259
- >
260
- <ToneIcon tone={toast.tone} size={toast.size} reducedMotion={reducedMotion} />
261
- <div className="flex min-w-0 flex-col py-px">
262
- <AnimatePresence mode="popLayout" initial={false}>
263
- <motion.span
264
- key={toast.title}
265
- className={titleClasses[toast.size]}
266
- initial={
267
- reducedMotion
268
- ? false
269
- : {
270
- opacity: 0,
271
- y: 6,
272
- filter: "blur(4px)",
273
- }
274
- }
275
- animate={{
276
- opacity: 1,
277
- y: 0,
278
- filter: "blur(0px)",
279
- }}
280
- exit={
281
- reducedMotion
282
- ? { opacity: 0 }
283
- : {
284
- opacity: 0,
285
- y: -6,
286
- filter: "blur(4px)",
287
- }
288
- }
289
- transition={reducedMotion ? { duration: 0 } : spring.snappy}
290
- >
291
- {toast.title}
292
- </motion.span>
293
- </AnimatePresence>
294
- {toast.description && (
295
- <span className={descriptionClasses[toast.size]}>
296
- {toast.description}
297
- </span>
298
- )}
299
- </div>
300
- {toast.action && (
301
- <button
302
- type="button"
303
- onClick={() => {
304
- toast.action?.onClick()
305
- toastApi.dismiss(toast.id)
306
- }}
307
- className={actionClasses[toast.size]}
308
- >
309
- {toast.action.label}
310
- </button>
311
- )}
312
- </motion.div>
313
- </motion.div>
314
- </motion.li>
315
- )
253
+ {/* Content, crisp above the shapes. */}
254
+ <motion.div
255
+ ref={content}
256
+ className={contentClasses[toast.size]}
257
+ style={{ minHeight: pill }}
258
+ animate={{ opacity: open ? 1 : 0 }}
259
+ transition={
260
+ reducedMotion
261
+ ? { duration: 0 }
262
+ : {
263
+ duration: open ? 0.22 : 0.1,
264
+ delay: open ? 0.08 : 0,
265
+ }
266
+ }
267
+ >
268
+ <ToneIcon
269
+ tone={toast.tone}
270
+ size={toast.size}
271
+ reducedMotion={reducedMotion}
272
+ />
273
+ <div className="flex min-w-0 flex-col py-px">
274
+ <AnimatePresence mode="popLayout" initial={false}>
275
+ <motion.span
276
+ key={toast.title}
277
+ className={titleClasses[toast.size]}
278
+ initial={
279
+ reducedMotion
280
+ ? false
281
+ : {
282
+ opacity: 0,
283
+ y: 6,
284
+ filter: "blur(4px)",
285
+ }
286
+ }
287
+ animate={{
288
+ opacity: 1,
289
+ y: 0,
290
+ filter: "blur(0px)",
291
+ }}
292
+ exit={
293
+ reducedMotion
294
+ ? { opacity: 0 }
295
+ : {
296
+ opacity: 0,
297
+ y: -6,
298
+ filter: "blur(4px)",
299
+ }
300
+ }
301
+ transition={reducedMotion ? { duration: 0 } : spring.snappy}
302
+ >
303
+ {toast.title}
304
+ </motion.span>
305
+ </AnimatePresence>
306
+ {toast.description && (
307
+ <span className={descriptionClasses[toast.size]}>
308
+ {toast.description}
309
+ </span>
310
+ )}
311
+ </div>
312
+ {toast.action && (
313
+ <button
314
+ type="button"
315
+ onClick={() => {
316
+ toast.action?.onClick();
317
+ toastApi.dismiss(toast.id);
318
+ }}
319
+ className={actionClasses[toast.size]}
320
+ >
321
+ {toast.action.label}
322
+ </button>
323
+ )}
324
+ </motion.div>
325
+ </motion.div>
326
+ </motion.li>
327
+ );
316
328
  }
317
329
 
318
330
  const toneColors: Record<ToastTone, string> = {
319
- neutral: "bg-transparent",
320
- success: "bg-success text-white",
321
- error: "bg-danger text-white",
322
- loading: "bg-on-inverse/15 text-on-inverse",
323
- }
331
+ neutral: "bg-transparent",
332
+ success: "bg-success text-white",
333
+ error: "bg-danger text-white",
334
+ loading: "bg-on-inverse/15 text-on-inverse",
335
+ };
324
336
 
325
337
  function ToneIcon({
326
- tone,
327
- size,
328
- reducedMotion,
338
+ tone,
339
+ size,
340
+ reducedMotion,
329
341
  }: {
330
- tone: ToastTone
331
- size: ToastSize
332
- reducedMotion: boolean
342
+ tone: ToastTone;
343
+ size: ToastSize;
344
+ reducedMotion: boolean;
333
345
  }) {
334
- return (
335
- <AnimatePresence mode="popLayout" initial={false}>
336
- <motion.span
337
- key={tone}
338
- className={cn(toneIconClasses[size], toneColors[tone])}
339
- initial={
340
- reducedMotion
341
- ? false
342
- : {
343
- scale: 0.3,
344
- opacity: 0,
345
- rotate: -40,
346
- }
347
- }
348
- animate={{ scale: 1, opacity: 1, rotate: 0 }}
349
- exit={{ scale: 0.3, opacity: 0 }}
350
- transition={reducedMotion ? { duration: 0 } : spring.bouncy}
351
- >
352
- {tone === "success" && <CheckIcon size={13} strokeWidth={3} />}
353
- {tone === "error" && <CloseIcon size={12} strokeWidth={3} />}
354
- {/* The full stop from the Notables mark. */}
355
- {tone === "neutral" && <span className="size-2 rounded-full bg-[#EE8A2A]" />}
356
- {tone === "loading" && <Spinner reducedMotion={reducedMotion} />}
357
- </motion.span>
358
- </AnimatePresence>
359
- )
346
+ return (
347
+ <AnimatePresence mode="popLayout" initial={false}>
348
+ <motion.span
349
+ key={tone}
350
+ className={cn(toneIconClasses[size], toneColors[tone])}
351
+ initial={
352
+ reducedMotion
353
+ ? false
354
+ : {
355
+ scale: 0.3,
356
+ opacity: 0,
357
+ rotate: -40,
358
+ }
359
+ }
360
+ animate={{ scale: 1, opacity: 1, rotate: 0 }}
361
+ exit={{ scale: 0.3, opacity: 0 }}
362
+ transition={reducedMotion ? { duration: 0 } : spring.bouncy}
363
+ >
364
+ {tone === "success" && <CheckIcon size={13} strokeWidth={3} />}
365
+ {tone === "error" && <CloseIcon size={12} strokeWidth={3} />}
366
+ {/* The full stop from the Notables mark. */}
367
+ {tone === "neutral" && (
368
+ <span className="size-2 rounded-full bg-[#EE8A2A]" />
369
+ )}
370
+ {tone === "loading" && <Spinner reducedMotion={reducedMotion} />}
371
+ </motion.span>
372
+ </AnimatePresence>
373
+ );
360
374
  }
361
375
 
362
376
  function Spinner({ reducedMotion }: { reducedMotion: boolean }) {
363
- return (
364
- <motion.svg
365
- viewBox="0 0 20 20"
366
- className="size-4"
367
- animate={reducedMotion ? undefined : { rotate: 360 }}
368
- transition={{
369
- repeat: Number.POSITIVE_INFINITY,
370
- duration: 0.8,
371
- ease: "linear",
372
- }}
373
- aria-hidden="true"
374
- >
375
- <circle
376
- cx="10"
377
- cy="10"
378
- r="7"
379
- fill="none"
380
- stroke="currentColor"
381
- strokeOpacity="0.25"
382
- strokeWidth="2.4"
383
- />
384
- <path
385
- d="M10 3a7 7 0 0 1 7 7"
386
- fill="none"
387
- stroke="currentColor"
388
- strokeWidth="2.4"
389
- strokeLinecap="round"
390
- />
391
- </motion.svg>
392
- )
377
+ return (
378
+ <motion.svg
379
+ viewBox="0 0 20 20"
380
+ className="size-4"
381
+ animate={reducedMotion ? undefined : { rotate: 360 }}
382
+ transition={{
383
+ repeat: Number.POSITIVE_INFINITY,
384
+ duration: 0.8,
385
+ ease: "linear",
386
+ }}
387
+ aria-hidden="true"
388
+ >
389
+ <circle
390
+ cx="10"
391
+ cy="10"
392
+ r="7"
393
+ fill="none"
394
+ stroke="currentColor"
395
+ strokeOpacity="0.25"
396
+ strokeWidth="2.4"
397
+ />
398
+ <path
399
+ d="M10 3a7 7 0 0 1 7 7"
400
+ fill="none"
401
+ stroke="currentColor"
402
+ strokeWidth="2.4"
403
+ strokeLinecap="round"
404
+ />
405
+ </motion.svg>
406
+ );
393
407
  }