gbs-add-block 1.2.11 → 1.2.13
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/README.md +4 -2
- package/index.cjs +7 -1
- package/package.json +1 -1
- package/source/beta-components/fileuploader/README.md +99 -0
- package/source/beta-components/fileuploader/__tests__/core.test.ts +395 -0
- package/source/beta-components/fileuploader/core/format.ts +79 -0
- package/source/beta-components/fileuploader/core/index.ts +26 -0
- package/source/beta-components/fileuploader/core/store.ts +432 -0
- package/source/beta-components/fileuploader/core/transport.ts +145 -0
- package/source/beta-components/fileuploader/core/types.ts +126 -0
- package/source/beta-components/fileuploader/core/validate.ts +83 -0
- package/source/beta-components/fileuploader/index.ts +7 -0
- package/source/beta-components/fileuploader/react/FileRow.tsx +268 -0
- package/source/beta-components/fileuploader/react/FileUploader.tsx +515 -0
- package/source/beta-components/fileuploader/react/icons.tsx +80 -0
- package/source/beta-components/fileuploader/react/locale.ts +33 -0
- package/source/beta-components/fileuploader/react/props.ts +31 -0
- package/source/beta-components/fileuploader/react/useFileUploader.ts +32 -0
- package/source/beta-components/fileuploader/styles.css +395 -0
- package/source/beta-components/toaster/README.md +128 -0
- package/source/beta-components/toaster/__tests__/core.test.ts +256 -0
- package/source/beta-components/toaster/core/api.ts +87 -0
- package/source/beta-components/toaster/core/index.ts +14 -0
- package/source/beta-components/toaster/core/store.ts +211 -0
- package/source/beta-components/toaster/core/toast.ts +12 -0
- package/source/beta-components/toaster/core/types.ts +78 -0
- package/source/beta-components/toaster/index.ts +6 -0
- package/source/beta-components/toaster/react/ToastItem.tsx +164 -0
- package/source/beta-components/toaster/react/Toaster.tsx +187 -0
- package/source/beta-components/toaster/react/icons.tsx +56 -0
- package/source/beta-components/toaster/react/locale.ts +6 -0
- package/source/beta-components/toaster/react/props.ts +15 -0
- package/source/beta-components/toaster/react/useToasts.ts +11 -0
- package/source/beta-components/toaster/styles.css +283 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type PointerEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import type { ToastStore } from "../core/store";
|
|
13
|
+
import type { ToastAction, ToasterLocaleText, ToastRecord, ToastType } from "../core/types";
|
|
14
|
+
import { ErrorIcon, InfoIcon, SpinnerIcon, SuccessIcon, WarningIcon, XIcon } from "./icons";
|
|
15
|
+
import { cx, type ToasterSlot } from "./props";
|
|
16
|
+
|
|
17
|
+
/** Horizontal drag, in pixels, that dismisses a toast. */
|
|
18
|
+
const SWIPE_DISMISS = 64;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_ICONS: Record<ToastType, ReactNode> = {
|
|
21
|
+
default: null,
|
|
22
|
+
success: <SuccessIcon />,
|
|
23
|
+
error: <ErrorIcon />,
|
|
24
|
+
warning: <WarningIcon />,
|
|
25
|
+
info: <InfoIcon />,
|
|
26
|
+
loading: <SpinnerIcon />,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface ToastItemProps {
|
|
30
|
+
toast: ToastRecord;
|
|
31
|
+
store: ToastStore;
|
|
32
|
+
closeButton: boolean;
|
|
33
|
+
icons?: Partial<Record<ToastType, ReactNode>>;
|
|
34
|
+
classNames?: Partial<Record<ToasterSlot, string>>;
|
|
35
|
+
text: ToasterLocaleText;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ToastItem({ toast, store, closeButton, icons, classNames, text }: ToastItemProps) {
|
|
39
|
+
const [swipe, setSwipe] = useState(0);
|
|
40
|
+
const drag = useRef<{ pointerId: number; startX: number } | null>(null);
|
|
41
|
+
const { action, cancel, render } = toast;
|
|
42
|
+
const closing = toast.state === "closing";
|
|
43
|
+
|
|
44
|
+
const dismiss = () => store.dismiss(toast.id);
|
|
45
|
+
|
|
46
|
+
// `null` from the toast or the Toaster hides the icon; `undefined` means "use the default".
|
|
47
|
+
const icon =
|
|
48
|
+
toast.icon !== undefined
|
|
49
|
+
? toast.icon
|
|
50
|
+
: icons !== undefined && Object.hasOwn(icons, toast.type)
|
|
51
|
+
? icons[toast.type]
|
|
52
|
+
: DEFAULT_ICONS[toast.type];
|
|
53
|
+
|
|
54
|
+
const runAction = (item: ToastAction, event: MouseEvent<HTMLButtonElement>) => {
|
|
55
|
+
item.onClick(event);
|
|
56
|
+
if (!event.defaultPrevented) dismiss();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onKeyDown = (event: KeyboardEvent<HTMLLIElement>) => {
|
|
60
|
+
if (event.key !== "Escape" || !toast.dismissible) return;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
// Keep focus in the notification region instead of dropping it on <body>.
|
|
63
|
+
event.currentTarget.closest<HTMLElement>(".ts-list")?.focus();
|
|
64
|
+
dismiss();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onPointerDown = (event: PointerEvent<HTMLLIElement>) => {
|
|
68
|
+
if (!toast.dismissible || closing || event.button !== 0) return;
|
|
69
|
+
if ((event.target as Element).closest("button, a, input, select, textarea")) return;
|
|
70
|
+
drag.current = { pointerId: event.pointerId, startX: event.clientX };
|
|
71
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onPointerMove = (event: PointerEvent<HTMLLIElement>) => {
|
|
75
|
+
const start = drag.current;
|
|
76
|
+
if (start?.pointerId === event.pointerId) setSwipe(event.clientX - start.startX);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const endSwipe = (event: PointerEvent<HTMLLIElement>) => {
|
|
80
|
+
const start = drag.current;
|
|
81
|
+
if (start?.pointerId !== event.pointerId) return;
|
|
82
|
+
drag.current = null;
|
|
83
|
+
const far = Math.abs(event.clientX - start.startX) >= SWIPE_DISMISS;
|
|
84
|
+
if (event.type === "pointerup" && far) dismiss();
|
|
85
|
+
else setSwipe(0);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<li
|
|
90
|
+
className={cx("ts-toast", classNames?.toast, toast.className)}
|
|
91
|
+
// The list is a polite live region; errors interrupt.
|
|
92
|
+
role={toast.type === "error" ? "alert" : undefined}
|
|
93
|
+
aria-atomic="true"
|
|
94
|
+
tabIndex={0}
|
|
95
|
+
data-type={toast.type}
|
|
96
|
+
data-state={toast.state}
|
|
97
|
+
data-custom={render ? "" : undefined}
|
|
98
|
+
data-swiping={swipe !== 0 ? "" : undefined}
|
|
99
|
+
style={{ "--ts-swipe": `${swipe}px` } as CSSProperties}
|
|
100
|
+
onKeyDown={onKeyDown}
|
|
101
|
+
onPointerDown={onPointerDown}
|
|
102
|
+
onPointerMove={onPointerMove}
|
|
103
|
+
onPointerUp={endSwipe}
|
|
104
|
+
onPointerCancel={endSwipe}
|
|
105
|
+
>
|
|
106
|
+
{!render && icon != null && (
|
|
107
|
+
<span className={cx("ts-icon", classNames?.icon)} aria-hidden="true">
|
|
108
|
+
{icon}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<div className={cx("ts-content", classNames?.content)}>
|
|
113
|
+
{render ? (
|
|
114
|
+
render({ id: toast.id, dismiss })
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
<div className={cx("ts-title", classNames?.title)}>{toast.title}</div>
|
|
118
|
+
{toast.description != null && (
|
|
119
|
+
<div className={cx("ts-description", classNames?.description)}>
|
|
120
|
+
{toast.description}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{!render && (action || cancel) && (
|
|
128
|
+
<div className={cx("ts-actions", classNames?.actions)}>
|
|
129
|
+
{cancel && (
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className={cx("ts-button", classNames?.cancel)}
|
|
133
|
+
data-variant="cancel"
|
|
134
|
+
onClick={(event) => runAction(cancel, event)}
|
|
135
|
+
>
|
|
136
|
+
{cancel.label}
|
|
137
|
+
</button>
|
|
138
|
+
)}
|
|
139
|
+
{action && (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className={cx("ts-button", classNames?.action)}
|
|
143
|
+
data-variant="action"
|
|
144
|
+
onClick={(event) => runAction(action, event)}
|
|
145
|
+
>
|
|
146
|
+
{action.label}
|
|
147
|
+
</button>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{closeButton && toast.dismissible && (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className={cx("ts-close", classNames?.close)}
|
|
156
|
+
aria-label={text.close}
|
|
157
|
+
onClick={dismiss}
|
|
158
|
+
>
|
|
159
|
+
<XIcon />
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
import { visibleToasts, type ToastStore } from "../core/store";
|
|
14
|
+
import { toastStore } from "../core/toast";
|
|
15
|
+
import type { ToasterLocaleText, ToastPosition, ToastType } from "../core/types";
|
|
16
|
+
import { defaultToasterText } from "./locale";
|
|
17
|
+
import { cx, type ToasterSlot } from "./props";
|
|
18
|
+
import { ToastItem } from "./ToastItem";
|
|
19
|
+
import { useToasts } from "./useToasts";
|
|
20
|
+
|
|
21
|
+
export interface ToasterProps {
|
|
22
|
+
/** Default `"top-right"`. */
|
|
23
|
+
position?: ToastPosition;
|
|
24
|
+
/** Open toasts on screen at once; the rest wait their turn. Default 3. */
|
|
25
|
+
limit?: number;
|
|
26
|
+
/** Default auto-close delay in milliseconds. Default 5000. */
|
|
27
|
+
duration?: number;
|
|
28
|
+
/** Show a close button on dismissible toasts. Default true. */
|
|
29
|
+
closeButton?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Keys that move focus to the notifications: `KeyboardEvent` modifier names
|
|
32
|
+
* plus a `code`. Default `["altKey", "KeyT"]`.
|
|
33
|
+
*/
|
|
34
|
+
hotkey?: readonly string[];
|
|
35
|
+
/** Replace the icon per type; `null` hides it. */
|
|
36
|
+
icons?: Partial<Record<ToastType, ReactNode>>;
|
|
37
|
+
/** A store from `createToastStore()`. Defaults to the one behind `toast()`. */
|
|
38
|
+
store?: ToastStore;
|
|
39
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
40
|
+
className?: string;
|
|
41
|
+
classNames?: Partial<Record<ToasterSlot, string>>;
|
|
42
|
+
style?: CSSProperties;
|
|
43
|
+
localeText?: Partial<ToasterLocaleText>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Modifier = "altKey" | "ctrlKey" | "metaKey" | "shiftKey";
|
|
47
|
+
|
|
48
|
+
const MODIFIER_LABELS: Record<Modifier, string> = {
|
|
49
|
+
altKey: "Alt",
|
|
50
|
+
ctrlKey: "Ctrl",
|
|
51
|
+
metaKey: "Meta",
|
|
52
|
+
shiftKey: "Shift",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const DEFAULT_HOTKEY: readonly string[] = ["altKey", "KeyT"];
|
|
56
|
+
|
|
57
|
+
const isModifier = (key: string): key is Modifier => Object.hasOwn(MODIFIER_LABELS, key);
|
|
58
|
+
|
|
59
|
+
const hotkeyLabel = (keys: readonly string[]) =>
|
|
60
|
+
keys
|
|
61
|
+
.map((key) => (isModifier(key) ? MODIFIER_LABELS[key] : key.replace(/^(Key|Digit)/, "")))
|
|
62
|
+
.join("+");
|
|
63
|
+
|
|
64
|
+
const subscribeNothing = () => () => {};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mount once, anywhere in the app. Toasts come from `toast()` and render in a
|
|
68
|
+
* portal on `<body>`, so no parent's overflow, transform or z-index can hide them.
|
|
69
|
+
*/
|
|
70
|
+
export function Toaster(props: ToasterProps) {
|
|
71
|
+
const {
|
|
72
|
+
position = "top-right",
|
|
73
|
+
limit,
|
|
74
|
+
duration,
|
|
75
|
+
closeButton = true,
|
|
76
|
+
hotkey = DEFAULT_HOTKEY,
|
|
77
|
+
icons,
|
|
78
|
+
store = toastStore,
|
|
79
|
+
dir,
|
|
80
|
+
className,
|
|
81
|
+
classNames,
|
|
82
|
+
style,
|
|
83
|
+
localeText,
|
|
84
|
+
} = props;
|
|
85
|
+
|
|
86
|
+
const snapshot = useToasts(store);
|
|
87
|
+
// False during a server render and hydration, so the portal never causes a mismatch.
|
|
88
|
+
const mounted = useSyncExternalStore(
|
|
89
|
+
subscribeNothing,
|
|
90
|
+
() => true,
|
|
91
|
+
() => false,
|
|
92
|
+
);
|
|
93
|
+
const text = useMemo(() => ({ ...defaultToasterText, ...localeText }), [localeText]);
|
|
94
|
+
|
|
95
|
+
const listRef = useRef<HTMLOListElement>(null);
|
|
96
|
+
const pointerInside = useRef(false);
|
|
97
|
+
const focusInside = useRef(false);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
store.configure({ limit, duration });
|
|
101
|
+
}, [store, limit, duration]);
|
|
102
|
+
|
|
103
|
+
/** Timers hold while someone is reading: pointer over, focus inside, or tab hidden. */
|
|
104
|
+
const syncPause = useCallback(() => {
|
|
105
|
+
const hold =
|
|
106
|
+
pointerInside.current || focusInside.current || document.visibilityState === "hidden";
|
|
107
|
+
if (hold) store.pause();
|
|
108
|
+
else store.resume();
|
|
109
|
+
}, [store]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
document.addEventListener("visibilitychange", syncPause);
|
|
113
|
+
return () => document.removeEventListener("visibilitychange", syncPause);
|
|
114
|
+
}, [syncPause]);
|
|
115
|
+
|
|
116
|
+
const empty = snapshot.toasts.length === 0;
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!empty) return;
|
|
119
|
+
// A toast that disappears under the pointer never fires pointerleave.
|
|
120
|
+
pointerInside.current = false;
|
|
121
|
+
focusInside.current = false;
|
|
122
|
+
syncPause();
|
|
123
|
+
}, [empty, syncPause]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (hotkey.length === 0) return;
|
|
127
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
128
|
+
const pressed = hotkey.every((key) => (isModifier(key) ? event[key] : event.code === key));
|
|
129
|
+
if (!pressed) return;
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
listRef.current?.focus();
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener("keydown", onKeyDown);
|
|
134
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
135
|
+
}, [hotkey]);
|
|
136
|
+
|
|
137
|
+
if (!mounted) return null;
|
|
138
|
+
|
|
139
|
+
return createPortal(
|
|
140
|
+
<section
|
|
141
|
+
className={cx("ts-region", classNames?.region, className)}
|
|
142
|
+
style={style}
|
|
143
|
+
dir={dir}
|
|
144
|
+
data-position={position}
|
|
145
|
+
aria-label={text.regionLabel(hotkeyLabel(hotkey))}
|
|
146
|
+
onPointerEnter={() => {
|
|
147
|
+
pointerInside.current = true;
|
|
148
|
+
syncPause();
|
|
149
|
+
}}
|
|
150
|
+
onPointerLeave={() => {
|
|
151
|
+
pointerInside.current = false;
|
|
152
|
+
syncPause();
|
|
153
|
+
}}
|
|
154
|
+
onFocus={() => {
|
|
155
|
+
focusInside.current = true;
|
|
156
|
+
syncPause();
|
|
157
|
+
}}
|
|
158
|
+
onBlur={(event) => {
|
|
159
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
160
|
+
focusInside.current = false;
|
|
161
|
+
syncPause();
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{/* Always mounted, even when empty, so screen readers announce the first toast. */}
|
|
165
|
+
<ol
|
|
166
|
+
ref={listRef}
|
|
167
|
+
tabIndex={-1}
|
|
168
|
+
className={cx("ts-list", classNames?.list)}
|
|
169
|
+
aria-live="polite"
|
|
170
|
+
aria-relevant="additions text"
|
|
171
|
+
>
|
|
172
|
+
{visibleToasts(snapshot).map((item) => (
|
|
173
|
+
<ToastItem
|
|
174
|
+
key={item.id}
|
|
175
|
+
toast={item}
|
|
176
|
+
store={store}
|
|
177
|
+
closeButton={closeButton}
|
|
178
|
+
icons={icons}
|
|
179
|
+
classNames={classNames}
|
|
180
|
+
text={text}
|
|
181
|
+
/>
|
|
182
|
+
))}
|
|
183
|
+
</ol>
|
|
184
|
+
</section>,
|
|
185
|
+
document.body,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
type IconProps = SVGProps<SVGSVGElement>;
|
|
4
|
+
|
|
5
|
+
function Svg(props: IconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
width="18"
|
|
9
|
+
height="18"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth="2"
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
focusable="false"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SuccessIcon = (p: IconProps) => (
|
|
24
|
+
<Svg {...p}>
|
|
25
|
+
<circle cx="12" cy="12" r="9" />
|
|
26
|
+
<path d="m8 12.5 2.5 2.5L16 9.5" />
|
|
27
|
+
</Svg>
|
|
28
|
+
);
|
|
29
|
+
export const ErrorIcon = (p: IconProps) => (
|
|
30
|
+
<Svg {...p}>
|
|
31
|
+
<circle cx="12" cy="12" r="9" />
|
|
32
|
+
<path d="M12 7.5v5.5M12 16.5h.01" />
|
|
33
|
+
</Svg>
|
|
34
|
+
);
|
|
35
|
+
export const WarningIcon = (p: IconProps) => (
|
|
36
|
+
<Svg {...p}>
|
|
37
|
+
<path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
|
|
38
|
+
<path d="M12 9v4M12 17h.01" />
|
|
39
|
+
</Svg>
|
|
40
|
+
);
|
|
41
|
+
export const InfoIcon = (p: IconProps) => (
|
|
42
|
+
<Svg {...p}>
|
|
43
|
+
<circle cx="12" cy="12" r="9" />
|
|
44
|
+
<path d="M12 11v5M12 7.5h.01" />
|
|
45
|
+
</Svg>
|
|
46
|
+
);
|
|
47
|
+
export const SpinnerIcon = (p: IconProps) => (
|
|
48
|
+
<Svg {...p} className={["ts-spinner", p.className].filter(Boolean).join(" ")}>
|
|
49
|
+
<path d="M12 3a9 9 0 1 0 9 9" />
|
|
50
|
+
</Svg>
|
|
51
|
+
);
|
|
52
|
+
export const XIcon = (p: IconProps) => (
|
|
53
|
+
<Svg width={14} height={14} {...p}>
|
|
54
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
55
|
+
</Svg>
|
|
56
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ToasterSlot =
|
|
2
|
+
| "region"
|
|
3
|
+
| "list"
|
|
4
|
+
| "toast"
|
|
5
|
+
| "icon"
|
|
6
|
+
| "content"
|
|
7
|
+
| "title"
|
|
8
|
+
| "description"
|
|
9
|
+
| "actions"
|
|
10
|
+
| "action"
|
|
11
|
+
| "cancel"
|
|
12
|
+
| "close";
|
|
13
|
+
|
|
14
|
+
export const cx = (...names: (string | false | null | undefined)[]) =>
|
|
15
|
+
names.filter(Boolean).join(" ");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import type { ToastStore } from "../core/store";
|
|
5
|
+
import { toastStore } from "../core/toast";
|
|
6
|
+
import type { ToastSnapshot } from "../core/types";
|
|
7
|
+
|
|
8
|
+
/** The live toast list, for building your own notification UI on the same store. */
|
|
9
|
+
export function useToasts(store: ToastStore = toastStore): ToastSnapshot {
|
|
10
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
11
|
+
}
|