@enigmax/primitives 0.20.0 → 0.22.0
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/chunk-WPTBURIC.js +1627 -0
- package/dist/{index-dTdAbOWl.d.ts → index-BHpOZncw.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/next/index.d.ts +5 -5
- package/dist/next/index.js +1 -1
- package/dist/react/button.d.ts +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +1 -1
- package/dist/react/input.d.ts +2 -2
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/search.d.ts +1 -1
- package/dist/react/slot.d.ts +2 -2
- package/dist/react/toast.d.ts +168 -34
- package/dist/react/toast.js +2 -2
- package/dist/react-router/index.d.ts +5 -5
- package/dist/react-router/index.js +1 -1
- package/dist/search/index.d.ts +2 -2
- package/dist/{search-DXxY8SEH.d.ts → search-BD9-5O5U.d.ts} +7 -2
- package/package.json +1 -1
- package/recipes/toast/styles.css +697 -128
- package/registry.json +79 -26
- package/src/core/search.ts +6 -1
- package/src/react/index.ts +2 -1
- package/src/react/toast/NOTICE +20 -0
- package/src/react/toast/assets.tsx +85 -0
- package/src/react/toast/cn.ts +10 -0
- package/src/react/toast/hooks.ts +13 -0
- package/src/react/toast/index.tsx +736 -0
- package/src/react/toast/state.ts +207 -0
- package/src/react/toast/styles.ts +738 -0
- package/src/react/toast/types.ts +193 -0
- package/src/react/toaster.tsx +97 -209
- package/dist/chunk-ZCUFYBPB.js +0 -154
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type ToastTypes = "normal" | "action" | "success" | "info" | "warning" | "error" | "loading" | "default";
|
|
4
|
+
|
|
5
|
+
export type PromiseT<Data = any> = Promise<Data> | (() => Promise<Data>);
|
|
6
|
+
|
|
7
|
+
export type PromiseTResult<Data = any> = string | React.ReactNode | ((data: Data) => React.ReactNode | string | Promise<React.ReactNode | string>);
|
|
8
|
+
|
|
9
|
+
export type PromiseExternalToast = Omit<ExternalToast, "description">;
|
|
10
|
+
|
|
11
|
+
export type PromiseData<ToastData = any> = PromiseExternalToast & {
|
|
12
|
+
loading?: string | React.ReactNode;
|
|
13
|
+
success?: PromiseTResult<ToastData>;
|
|
14
|
+
error?: PromiseTResult;
|
|
15
|
+
description?: PromiseTResult;
|
|
16
|
+
finally?: () => void | Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface ToastClassnames {
|
|
20
|
+
toast?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
loader?: string;
|
|
24
|
+
closeButton?: string;
|
|
25
|
+
cancelButton?: string;
|
|
26
|
+
actionButton?: string;
|
|
27
|
+
success?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
info?: string;
|
|
30
|
+
warning?: string;
|
|
31
|
+
loading?: string;
|
|
32
|
+
default?: string;
|
|
33
|
+
content?: string;
|
|
34
|
+
icon?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ToastIcons {
|
|
38
|
+
success?: React.ReactNode;
|
|
39
|
+
info?: React.ReactNode;
|
|
40
|
+
warning?: React.ReactNode;
|
|
41
|
+
error?: React.ReactNode;
|
|
42
|
+
loading?: React.ReactNode;
|
|
43
|
+
close?: React.ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Action {
|
|
47
|
+
label: React.ReactNode;
|
|
48
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
49
|
+
actionButtonStyle?: React.CSSProperties;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ToastT {
|
|
53
|
+
id: number | string;
|
|
54
|
+
title?: (() => React.ReactNode) | React.ReactNode;
|
|
55
|
+
type?: ToastTypes;
|
|
56
|
+
icon?: React.ReactNode;
|
|
57
|
+
jsx?: React.ReactNode;
|
|
58
|
+
richColors?: boolean;
|
|
59
|
+
invert?: boolean;
|
|
60
|
+
closeButton?: boolean;
|
|
61
|
+
dismissible?: boolean;
|
|
62
|
+
description?: (() => React.ReactNode) | React.ReactNode;
|
|
63
|
+
duration?: number;
|
|
64
|
+
delete?: boolean;
|
|
65
|
+
action?: Action | React.ReactNode;
|
|
66
|
+
cancel?: Action | React.ReactNode;
|
|
67
|
+
onDismiss?: (toast: ToastT) => void;
|
|
68
|
+
onAutoClose?: (toast: ToastT) => void;
|
|
69
|
+
promise?: PromiseT;
|
|
70
|
+
cancelButtonStyle?: React.CSSProperties;
|
|
71
|
+
actionButtonStyle?: React.CSSProperties;
|
|
72
|
+
style?: React.CSSProperties;
|
|
73
|
+
unstyled?: boolean;
|
|
74
|
+
className?: string;
|
|
75
|
+
classNames?: ToastClassnames;
|
|
76
|
+
descriptionClassName?: string;
|
|
77
|
+
position?: Position;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isAction(action: Action | React.ReactNode): action is Action {
|
|
81
|
+
return (action as Action).label !== undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type Position = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center";
|
|
85
|
+
export interface HeightT {
|
|
86
|
+
height: number;
|
|
87
|
+
toastId: number | string;
|
|
88
|
+
position: Position;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ToastOptions {
|
|
92
|
+
className?: string;
|
|
93
|
+
closeButton?: boolean;
|
|
94
|
+
descriptionClassName?: string;
|
|
95
|
+
style?: React.CSSProperties;
|
|
96
|
+
cancelButtonStyle?: React.CSSProperties;
|
|
97
|
+
actionButtonStyle?: React.CSSProperties;
|
|
98
|
+
duration?: number;
|
|
99
|
+
unstyled?: boolean;
|
|
100
|
+
classNames?: ToastClassnames;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type Offset =
|
|
104
|
+
| {
|
|
105
|
+
top?: string | number;
|
|
106
|
+
right?: string | number;
|
|
107
|
+
bottom?: string | number;
|
|
108
|
+
left?: string | number;
|
|
109
|
+
}
|
|
110
|
+
| string
|
|
111
|
+
| number;
|
|
112
|
+
|
|
113
|
+
export interface ToasterProps {
|
|
114
|
+
invert?: boolean;
|
|
115
|
+
theme?: "light" | "dark" | "system";
|
|
116
|
+
position?: Position;
|
|
117
|
+
hotkey?: string[];
|
|
118
|
+
richColors?: boolean;
|
|
119
|
+
expand?: boolean;
|
|
120
|
+
duration?: number;
|
|
121
|
+
gap?: number;
|
|
122
|
+
visibleToasts?: number;
|
|
123
|
+
closeButton?: boolean;
|
|
124
|
+
toastOptions?: ToastOptions;
|
|
125
|
+
className?: string;
|
|
126
|
+
style?: React.CSSProperties;
|
|
127
|
+
offset?: Offset;
|
|
128
|
+
mobileOffset?: Offset;
|
|
129
|
+
dir?: "rtl" | "ltr" | "auto";
|
|
130
|
+
swipeDirections?: SwipeDirection[];
|
|
131
|
+
/**
|
|
132
|
+
* @deprecated Please use the `icons` prop instead:
|
|
133
|
+
* ```jsx
|
|
134
|
+
* <Toaster
|
|
135
|
+
* icons={{ loading: <LoadingIcon /> }}
|
|
136
|
+
* />
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
loadingIcon?: React.ReactNode;
|
|
140
|
+
icons?: ToastIcons;
|
|
141
|
+
containerAriaLabel?: string;
|
|
142
|
+
pauseWhenPageIsHidden?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type SwipeDirection = "top" | "right" | "bottom" | "left";
|
|
146
|
+
|
|
147
|
+
export interface ToastProps {
|
|
148
|
+
toast: ToastT;
|
|
149
|
+
toasts: ToastT[];
|
|
150
|
+
index: number;
|
|
151
|
+
swipeDirections?: SwipeDirection[];
|
|
152
|
+
expanded: boolean;
|
|
153
|
+
invert: boolean;
|
|
154
|
+
heights: HeightT[];
|
|
155
|
+
setHeights: React.Dispatch<React.SetStateAction<HeightT[]>>;
|
|
156
|
+
removeToast: (toast: ToastT) => void;
|
|
157
|
+
gap?: number;
|
|
158
|
+
position: Position;
|
|
159
|
+
visibleToasts: number;
|
|
160
|
+
expandByDefault: boolean;
|
|
161
|
+
closeButton: boolean;
|
|
162
|
+
interacting: boolean;
|
|
163
|
+
style?: React.CSSProperties;
|
|
164
|
+
cancelButtonStyle?: React.CSSProperties;
|
|
165
|
+
actionButtonStyle?: React.CSSProperties;
|
|
166
|
+
duration?: number;
|
|
167
|
+
className?: string;
|
|
168
|
+
unstyled?: boolean;
|
|
169
|
+
descriptionClassName?: string;
|
|
170
|
+
loadingIcon?: React.ReactNode;
|
|
171
|
+
classNames?: ToastClassnames;
|
|
172
|
+
icons?: ToastIcons;
|
|
173
|
+
closeButtonAriaLabel?: string;
|
|
174
|
+
pauseWhenPageIsHidden: boolean;
|
|
175
|
+
defaultRichColors?: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export enum SwipeStateTypes {
|
|
179
|
+
SwipedOut = "SwipedOut",
|
|
180
|
+
SwipedBack = "SwipedBack",
|
|
181
|
+
NotSwiped = "NotSwiped"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type Theme = "light" | "dark";
|
|
185
|
+
|
|
186
|
+
export interface ToastToDismiss {
|
|
187
|
+
id: number | string;
|
|
188
|
+
dismiss: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type ExternalToast = Omit<ToastT, "id" | "type" | "title" | "jsx" | "delete" | "promise"> & {
|
|
192
|
+
id?: number | string;
|
|
193
|
+
};
|
package/src/react/toaster.tsx
CHANGED
|
@@ -1,238 +1,126 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { TOAST_STYLES } from "@/react/toast/styles";
|
|
3
4
|
import { defaultQueue } from "@/react/use-notifications";
|
|
5
|
+
import { toast as vendorToast } from "@/react/toast/state";
|
|
6
|
+
import { Toaster as VendorToaster } from "@/react/toast/index";
|
|
4
7
|
import type { Notification, Notifications } from "@/core/notifications";
|
|
5
|
-
import {
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, type ReactNode } from "react";
|
|
9
|
+
import type { ToasterProps as VendorToasterProps } from "@/react/toast/types";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
* `<Toaster />` - the stack, the
|
|
12
|
+
* `<Toaster />` - the toast stack, and the one component in this package that ships a look.
|
|
9
13
|
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* The implementation under `react/toast/` is vendored and deliberately unedited: it is the
|
|
15
|
+
* toast these projects already use, and a second one that merely resembled it would be a
|
|
16
|
+
* different thing on every screen. Two mechanical changes were unavoidable to make it a
|
|
17
|
+
* library - the `cn` helper became a local file rather than a path alias, and the
|
|
18
|
+
* `import "./styles.css"` became the injection below, because a package cannot assume its
|
|
19
|
+
* consumer has a CSS-capable bundler.
|
|
14
20
|
*
|
|
15
|
-
*
|
|
16
|
-
* toast
|
|
21
|
+
* WHY IT SHIPS STYLES. Everywhere else here a missing stylesheet gives you an unstyled
|
|
22
|
+
* component you can see and fix. A toast renders at the edge of the screen, stacked and
|
|
23
|
+
* animated, so the same omission gives you a pile of text in a corner - and "remember to
|
|
24
|
+
* import the CSS" is a footgun for something that appears once every few minutes. The sheet
|
|
25
|
+
* is injected once and PREPENDED to `<head>`, so anything the document already has outranks
|
|
26
|
+
* it by source order without a single `!important`; `styles={false}` turns it off, and
|
|
27
|
+
* `@enigmax/primitives/toast.css` is the same sheet for anyone who prefers the import.
|
|
17
28
|
*/
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
| "top-left" | "top-center" | "top-right"
|
|
21
|
-
| "bottom-left" | "bottom-center" | "bottom-right";
|
|
30
|
+
let injected = false;
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*/
|
|
32
|
-
exitDuration?: number;
|
|
33
|
-
/** Px a toast must travel before the release dismisses it. */
|
|
34
|
-
swipeThreshold?: number;
|
|
35
|
-
/** Render a toast yourself. The default renders title, body and the action. */
|
|
36
|
-
children?: (notification: Notification, controls: ToastControls) => ReactNode;
|
|
37
|
-
/** Accessible name for the region. */
|
|
38
|
-
label?: string;
|
|
39
|
-
className?: string;
|
|
40
|
-
style?: CSSProperties;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ToastControls {
|
|
44
|
-
dismiss: () => void;
|
|
32
|
+
function injectStyles(): void {
|
|
33
|
+
if (injected || typeof document === "undefined") return;
|
|
34
|
+
injected = true;
|
|
35
|
+
if (document.querySelector("[data-enigma-toast-styles]")) return;
|
|
36
|
+
const element = document.createElement("style");
|
|
37
|
+
element.setAttribute("data-enigma-toast-styles", "");
|
|
38
|
+
element.textContent = TOAST_STYLES;
|
|
39
|
+
document.head.prepend(element);
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
interface
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
export interface ToasterProps extends VendorToasterProps {
|
|
43
|
+
/**
|
|
44
|
+
* Inject the stylesheet. On by default - see the note above. `false` leaves the markup
|
|
45
|
+
* bare for a theme of your own.
|
|
46
|
+
*/
|
|
47
|
+
styles?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* A notification QUEUE to render, alongside anything raised with `toast()`.
|
|
50
|
+
*
|
|
51
|
+
* This is the bridge for `useNotifications().notify()`, which predates the vendored
|
|
52
|
+
* component: everything already written against the queue keeps working, and both APIs
|
|
53
|
+
* end up in one stack rather than in two competing ones. `null` unsubscribes.
|
|
54
|
+
*/
|
|
55
|
+
queue?: Notifications | null;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return { axis: "x", sign: position.endsWith("-left") ? -1 : 1 };
|
|
57
|
-
}
|
|
58
|
+
export function Toaster({ styles = true, queue = defaultQueue, ...props }: ToasterProps): ReactNode {
|
|
59
|
+
// Before paint: a sheet applied after the first frame shows the toast unstyled first.
|
|
60
|
+
useLayoutEffect(() => { if (styles) injectStyles(); }, [styles]);
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
position = "bottom-right",
|
|
62
|
-
exitDuration = 200,
|
|
63
|
-
swipeThreshold = 60,
|
|
64
|
-
children,
|
|
65
|
-
label = "Notifications",
|
|
66
|
-
className,
|
|
67
|
-
style
|
|
68
|
-
}: ToasterProps): ReactNode {
|
|
69
|
-
// Seeded from the queue rather than empty: a notify() that happened before this mounted
|
|
70
|
-
// - during a redirect, or from a module that runs early - would otherwise be invisible
|
|
71
|
-
// for a frame. On a server the queue is empty, so this still renders nothing there.
|
|
72
|
-
const [rendered, setRendered] = useState<Rendered[]>(
|
|
73
|
-
() => queue.items.map((notification) => ({ notification, leaving: false }))
|
|
74
|
-
);
|
|
75
|
-
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
62
|
+
/** Queue id -> the toast raised for it, so a dismissal reaches the right one. */
|
|
63
|
+
const forwarded = useRef(new Map<string, string | number>());
|
|
76
64
|
|
|
77
65
|
useEffect(() => {
|
|
66
|
+
if (!queue) return;
|
|
78
67
|
const sync = (items: readonly Notification[]): void => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return { ...entry, leaving: true };
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const seen = new Set(kept.map((entry) => entry.notification.id));
|
|
97
|
-
const added = items.filter((item) => !seen.has(item.id)).map((item) => ({ notification: item, leaving: false }));
|
|
98
|
-
return [...kept, ...added];
|
|
99
|
-
});
|
|
68
|
+
const live = new Set(items.map((item) => item.id));
|
|
69
|
+
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (forwarded.current.has(item.id)) continue;
|
|
72
|
+
forwarded.current.set(item.id, raise(item));
|
|
73
|
+
}
|
|
74
|
+
// Dismissed in the queue is dismissed on screen: the queue owns its own timers,
|
|
75
|
+
// and a toast that outlived the item behind it would be showing a state that no
|
|
76
|
+
// longer exists.
|
|
77
|
+
for (const [id, toastId] of forwarded.current) {
|
|
78
|
+
if (live.has(id)) continue;
|
|
79
|
+
vendorToast.dismiss(toastId);
|
|
80
|
+
forwarded.current.delete(id);
|
|
81
|
+
}
|
|
100
82
|
};
|
|
101
83
|
|
|
102
84
|
sync(queue.items);
|
|
103
85
|
return queue.subscribe(sync);
|
|
104
|
-
}, [queue
|
|
86
|
+
}, [queue]);
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
// every render would cancel the exit that is currently running.
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
const pending = timers.current;
|
|
110
|
-
return () => {
|
|
111
|
-
for (const timer of pending.values()) clearTimeout(timer);
|
|
112
|
-
pending.clear();
|
|
113
|
-
};
|
|
114
|
-
}, []);
|
|
115
|
-
|
|
116
|
-
if (!rendered.length) return null;
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<section
|
|
120
|
-
aria-label={label}
|
|
121
|
-
data-enigma-toaster=""
|
|
122
|
-
data-position={position}
|
|
123
|
-
className={className}
|
|
124
|
-
style={style}
|
|
125
|
-
// A pointer resting on the stack stops every clock, so a message cannot expire
|
|
126
|
-
// while it is being read. Focus counts too, for anyone using a keyboard.
|
|
127
|
-
onPointerEnter={() => queue.pause()}
|
|
128
|
-
onPointerLeave={() => queue.resume()}
|
|
129
|
-
onFocusCapture={() => queue.pause()}
|
|
130
|
-
onBlurCapture={() => queue.resume()}
|
|
131
|
-
>
|
|
132
|
-
{rendered.map((entry, index) => (
|
|
133
|
-
<Toast
|
|
134
|
-
key={entry.notification.id}
|
|
135
|
-
entry={entry}
|
|
136
|
-
index={rendered.length - 1 - index}
|
|
137
|
-
position={position}
|
|
138
|
-
swipeThreshold={swipeThreshold}
|
|
139
|
-
onDismiss={() => queue.dismiss(entry.notification.id)}
|
|
140
|
-
render={children}
|
|
141
|
-
/>
|
|
142
|
-
))}
|
|
143
|
-
</section>
|
|
144
|
-
);
|
|
88
|
+
return <VendorToaster {...props} />;
|
|
145
89
|
}
|
|
146
90
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
91
|
+
/** One queue item, as the vendored toast API expects it. */
|
|
92
|
+
function raise(item: Notification): string | number {
|
|
93
|
+
const options = {
|
|
94
|
+
description: item.body,
|
|
95
|
+
// The queue already counts down and removes the item; leaving the toast's own timer
|
|
96
|
+
// running as well would race it, and the shorter of the two would win at random.
|
|
97
|
+
duration: Infinity,
|
|
98
|
+
action: item.action
|
|
99
|
+
? { label: item.action.label, onClick: () => item.action?.onSelect() }
|
|
100
|
+
: undefined
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
switch (item.tone) {
|
|
104
|
+
case "success": return vendorToast.success(item.title, options);
|
|
105
|
+
case "error": return vendorToast.error(item.title, options);
|
|
106
|
+
case "warning": return vendorToast.warning(item.title, options);
|
|
107
|
+
case "info": return vendorToast.info(item.title, options);
|
|
108
|
+
case "loading": return vendorToast.loading(item.title, options);
|
|
109
|
+
default: return vendorToast(item.title, options);
|
|
110
|
+
}
|
|
155
111
|
}
|
|
156
112
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const start = useRef(0);
|
|
162
|
-
const { axis, sign } = swipeAxis(position);
|
|
163
|
-
|
|
164
|
-
const onPointerDown = useCallback((event: React.PointerEvent<HTMLElement>) => {
|
|
165
|
-
// Not on the buttons: a press on "Undo" is a press, not the start of a drag.
|
|
166
|
-
if ((event.target as HTMLElement).closest("button")) return;
|
|
167
|
-
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
168
|
-
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
169
|
-
start.current = axis === "x" ? event.clientX : event.clientY;
|
|
170
|
-
setSwiping(true);
|
|
171
|
-
}, [axis]);
|
|
172
|
-
|
|
173
|
-
const onPointerMove = useCallback((event: React.PointerEvent<HTMLElement>) => {
|
|
174
|
-
if (!swiping) return;
|
|
175
|
-
const delta = (axis === "x" ? event.clientX : event.clientY) - start.current;
|
|
176
|
-
// Only towards the edge it is pinned to. Dragging the other way does nothing, so a
|
|
177
|
-
// scroll or a misread gesture cannot pull the toast into the middle of the page.
|
|
178
|
-
setOffset(sign > 0 ? Math.max(0, delta) : Math.min(0, delta));
|
|
179
|
-
}, [swiping, axis, sign]);
|
|
180
|
-
|
|
181
|
-
const onPointerUp = useCallback(() => {
|
|
182
|
-
if (!swiping) return;
|
|
183
|
-
setSwiping(false);
|
|
184
|
-
if (Math.abs(offset) >= swipeThreshold) {
|
|
185
|
-
onDismiss();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
setOffset(0); // Under the threshold it springs back, so nothing is lost by accident.
|
|
189
|
-
}, [swiping, offset, swipeThreshold, onDismiss]);
|
|
113
|
+
/** The vendored API, which is the direct way to raise one. */
|
|
114
|
+
export { toast } from "@/react/toast/state";
|
|
115
|
+
export { useSonner } from "@/react/toast/index";
|
|
116
|
+
export type { ToastT, ExternalToast, ToastClassnames, Action } from "@/react/toast/types";
|
|
190
117
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
data-swiping={swiping ? "" : undefined}
|
|
201
|
-
// An error interrupts; anything else waits its turn rather than talking over
|
|
202
|
-
// whatever a screen reader is in the middle of.
|
|
203
|
-
role={notification.tone === "error" ? "alert" : "status"}
|
|
204
|
-
aria-live={notification.tone === "error" ? "assertive" : "polite"}
|
|
205
|
-
tabIndex={0}
|
|
206
|
-
onKeyDown={(event) => { if (event.key === "Escape") onDismiss(); }}
|
|
207
|
-
onPointerDown={onPointerDown}
|
|
208
|
-
onPointerMove={onPointerMove}
|
|
209
|
-
onPointerUp={onPointerUp}
|
|
210
|
-
onPointerCancel={onPointerUp}
|
|
211
|
-
style={{
|
|
212
|
-
// Read by the theme, so the transform stays the stylesheet's decision.
|
|
213
|
-
"--enigma-toast-swipe": `${offset}px`,
|
|
214
|
-
"--enigma-toast-index": index
|
|
215
|
-
} as CSSProperties}
|
|
216
|
-
>
|
|
217
|
-
{render
|
|
218
|
-
? render(notification, controls)
|
|
219
|
-
: (
|
|
220
|
-
<>
|
|
221
|
-
<p data-enigma-toast-title="">{notification.title}</p>
|
|
222
|
-
{notification.body && <p data-enigma-toast-body="">{notification.body}</p>}
|
|
223
|
-
{notification.action && (
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
data-enigma-toast-action=""
|
|
227
|
-
onClick={() => {
|
|
228
|
-
notification.action?.onSelect();
|
|
229
|
-
if (notification.action?.dismiss !== false) onDismiss();
|
|
230
|
-
}}
|
|
231
|
-
>{notification.action.label}</button>
|
|
232
|
-
)}
|
|
233
|
-
<button type="button" data-enigma-toast-close="" aria-label="Dismiss" onClick={onDismiss}>×</button>
|
|
234
|
-
</>
|
|
235
|
-
)}
|
|
236
|
-
</article>
|
|
237
|
-
);
|
|
238
|
-
}
|
|
118
|
+
/**
|
|
119
|
+
* The queue, from the entry that renders it.
|
|
120
|
+
*
|
|
121
|
+
* `@enigmax/primitives/react/toast` is where somebody looks for both halves - mounting the
|
|
122
|
+
* stack and telling it something - and sending them to a second subpath for the second half
|
|
123
|
+
* is the kind of split that makes an import line something to look up.
|
|
124
|
+
*/
|
|
125
|
+
export { useNotifications, createNotificationQueue, defaultQueue } from "@/react/use-notifications";
|
|
126
|
+
export type { Notification, NotificationInput, NotificationTone, NotificationAction, Notifications } from "@/core/notifications";
|