@a-type/ui 3.0.44 → 3.1.0-beta.4

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.
@@ -1,80 +1,261 @@
1
- export type * from 'react-hot-toast';
2
- export { toast } from 'react-hot-toast';
1
+ import {
2
+ Toast,
3
+ ToastManagerAddOptions,
4
+ ToastManagerPromiseOptions,
5
+ ToastObject,
6
+ } from '@base-ui/react/toast';
3
7
  import clsx from 'clsx';
4
- import { AnimatePresence, motion } from 'motion/react';
5
- import { createPortal } from 'react-dom';
6
- import { DefaultToastOptions, useToaster } from 'react-hot-toast';
8
+ import { ReactNode } from 'react';
7
9
  import { useResolvedColorMode } from '../../colorMode.js';
10
+ import { Button, ButtonProps } from '../button/index.js';
8
11
  import { Icon } from '../icon/Icon.js';
12
+ import { Spinner } from '../spinner/Spinner.js';
9
13
 
10
- const toastOptions: DefaultToastOptions = {};
14
+ export const manager = Toast.createToastManager();
11
15
 
12
- export const Toaster = (props: { className?: string }) => {
16
+ export const DefaultToastProvider = ({
17
+ children,
18
+ ...rest
19
+ }: {
20
+ children?: React.ReactNode;
21
+ timeout?: number;
22
+ }) => {
23
+ return (
24
+ <Toast.Provider toastManager={manager} {...rest}>
25
+ {children}
26
+ </Toast.Provider>
27
+ );
28
+ };
29
+
30
+ export function Toaster() {
31
+ return (
32
+ <Toast.Portal>
33
+ <Toast.Viewport className="overflow-clip">
34
+ <ToastList />
35
+ </Toast.Viewport>
36
+ </Toast.Portal>
37
+ );
38
+ }
39
+
40
+ function ToastList() {
41
+ const { toasts: untypedToasts } = Toast.useToastManager();
13
42
  const mode = useResolvedColorMode();
14
- const { toasts, handlers } = useToaster(toastOptions);
15
- const { startPause, endPause } = handlers;
16
- const visibleToasts = toasts.filter((t) => t.visible);
17
43
 
18
- const target = typeof document === 'undefined' ? null : document.body;
19
- if (!target) {
20
- return null;
21
- }
44
+ const toasts = untypedToasts as Array<ToastObject<CustomToastData>>;
22
45
 
23
- return createPortal(
24
- <div
46
+ return toasts.map((toast) => (
47
+ <Toast.Root
48
+ key={toast.id}
49
+ toast={toast}
50
+ swipeDirection={['up', 'right', 'left']}
25
51
  className={clsx(
26
- 'fixed z-100000 flex flex-col items-center gap-xs left-1/2 center-x top-sm max-w-400px',
52
+ // variable setup
53
+ '[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))]',
54
+ '[--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))]',
55
+ '[--offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))]',
56
+ // basic positioning
57
+ 'absolute left-0 top-xs left-auto z-[calc(100000-var(--toast-index))] mr-0 w-full origin-top',
58
+ 'h-[--height]',
59
+ 'flex flex-col gap-xs items-center',
60
+ // other properties
61
+ 'select-none',
62
+ // animation and interaction
63
+ 'translate-x-[--toast-swipe-movement-x] translate-y-[calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height)))] scale-[var(--scale)]',
64
+ '[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]',
65
+ // ::after
66
+ 'after:(absolute top-full left-0 h-[calc(var(--gap)+1px)] w-full content-empty)',
67
+ // starting style
68
+ 'data-[starting-style]:(-translate-y-150%)',
69
+ // limited
70
+ 'data-[limited]:opacity-0',
71
+ //expanded
72
+ 'data-[expanded]:(h-[--toast-height] translate-x-[--toast-swipe-movement-x] translate-y-[--offset-y] scale-100)',
73
+ // ending styles
74
+ 'data-[ending-style]:(opacity-0)',
75
+ // natural or close button
76
+ '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:(-translate-y-150% scale-90 opacity-50)',
77
+ // swiping down
78
+ 'data-[ending-style]:data-[swipe-direction=down]:(translate-y-[calc(var(--toast-swipe-movement-y)+150%)])',
79
+ 'data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:(translate-y-[calc(var(--toast-swipe-movement-y)+150%)])',
80
+ // swiping left
81
+ 'data-[ending-style]:data-[swipe-direction=left]:(translate-x-[calc(var(--toast-swipe-movement-x)-150%)] translate-y-[var(--offset-y)])',
82
+ 'data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:(translate-x-[calc(var(--toast-swipe-movement-x)-150%)] translate-y-[var(--offset-y)])',
83
+ // swiping right
84
+ 'data-[ending-style]:data-[swipe-direction=right]:(translate-x-[calc(var(--toast-swipe-movement-x)+150%)] translate-y-[var(--offset-y)])',
85
+ 'data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:(translate-x-[calc(var(--toast-swipe-movement-x)+150%)] translate-y-[var(--offset-y)])',
86
+ // swiping up
87
+ 'data-[ending-style]:data-[swipe-direction=up]:(translate-y-[calc(var(--toast-swipe-movement-y)-150%)])',
88
+ 'data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:(translate-y-[calc(var(--toast-swipe-movement-y)-150%)])',
89
+ // themeing
90
+ {
91
+ 'palette-success': toast.type === 'success',
92
+ 'palette-attention': toast.type === 'error',
93
+ 'palette-info': toast.type === 'blank',
94
+ },
27
95
  mode === 'dark' ? 'override-light' : 'override-dark',
28
- props.className,
29
96
  )}
30
- onMouseEnter={startPause}
31
- onMouseLeave={endPause}
32
97
  >
33
- <AnimatePresence>
34
- {visibleToasts.map((toast) => {
35
- const message =
36
- typeof toast.message === 'function'
37
- ? toast.message(toast)
38
- : toast.message;
39
- return (
40
- <motion.div
41
- key={toast.id}
42
- className={clsx(
43
- {
44
- 'palette-success': toast.type === 'success',
45
- 'palette-attention': toast.type === 'error',
46
- 'palette-info': toast.type === 'blank',
47
- },
48
- 'bg-main-wash color-black rounded-md shadow-md px-md py-sm',
49
- 'flex flex-row gap-sm',
50
- )}
51
- {...toast.ariaProps}
52
- initial={{ scale: 0.8, opacity: 0, y: -20 }}
53
- exit={{ scale: 0.8, opacity: 0, y: -20 }}
54
- animate={{
55
- scale: 1,
56
- opacity: 1,
57
- y: 0,
58
- }}
59
- layout
60
- >
61
- <Icon
62
- className="mt-2px"
63
- loading={toast.type === 'loading'}
64
- name={
65
- toast.type === 'success'
66
- ? 'check'
67
- : toast.type === 'error'
68
- ? 'warning'
69
- : 'info'
98
+ <Toast.Content className="[&[data-behind]:not([data-expanded])]:pointer-events-none flex flex-col gap-2px max-w-sm">
99
+ <div
100
+ className={clsx(
101
+ 'layer-components:(bg-main-wash color-black rounded-md b-1 b-solid b-black shadow-md pl-md pr-sm py-sm relative)',
102
+ 'layer-components:(flex flex-row gap-sm)',
103
+ '[[data-behind]:not([data-expanded])_&]:(bg-darken-2 max-h-[--height])',
104
+ )}
105
+ >
106
+ <div
107
+ className={clsx(
108
+ 'flex flex-row gap-xs items-center',
109
+ '[[data-behind]:not([data-expanded])_&]:(opacity-0) [[data-expanded]_&]:(opacity-100) transition-opacity [transition-duration:250ms]',
110
+ )}
111
+ >
112
+ <div className="flex flex-col gap-xs">
113
+ <Toast.Title className="text-sm leading-tight font-bold m-0" />
114
+ <div className="flex gap-sm">
115
+ {toast.data?.loading ? (
116
+ <Spinner size={15} className="relative top-2px" />
117
+ ) : toast.type === 'success' ? (
118
+ <Icon
119
+ name="check"
120
+ color="success"
121
+ className="relative top-2px"
122
+ />
123
+ ) : toast.type === 'error' ? (
124
+ <Icon
125
+ name="warning"
126
+ color="attention"
127
+ className="relative top-2px"
128
+ />
129
+ ) : null}
130
+ <Toast.Description className="text-sm m-0" />
131
+ </div>
132
+ </div>
133
+ <Toast.Close
134
+ className="mb-auto [[data-behind]:not([data-expanded])_&]:(invisible)"
135
+ aria-label="Close"
136
+ render={
137
+ <Button size="small" emphasis="ghost">
138
+ <Icon name="x" />
139
+ </Button>
140
+ }
141
+ />
142
+ </div>
143
+ </div>
144
+ {toast.data?.actions && (
145
+ <div className="flex gap-xxs items-center ml-auto [[data-behind]:not([data-expanded])_&]:(opacity-0) transition-opacity">
146
+ {toast.data.actions.toReversed().map((action, index: number) => (
147
+ <Toast.Action
148
+ key={index}
149
+ className="text-xs"
150
+ onClick={action.onClick}
151
+ render={
152
+ <Button
153
+ size="small"
154
+ emphasis={action.emphasis}
155
+ color={action.color}
156
+ />
70
157
  }
71
- />
72
- {message}
73
- </motion.div>
74
- );
75
- })}
76
- </AnimatePresence>
77
- </div>,
78
- target,
79
- );
80
- };
158
+ >
159
+ {action.label}
160
+ </Toast.Action>
161
+ ))}
162
+ </div>
163
+ )}
164
+ </Toast.Content>
165
+ </Toast.Root>
166
+ ));
167
+ }
168
+
169
+ export interface CustomToastData {
170
+ actions?: {
171
+ label: ReactNode;
172
+ onClick: () => void;
173
+ emphasis?: ButtonProps['emphasis'];
174
+ color?: ButtonProps['color'];
175
+ }[];
176
+ loading?: boolean;
177
+ }
178
+
179
+ export interface ToastOptions extends ToastManagerAddOptions<CustomToastData> {
180
+ /** @deprecated - use timeout */
181
+ duration?: number;
182
+ }
183
+
184
+ function baseToast(
185
+ messageOrOptions: string | ToastOptions,
186
+ maybeOptions?: ToastOptions,
187
+ ) {
188
+ const description =
189
+ typeof messageOrOptions === 'string' ? messageOrOptions : undefined;
190
+ const options =
191
+ typeof messageOrOptions === 'string' ? maybeOptions : messageOrOptions;
192
+ const extraOptions =
193
+ typeof messageOrOptions === 'string' && maybeOptions ? maybeOptions : {};
194
+
195
+ const finalOptions = {
196
+ description,
197
+ timeout:
198
+ options?.duration ??
199
+ extraOptions?.duration ??
200
+ options?.timeout ??
201
+ extraOptions?.timeout,
202
+ ...options,
203
+ ...extraOptions,
204
+ };
205
+ if (options?.id) {
206
+ manager.update<CustomToastData>(options.id, finalOptions);
207
+ return options.id;
208
+ }
209
+ return manager.add(finalOptions);
210
+ }
211
+
212
+ export const toast = Object.assign(baseToast, {
213
+ success(
214
+ messageOrOptions: string | ToastOptions,
215
+ maybeOptions?: ToastOptions,
216
+ ) {
217
+ return baseToast(messageOrOptions, {
218
+ type: 'success',
219
+ ...maybeOptions,
220
+ });
221
+ },
222
+ error(messageOrOptions: string | ToastOptions, maybeOptions?: ToastOptions) {
223
+ return baseToast(messageOrOptions, {
224
+ type: 'error',
225
+ ...maybeOptions,
226
+ });
227
+ },
228
+ promise: function <T>(
229
+ promise: Promise<T>,
230
+ options: ToastManagerPromiseOptions<T, CustomToastData>,
231
+ ) {
232
+ return manager.promise(promise, options);
233
+ },
234
+ loading: function (
235
+ messageOrOptions: string | ToastOptions,
236
+ maybeOptions?: ToastOptions,
237
+ ) {
238
+ const id = baseToast(messageOrOptions, {
239
+ timeout: 0,
240
+ data: { loading: true },
241
+ ...maybeOptions,
242
+ });
243
+
244
+ return {
245
+ id,
246
+ complete: (
247
+ messageOrOptions: string | ToastOptions,
248
+ maybeOptions?: ToastOptions,
249
+ ) => {
250
+ baseToast(messageOrOptions, {
251
+ id,
252
+ data: { loading: false },
253
+ ...maybeOptions,
254
+ });
255
+ },
256
+ };
257
+ },
258
+ });
259
+
260
+ export type * from '@base-ui/react/toast';
261
+ export { Toast };