flashpop 1.0.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/LICENSE +21 -0
- package/README.md +330 -0
- package/dist/index.cjs.js +807 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.esm.js +795 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/toast.css +2 -0
- package/dist/toast.css.map +1 -0
- package/package.json +73 -0
- package/src/ToastContainer.jsx +83 -0
- package/src/ToastContext.jsx +262 -0
- package/src/ToastItem.jsx +343 -0
- package/src/icons.jsx +135 -0
- package/src/index.d.ts +125 -0
- package/src/index.js +13 -0
- package/src/toast.css +586 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import React, { useState, useRef, useCallback, useEffect, createContext, useMemo, useContext } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
const SuccessIcon = ({
|
|
6
|
+
size = 20,
|
|
7
|
+
className = '',
|
|
8
|
+
...props
|
|
9
|
+
}) => /*#__PURE__*/jsxs("svg", {
|
|
10
|
+
width: size,
|
|
11
|
+
height: size,
|
|
12
|
+
viewBox: "0 0 24 24",
|
|
13
|
+
fill: "none",
|
|
14
|
+
stroke: "currentColor",
|
|
15
|
+
strokeWidth: "2",
|
|
16
|
+
strokeLinecap: "round",
|
|
17
|
+
strokeLinejoin: "round",
|
|
18
|
+
className: `easy-toast-icon easy-toast-icon--success ${className}`,
|
|
19
|
+
"aria-hidden": "true",
|
|
20
|
+
...props,
|
|
21
|
+
children: [/*#__PURE__*/jsx("path", {
|
|
22
|
+
d: "M22 11.08V12a10 10 0 1 1-5.93-9.14"
|
|
23
|
+
}), /*#__PURE__*/jsx("polyline", {
|
|
24
|
+
points: "22 4 12 14.01 9 11.01"
|
|
25
|
+
})]
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Error Icon SVG
|
|
30
|
+
*/
|
|
31
|
+
const ErrorIcon = ({
|
|
32
|
+
size = 20,
|
|
33
|
+
className = '',
|
|
34
|
+
...props
|
|
35
|
+
}) => /*#__PURE__*/jsxs("svg", {
|
|
36
|
+
width: size,
|
|
37
|
+
height: size,
|
|
38
|
+
viewBox: "0 0 24 24",
|
|
39
|
+
fill: "none",
|
|
40
|
+
stroke: "currentColor",
|
|
41
|
+
strokeWidth: "2",
|
|
42
|
+
strokeLinecap: "round",
|
|
43
|
+
strokeLinejoin: "round",
|
|
44
|
+
className: `easy-toast-icon easy-toast-icon--error ${className}`,
|
|
45
|
+
"aria-hidden": "true",
|
|
46
|
+
...props,
|
|
47
|
+
children: [/*#__PURE__*/jsx("circle", {
|
|
48
|
+
cx: "12",
|
|
49
|
+
cy: "12",
|
|
50
|
+
r: "10"
|
|
51
|
+
}), /*#__PURE__*/jsx("line", {
|
|
52
|
+
x1: "15",
|
|
53
|
+
y1: "9",
|
|
54
|
+
x2: "9",
|
|
55
|
+
y2: "15"
|
|
56
|
+
}), /*#__PURE__*/jsx("line", {
|
|
57
|
+
x1: "9",
|
|
58
|
+
y1: "9",
|
|
59
|
+
x2: "15",
|
|
60
|
+
y2: "15"
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Warning Icon SVG
|
|
66
|
+
*/
|
|
67
|
+
const WarningIcon = ({
|
|
68
|
+
size = 20,
|
|
69
|
+
className = '',
|
|
70
|
+
...props
|
|
71
|
+
}) => /*#__PURE__*/jsxs("svg", {
|
|
72
|
+
width: size,
|
|
73
|
+
height: size,
|
|
74
|
+
viewBox: "0 0 24 24",
|
|
75
|
+
fill: "none",
|
|
76
|
+
stroke: "currentColor",
|
|
77
|
+
strokeWidth: "2",
|
|
78
|
+
strokeLinecap: "round",
|
|
79
|
+
strokeLinejoin: "round",
|
|
80
|
+
className: `easy-toast-icon easy-toast-icon--warning ${className}`,
|
|
81
|
+
"aria-hidden": "true",
|
|
82
|
+
...props,
|
|
83
|
+
children: [/*#__PURE__*/jsx("path", {
|
|
84
|
+
d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
|
85
|
+
}), /*#__PURE__*/jsx("line", {
|
|
86
|
+
x1: "12",
|
|
87
|
+
y1: "9",
|
|
88
|
+
x2: "12",
|
|
89
|
+
y2: "13"
|
|
90
|
+
}), /*#__PURE__*/jsx("line", {
|
|
91
|
+
x1: "12",
|
|
92
|
+
y1: "17",
|
|
93
|
+
x2: "12.01",
|
|
94
|
+
y2: "17"
|
|
95
|
+
})]
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Info Icon SVG
|
|
100
|
+
*/
|
|
101
|
+
const InfoIcon = ({
|
|
102
|
+
size = 20,
|
|
103
|
+
className = '',
|
|
104
|
+
...props
|
|
105
|
+
}) => /*#__PURE__*/jsxs("svg", {
|
|
106
|
+
width: size,
|
|
107
|
+
height: size,
|
|
108
|
+
viewBox: "0 0 24 24",
|
|
109
|
+
fill: "none",
|
|
110
|
+
stroke: "currentColor",
|
|
111
|
+
strokeWidth: "2",
|
|
112
|
+
strokeLinecap: "round",
|
|
113
|
+
strokeLinejoin: "round",
|
|
114
|
+
className: `easy-toast-icon easy-toast-icon--info ${className}`,
|
|
115
|
+
"aria-hidden": "true",
|
|
116
|
+
...props,
|
|
117
|
+
children: [/*#__PURE__*/jsx("circle", {
|
|
118
|
+
cx: "12",
|
|
119
|
+
cy: "12",
|
|
120
|
+
r: "10"
|
|
121
|
+
}), /*#__PURE__*/jsx("line", {
|
|
122
|
+
x1: "12",
|
|
123
|
+
y1: "16",
|
|
124
|
+
x2: "12",
|
|
125
|
+
y2: "12"
|
|
126
|
+
}), /*#__PURE__*/jsx("line", {
|
|
127
|
+
x1: "12",
|
|
128
|
+
y1: "8",
|
|
129
|
+
x2: "12.01",
|
|
130
|
+
y2: "8"
|
|
131
|
+
})]
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Loading Spinner SVG
|
|
136
|
+
*/
|
|
137
|
+
const LoadingIcon = ({
|
|
138
|
+
size = 20,
|
|
139
|
+
className = '',
|
|
140
|
+
...props
|
|
141
|
+
}) => /*#__PURE__*/jsx("svg", {
|
|
142
|
+
width: size,
|
|
143
|
+
height: size,
|
|
144
|
+
viewBox: "0 0 24 24",
|
|
145
|
+
fill: "none",
|
|
146
|
+
stroke: "currentColor",
|
|
147
|
+
strokeWidth: "2.5",
|
|
148
|
+
strokeLinecap: "round",
|
|
149
|
+
strokeLinejoin: "round",
|
|
150
|
+
className: `easy-toast-icon easy-toast-icon--loading ${className}`,
|
|
151
|
+
"aria-hidden": "true",
|
|
152
|
+
...props,
|
|
153
|
+
children: /*#__PURE__*/jsx("path", {
|
|
154
|
+
d: "M21 12a9 9 0 1 1-6.219-8.56"
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Close (Dismiss) Button Icon SVG
|
|
160
|
+
*/
|
|
161
|
+
const CloseIcon = ({
|
|
162
|
+
size = 16,
|
|
163
|
+
className = '',
|
|
164
|
+
...props
|
|
165
|
+
}) => /*#__PURE__*/jsxs("svg", {
|
|
166
|
+
width: size,
|
|
167
|
+
height: size,
|
|
168
|
+
viewBox: "0 0 24 24",
|
|
169
|
+
fill: "none",
|
|
170
|
+
stroke: "currentColor",
|
|
171
|
+
strokeWidth: "2",
|
|
172
|
+
strokeLinecap: "round",
|
|
173
|
+
strokeLinejoin: "round",
|
|
174
|
+
className: `easy-toast-close-icon ${className}`,
|
|
175
|
+
"aria-hidden": "true",
|
|
176
|
+
...props,
|
|
177
|
+
children: [/*#__PURE__*/jsx("line", {
|
|
178
|
+
x1: "18",
|
|
179
|
+
y1: "6",
|
|
180
|
+
x2: "6",
|
|
181
|
+
y2: "18"
|
|
182
|
+
}), /*#__PURE__*/jsx("line", {
|
|
183
|
+
x1: "6",
|
|
184
|
+
y1: "6",
|
|
185
|
+
x2: "18",
|
|
186
|
+
y2: "18"
|
|
187
|
+
})]
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const ToastItem = ({
|
|
191
|
+
id,
|
|
192
|
+
type = 'default',
|
|
193
|
+
title,
|
|
194
|
+
message,
|
|
195
|
+
duration = 3000,
|
|
196
|
+
showProgressBar = true,
|
|
197
|
+
pauseOnHover = true,
|
|
198
|
+
closeButton = true,
|
|
199
|
+
icon,
|
|
200
|
+
action,
|
|
201
|
+
onClose,
|
|
202
|
+
onAccept,
|
|
203
|
+
onCancel,
|
|
204
|
+
acceptLabel,
|
|
205
|
+
cancelLabel = 'Dismiss',
|
|
206
|
+
swipeable = true,
|
|
207
|
+
swipeThreshold = 70,
|
|
208
|
+
theme = 'light',
|
|
209
|
+
className = '',
|
|
210
|
+
style = {},
|
|
211
|
+
onDismiss
|
|
212
|
+
}) => {
|
|
213
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
214
|
+
const [exitDirection, setExitDirection] = useState(null); // 'left' | 'right' | null
|
|
215
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
216
|
+
|
|
217
|
+
// Swipe state
|
|
218
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
219
|
+
const [dragOffset, setDragOffset] = useState(0);
|
|
220
|
+
const startXRef = useRef(0);
|
|
221
|
+
const isPointerDownRef = useRef(false);
|
|
222
|
+
|
|
223
|
+
// Ref to track remaining duration when paused
|
|
224
|
+
const remainingTimeRef = useRef(duration);
|
|
225
|
+
const startTimeRef = useRef(null);
|
|
226
|
+
const timerRef = useRef(null);
|
|
227
|
+
|
|
228
|
+
// Check if this toast supports an "accept" action
|
|
229
|
+
const hasAcceptAction = Boolean(onAccept || action && action.onClick);
|
|
230
|
+
const resolvedAcceptLabel = acceptLabel || action && action.label || 'Accept';
|
|
231
|
+
|
|
232
|
+
// Trigger graceful exit animation before removal
|
|
233
|
+
const handleDismiss = useCallback((direction = null) => {
|
|
234
|
+
if (isExiting) return;
|
|
235
|
+
setIsExiting(true);
|
|
236
|
+
if (direction) {
|
|
237
|
+
setExitDirection(direction);
|
|
238
|
+
}
|
|
239
|
+
if (onClose) {
|
|
240
|
+
onClose(id);
|
|
241
|
+
}
|
|
242
|
+
// Match the CSS exit animation duration
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
onDismiss(id);
|
|
245
|
+
}, 280);
|
|
246
|
+
}, [id, isExiting, onClose, onDismiss]);
|
|
247
|
+
|
|
248
|
+
// Trigger accept action
|
|
249
|
+
const handleAccept = useCallback(() => {
|
|
250
|
+
if (isExiting) return;
|
|
251
|
+
if (onAccept) {
|
|
252
|
+
onAccept(id);
|
|
253
|
+
} else if (action && action.onClick) {
|
|
254
|
+
action.onClick(id);
|
|
255
|
+
}
|
|
256
|
+
handleDismiss('right');
|
|
257
|
+
}, [action, handleDismiss, id, isExiting, onAccept]);
|
|
258
|
+
|
|
259
|
+
// Handle auto-dismiss timer
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (duration === false || duration === Infinity || duration <= 0) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const startTimer = time => {
|
|
265
|
+
startTimeRef.current = Date.now();
|
|
266
|
+
timerRef.current = setTimeout(() => {
|
|
267
|
+
handleDismiss();
|
|
268
|
+
}, time);
|
|
269
|
+
};
|
|
270
|
+
if (!isPaused && !isDragging) {
|
|
271
|
+
startTimer(remainingTimeRef.current);
|
|
272
|
+
}
|
|
273
|
+
return () => {
|
|
274
|
+
if (timerRef.current) {
|
|
275
|
+
clearTimeout(timerRef.current);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}, [duration, isPaused, isDragging, handleDismiss]);
|
|
279
|
+
|
|
280
|
+
// Pause on mouse enter
|
|
281
|
+
const handleMouseEnter = () => {
|
|
282
|
+
if (!pauseOnHover || duration === false || duration === Infinity || duration <= 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (timerRef.current) {
|
|
286
|
+
clearTimeout(timerRef.current);
|
|
287
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
288
|
+
remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed);
|
|
289
|
+
}
|
|
290
|
+
setIsPaused(true);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Resume on mouse leave
|
|
294
|
+
const handleMouseLeave = () => {
|
|
295
|
+
if (!pauseOnHover || duration === false || duration === Infinity || duration <= 0 || isDragging) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
setIsPaused(false);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// --- Pointer Swipe Handlers ---
|
|
302
|
+
const handlePointerDown = e => {
|
|
303
|
+
if (!swipeable || isExiting) return;
|
|
304
|
+
// Don't drag if clicking buttons or interactive elements
|
|
305
|
+
if (e.target.closest('button') || e.target.closest('a')) return;
|
|
306
|
+
if (e.button !== undefined && e.button !== 0) return; // Only primary mouse button
|
|
307
|
+
|
|
308
|
+
isPointerDownRef.current = true;
|
|
309
|
+
startXRef.current = e.clientX;
|
|
310
|
+
setIsDragging(true);
|
|
311
|
+
|
|
312
|
+
// Pause timer while swiping
|
|
313
|
+
if (timerRef.current) {
|
|
314
|
+
clearTimeout(timerRef.current);
|
|
315
|
+
const elapsed = Date.now() - (startTimeRef.current || Date.now());
|
|
316
|
+
remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
// Ignore if setPointerCapture not supported
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const handlePointerMove = e => {
|
|
325
|
+
if (!isPointerDownRef.current || !isDragging) return;
|
|
326
|
+
const currentX = e.clientX;
|
|
327
|
+
const diff = currentX - startXRef.current;
|
|
328
|
+
setDragOffset(diff);
|
|
329
|
+
};
|
|
330
|
+
const handlePointerUp = e => {
|
|
331
|
+
if (!isPointerDownRef.current) return;
|
|
332
|
+
isPointerDownRef.current = false;
|
|
333
|
+
setIsDragging(false);
|
|
334
|
+
try {
|
|
335
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
// Ignore
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check if swipe distance exceeded threshold
|
|
341
|
+
if (dragOffset >= swipeThreshold) {
|
|
342
|
+
// Swiped Right
|
|
343
|
+
if (hasAcceptAction) {
|
|
344
|
+
handleAccept();
|
|
345
|
+
} else {
|
|
346
|
+
if (onCancel) onCancel(id);
|
|
347
|
+
handleDismiss('right');
|
|
348
|
+
}
|
|
349
|
+
} else if (dragOffset <= -swipeThreshold) {
|
|
350
|
+
// Swiped Left
|
|
351
|
+
if (onCancel) onCancel(id);
|
|
352
|
+
handleDismiss('left');
|
|
353
|
+
} else {
|
|
354
|
+
// Reset back to center
|
|
355
|
+
setDragOffset(0);
|
|
356
|
+
setIsPaused(false);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const handlePointerCancel = () => {
|
|
360
|
+
isPointerDownRef.current = false;
|
|
361
|
+
setIsDragging(false);
|
|
362
|
+
setDragOffset(0);
|
|
363
|
+
setIsPaused(false);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Resolve appropriate icon
|
|
367
|
+
const renderIcon = () => {
|
|
368
|
+
if (icon === false) return null;
|
|
369
|
+
if (icon && /*#__PURE__*/React.isValidElement(icon)) {
|
|
370
|
+
return /*#__PURE__*/jsx("span", {
|
|
371
|
+
className: "easy-toast-custom-icon",
|
|
372
|
+
children: icon
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
switch (type) {
|
|
376
|
+
case 'success':
|
|
377
|
+
return /*#__PURE__*/jsx(SuccessIcon, {});
|
|
378
|
+
case 'error':
|
|
379
|
+
return /*#__PURE__*/jsx(ErrorIcon, {});
|
|
380
|
+
case 'warning':
|
|
381
|
+
return /*#__PURE__*/jsx(WarningIcon, {});
|
|
382
|
+
case 'info':
|
|
383
|
+
return /*#__PURE__*/jsx(InfoIcon, {});
|
|
384
|
+
case 'loading':
|
|
385
|
+
return /*#__PURE__*/jsx(LoadingIcon, {});
|
|
386
|
+
default:
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const hasIcon = icon !== false && (icon || type !== 'default');
|
|
391
|
+
const hasProgressBar = showProgressBar && duration && duration !== Infinity && duration > 0;
|
|
392
|
+
|
|
393
|
+
// Compute inline drag transform
|
|
394
|
+
const getDragStyle = () => {
|
|
395
|
+
if (isDragging) {
|
|
396
|
+
return {
|
|
397
|
+
transform: `translateX(${dragOffset}px)`,
|
|
398
|
+
transition: 'none',
|
|
399
|
+
opacity: Math.max(0.45, 1 - Math.abs(dragOffset) / 280)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (exitDirection === 'left') {
|
|
403
|
+
return {
|
|
404
|
+
transform: 'translateX(-115%) scale(0.9)',
|
|
405
|
+
opacity: 0,
|
|
406
|
+
transition: 'transform 0.28s cubic-bezier(0.4, 0, 1, 1), opacity 0.28s ease'
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (exitDirection === 'right') {
|
|
410
|
+
return {
|
|
411
|
+
transform: 'translateX(115%) scale(0.9)',
|
|
412
|
+
opacity: 0,
|
|
413
|
+
transition: 'transform 0.28s cubic-bezier(0.4, 0, 1, 1), opacity 0.28s ease'
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
transition: 'transform 0.24s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.24s ease'
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Visual cues during swipe
|
|
422
|
+
const isSwipingAccept = dragOffset > 25 && hasAcceptAction;
|
|
423
|
+
const isSwipingDismissRight = dragOffset > 25 && !hasAcceptAction;
|
|
424
|
+
const isSwipingCancel = dragOffset < -25;
|
|
425
|
+
return /*#__PURE__*/jsxs("div", {
|
|
426
|
+
role: type === 'error' ? 'alert' : 'status',
|
|
427
|
+
"aria-live": type === 'error' ? 'assertive' : 'polite',
|
|
428
|
+
className: `easy-toast-item easy-toast--${type} easy-toast--theme-${theme} ${isExiting ? 'easy-toast--exit' : 'easy-toast--enter'} ${swipeable ? 'easy-toast--swipeable' : ''} ${isDragging ? 'easy-toast--dragging' : ''} ${isSwipingAccept ? 'easy-toast--swipe-accept' : ''} ${isSwipingCancel || isSwipingDismissRight ? 'easy-toast--swipe-cancel' : ''} ${className}`,
|
|
429
|
+
style: {
|
|
430
|
+
...style,
|
|
431
|
+
...getDragStyle()
|
|
432
|
+
},
|
|
433
|
+
onMouseEnter: handleMouseEnter,
|
|
434
|
+
onMouseLeave: handleMouseLeave,
|
|
435
|
+
onPointerDown: handlePointerDown,
|
|
436
|
+
onPointerMove: handlePointerMove,
|
|
437
|
+
onPointerUp: handlePointerUp,
|
|
438
|
+
onPointerCancel: handlePointerCancel,
|
|
439
|
+
children: [isSwipingAccept && /*#__PURE__*/jsxs("div", {
|
|
440
|
+
className: "easy-toast-swipe-badge easy-toast-swipe-badge--accept",
|
|
441
|
+
children: ["\u2713 ", resolvedAcceptLabel]
|
|
442
|
+
}), (isSwipingCancel || isSwipingDismissRight) && /*#__PURE__*/jsxs("div", {
|
|
443
|
+
className: "easy-toast-swipe-badge easy-toast-swipe-badge--cancel",
|
|
444
|
+
children: ["\u2715 ", cancelLabel]
|
|
445
|
+
}), /*#__PURE__*/jsxs("div", {
|
|
446
|
+
className: "easy-toast-content-wrapper",
|
|
447
|
+
children: [hasIcon && /*#__PURE__*/jsx("div", {
|
|
448
|
+
className: "easy-toast-icon-container",
|
|
449
|
+
children: renderIcon()
|
|
450
|
+
}), /*#__PURE__*/jsxs("div", {
|
|
451
|
+
className: "easy-toast-text-container",
|
|
452
|
+
children: [title && /*#__PURE__*/jsx("div", {
|
|
453
|
+
className: "easy-toast-title",
|
|
454
|
+
children: title
|
|
455
|
+
}), /*#__PURE__*/jsx("div", {
|
|
456
|
+
className: "easy-toast-message",
|
|
457
|
+
children: message
|
|
458
|
+
})]
|
|
459
|
+
}), action && /*#__PURE__*/jsx("div", {
|
|
460
|
+
className: "easy-toast-action-container",
|
|
461
|
+
children: /*#__PURE__*/jsx("button", {
|
|
462
|
+
type: "button",
|
|
463
|
+
className: "easy-toast-action-button",
|
|
464
|
+
onClick: () => {
|
|
465
|
+
action.onClick?.(id);
|
|
466
|
+
if (action.dismissOnClick !== false) {
|
|
467
|
+
handleDismiss('right');
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
children: action.label
|
|
471
|
+
})
|
|
472
|
+
}), closeButton && /*#__PURE__*/jsx("button", {
|
|
473
|
+
type: "button",
|
|
474
|
+
className: "easy-toast-close-button",
|
|
475
|
+
"aria-label": "Close notification",
|
|
476
|
+
onClick: () => handleDismiss(),
|
|
477
|
+
children: /*#__PURE__*/jsx(CloseIcon, {})
|
|
478
|
+
})]
|
|
479
|
+
}), hasProgressBar && /*#__PURE__*/jsx("div", {
|
|
480
|
+
className: "easy-toast-progress-bar-track",
|
|
481
|
+
children: /*#__PURE__*/jsx("div", {
|
|
482
|
+
className: "easy-toast-progress-bar",
|
|
483
|
+
style: {
|
|
484
|
+
animationDuration: `${duration}ms`,
|
|
485
|
+
animationPlayState: isPaused || isDragging ? 'paused' : 'running'
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
})]
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const VALID_POSITIONS = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'];
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* ToastContainer Component
|
|
496
|
+
* Renders toast groups in their respective screen positions via a React Portal.
|
|
497
|
+
*/
|
|
498
|
+
const ToastContainer = ({
|
|
499
|
+
toasts = [],
|
|
500
|
+
onDismiss,
|
|
501
|
+
position: defaultPosition = 'top-right',
|
|
502
|
+
theme: defaultTheme = 'light',
|
|
503
|
+
newestOnTop = false,
|
|
504
|
+
pauseOnHover = true,
|
|
505
|
+
showProgressBar = true,
|
|
506
|
+
swipeable = true,
|
|
507
|
+
swipeThreshold = 70,
|
|
508
|
+
limit,
|
|
509
|
+
className = '',
|
|
510
|
+
style = {}
|
|
511
|
+
}) => {
|
|
512
|
+
// Guard for SSR (Next.js, Gatsby, Remix)
|
|
513
|
+
const isMounted = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
514
|
+
if (!isMounted) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Filter toasts if limit is set
|
|
519
|
+
const visibleToasts = limit && limit > 0 ? toasts.slice(-limit) : toasts;
|
|
520
|
+
|
|
521
|
+
// Group toasts by position
|
|
522
|
+
const groupedToasts = visibleToasts.reduce((acc, toast) => {
|
|
523
|
+
const pos = toast.position || defaultPosition;
|
|
524
|
+
const validPos = VALID_POSITIONS.includes(pos) ? pos : 'top-right';
|
|
525
|
+
if (!acc[validPos]) {
|
|
526
|
+
acc[validPos] = [];
|
|
527
|
+
}
|
|
528
|
+
acc[validPos].push(toast);
|
|
529
|
+
return acc;
|
|
530
|
+
}, {});
|
|
531
|
+
const portalContent = /*#__PURE__*/jsx("div", {
|
|
532
|
+
className: `easy-toast-portal ${className}`,
|
|
533
|
+
style: style,
|
|
534
|
+
children: Object.entries(groupedToasts).map(([pos, items]) => {
|
|
535
|
+
const sortedItems = newestOnTop ? [...items].reverse() : items;
|
|
536
|
+
return /*#__PURE__*/jsx("div", {
|
|
537
|
+
className: `easy-toast-container easy-toast-container--${pos}`,
|
|
538
|
+
"aria-live": "polite",
|
|
539
|
+
children: sortedItems.map(toast => /*#__PURE__*/jsx(ToastItem, {
|
|
540
|
+
...toast,
|
|
541
|
+
pauseOnHover: toast.pauseOnHover ?? pauseOnHover,
|
|
542
|
+
showProgressBar: toast.showProgressBar ?? showProgressBar,
|
|
543
|
+
swipeable: toast.swipeable ?? swipeable,
|
|
544
|
+
swipeThreshold: toast.swipeThreshold ?? swipeThreshold,
|
|
545
|
+
theme: toast.theme || defaultTheme,
|
|
546
|
+
onDismiss: onDismiss
|
|
547
|
+
}, toast.id))
|
|
548
|
+
}, pos);
|
|
549
|
+
})
|
|
550
|
+
});
|
|
551
|
+
return /*#__PURE__*/createPortal(portalContent, document.body);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const ToastContext = /*#__PURE__*/createContext(null);
|
|
555
|
+
let toastCount = 0;
|
|
556
|
+
const generateId = () => {
|
|
557
|
+
toastCount += 1;
|
|
558
|
+
return `easy-toast-${Date.now()}-${toastCount}`;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* ToastProvider Component
|
|
563
|
+
* Wraps your application to provide toast functionality anywhere in the component tree.
|
|
564
|
+
*/
|
|
565
|
+
const ToastProvider = ({
|
|
566
|
+
children,
|
|
567
|
+
position = 'top-right',
|
|
568
|
+
autoClose = 3000,
|
|
569
|
+
pauseOnHover = true,
|
|
570
|
+
showProgressBar = true,
|
|
571
|
+
swipeable = true,
|
|
572
|
+
swipeThreshold = 70,
|
|
573
|
+
theme = 'light',
|
|
574
|
+
limit,
|
|
575
|
+
newestOnTop = false,
|
|
576
|
+
containerClassName = '',
|
|
577
|
+
containerStyle = {}
|
|
578
|
+
}) => {
|
|
579
|
+
const [toasts, setToasts] = useState([]);
|
|
580
|
+
const toastsRef = useRef(toasts);
|
|
581
|
+
toastsRef.current = toasts;
|
|
582
|
+
|
|
583
|
+
// Add a new toast
|
|
584
|
+
const addToast = useCallback((messageOrOptions, maybeOptions = {}) => {
|
|
585
|
+
let options = {};
|
|
586
|
+
if (typeof messageOrOptions === 'object' && ! /*#__PURE__*/React.isValidElement(messageOrOptions)) {
|
|
587
|
+
options = {
|
|
588
|
+
...messageOrOptions
|
|
589
|
+
};
|
|
590
|
+
} else {
|
|
591
|
+
options = {
|
|
592
|
+
...maybeOptions,
|
|
593
|
+
message: messageOrOptions
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const id = options.id || generateId();
|
|
597
|
+
const newToast = {
|
|
598
|
+
id,
|
|
599
|
+
type: options.type || 'default',
|
|
600
|
+
message: options.message,
|
|
601
|
+
title: options.title,
|
|
602
|
+
duration: options.duration !== undefined ? options.duration : autoClose,
|
|
603
|
+
showProgressBar: options.showProgressBar !== undefined ? options.showProgressBar : showProgressBar,
|
|
604
|
+
pauseOnHover: options.pauseOnHover !== undefined ? options.pauseOnHover : pauseOnHover,
|
|
605
|
+
swipeable: options.swipeable !== undefined ? options.swipeable : swipeable,
|
|
606
|
+
swipeThreshold: options.swipeThreshold !== undefined ? options.swipeThreshold : swipeThreshold,
|
|
607
|
+
closeButton: options.closeButton !== undefined ? options.closeButton : true,
|
|
608
|
+
icon: options.icon,
|
|
609
|
+
action: options.action,
|
|
610
|
+
onClose: options.onClose,
|
|
611
|
+
onAccept: options.onAccept,
|
|
612
|
+
onCancel: options.onCancel,
|
|
613
|
+
acceptLabel: options.acceptLabel,
|
|
614
|
+
cancelLabel: options.cancelLabel,
|
|
615
|
+
position: options.position || position,
|
|
616
|
+
theme: options.theme || theme,
|
|
617
|
+
className: options.className || '',
|
|
618
|
+
style: options.style || {}
|
|
619
|
+
};
|
|
620
|
+
setToasts(prev => {
|
|
621
|
+
// If updating an existing toast with same id
|
|
622
|
+
const existingIndex = prev.findIndex(t => t.id === id);
|
|
623
|
+
if (existingIndex > -1) {
|
|
624
|
+
const updated = [...prev];
|
|
625
|
+
updated[existingIndex] = {
|
|
626
|
+
...updated[existingIndex],
|
|
627
|
+
...newToast
|
|
628
|
+
};
|
|
629
|
+
return updated;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Apply limit if specified
|
|
633
|
+
if (limit && limit > 0 && prev.length >= limit) {
|
|
634
|
+
return [...prev.slice(prev.length - limit + 1), newToast];
|
|
635
|
+
}
|
|
636
|
+
return [...prev, newToast];
|
|
637
|
+
});
|
|
638
|
+
return id;
|
|
639
|
+
}, [autoClose, showProgressBar, pauseOnHover, position, theme, limit]);
|
|
640
|
+
|
|
641
|
+
// Update an existing toast (e.g. for promises)
|
|
642
|
+
const updateToast = useCallback((id, updatedOptions) => {
|
|
643
|
+
setToasts(prev => prev.map(t => {
|
|
644
|
+
if (t.id === id) {
|
|
645
|
+
return {
|
|
646
|
+
...t,
|
|
647
|
+
...updatedOptions,
|
|
648
|
+
id // preserve id
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return t;
|
|
652
|
+
}));
|
|
653
|
+
}, []);
|
|
654
|
+
|
|
655
|
+
// Dismiss a specific toast
|
|
656
|
+
const dismissToast = useCallback(id => {
|
|
657
|
+
setToasts(prev => prev.filter(t => t.id !== id));
|
|
658
|
+
}, []);
|
|
659
|
+
|
|
660
|
+
// Dismiss all toasts
|
|
661
|
+
const dismissAll = useCallback(() => {
|
|
662
|
+
setToasts([]);
|
|
663
|
+
}, []);
|
|
664
|
+
|
|
665
|
+
// Context value with core functions
|
|
666
|
+
const contextValue = useMemo(() => ({
|
|
667
|
+
addToast,
|
|
668
|
+
updateToast,
|
|
669
|
+
dismissToast,
|
|
670
|
+
dismissAll,
|
|
671
|
+
toasts
|
|
672
|
+
}), [addToast, updateToast, dismissToast, dismissAll, toasts]);
|
|
673
|
+
return /*#__PURE__*/jsxs(ToastContext.Provider, {
|
|
674
|
+
value: contextValue,
|
|
675
|
+
children: [children, /*#__PURE__*/jsx(ToastContainer, {
|
|
676
|
+
toasts: toasts,
|
|
677
|
+
onDismiss: dismissToast,
|
|
678
|
+
position: position,
|
|
679
|
+
theme: theme,
|
|
680
|
+
limit: limit,
|
|
681
|
+
newestOnTop: newestOnTop,
|
|
682
|
+
pauseOnHover: pauseOnHover,
|
|
683
|
+
showProgressBar: showProgressBar,
|
|
684
|
+
swipeable: swipeable,
|
|
685
|
+
swipeThreshold: swipeThreshold,
|
|
686
|
+
className: containerClassName,
|
|
687
|
+
style: containerStyle
|
|
688
|
+
})]
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Custom Hook: useToast
|
|
694
|
+
* Exposes an intuitive API to trigger and manage toast notifications.
|
|
695
|
+
*
|
|
696
|
+
* Example:
|
|
697
|
+
* const toast = useToast();
|
|
698
|
+
* toast.success('Profile updated!');
|
|
699
|
+
* toast.error('Something went wrong');
|
|
700
|
+
*/
|
|
701
|
+
const useToast = () => {
|
|
702
|
+
const context = useContext(ToastContext);
|
|
703
|
+
if (!context) {
|
|
704
|
+
throw new Error('useToast must be used within a <ToastProvider>. Please wrap your root component in <ToastProvider>.');
|
|
705
|
+
}
|
|
706
|
+
const {
|
|
707
|
+
addToast,
|
|
708
|
+
updateToast,
|
|
709
|
+
dismissToast,
|
|
710
|
+
dismissAll
|
|
711
|
+
} = context;
|
|
712
|
+
|
|
713
|
+
// Base callable toast function
|
|
714
|
+
const toast = useCallback((message, options) => {
|
|
715
|
+
return addToast(message, options);
|
|
716
|
+
}, [addToast]);
|
|
717
|
+
|
|
718
|
+
// Helper: toast.success
|
|
719
|
+
toast.success = useCallback((message, options = {}) => {
|
|
720
|
+
return addToast(message, {
|
|
721
|
+
...options,
|
|
722
|
+
type: 'success'
|
|
723
|
+
});
|
|
724
|
+
}, [addToast]);
|
|
725
|
+
|
|
726
|
+
// Helper: toast.error
|
|
727
|
+
toast.error = useCallback((message, options = {}) => {
|
|
728
|
+
return addToast(message, {
|
|
729
|
+
...options,
|
|
730
|
+
type: 'error'
|
|
731
|
+
});
|
|
732
|
+
}, [addToast]);
|
|
733
|
+
|
|
734
|
+
// Helper: toast.warning
|
|
735
|
+
toast.warning = useCallback((message, options = {}) => {
|
|
736
|
+
return addToast(message, {
|
|
737
|
+
...options,
|
|
738
|
+
type: 'warning'
|
|
739
|
+
});
|
|
740
|
+
}, [addToast]);
|
|
741
|
+
|
|
742
|
+
// Helper: toast.info
|
|
743
|
+
toast.info = useCallback((message, options = {}) => {
|
|
744
|
+
return addToast(message, {
|
|
745
|
+
...options,
|
|
746
|
+
type: 'info'
|
|
747
|
+
});
|
|
748
|
+
}, [addToast]);
|
|
749
|
+
|
|
750
|
+
// Helper: toast.loading
|
|
751
|
+
toast.loading = useCallback((message, options = {}) => {
|
|
752
|
+
return addToast(message, {
|
|
753
|
+
...options,
|
|
754
|
+
type: 'loading',
|
|
755
|
+
duration: false,
|
|
756
|
+
// does not auto-dismiss
|
|
757
|
+
showProgressBar: false
|
|
758
|
+
});
|
|
759
|
+
}, [addToast]);
|
|
760
|
+
|
|
761
|
+
// Helper: toast.promise
|
|
762
|
+
toast.promise = useCallback((promise, messages, options = {}) => {
|
|
763
|
+
const id = toast.loading(messages.loading || 'Loading...', options);
|
|
764
|
+
const p = promise instanceof Promise ? promise : Promise.resolve(promise);
|
|
765
|
+
p.then(result => {
|
|
766
|
+
const successMsg = typeof messages.success === 'function' ? messages.success(result) : messages.success || 'Success!';
|
|
767
|
+
updateToast(id, {
|
|
768
|
+
type: 'success',
|
|
769
|
+
message: successMsg,
|
|
770
|
+
duration: options.duration !== undefined ? options.duration : 3000,
|
|
771
|
+
showProgressBar: options.showProgressBar !== undefined ? options.showProgressBar : true,
|
|
772
|
+
...options.successOptions
|
|
773
|
+
});
|
|
774
|
+
}).catch(err => {
|
|
775
|
+
const errorMsg = typeof messages.error === 'function' ? messages.error(err) : messages.error || 'Something went wrong';
|
|
776
|
+
updateToast(id, {
|
|
777
|
+
type: 'error',
|
|
778
|
+
message: errorMsg,
|
|
779
|
+
duration: options.duration !== undefined ? options.duration : 4000,
|
|
780
|
+
showProgressBar: options.showProgressBar !== undefined ? options.showProgressBar : true,
|
|
781
|
+
...options.errorOptions
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
return p;
|
|
785
|
+
}, [toast, updateToast]);
|
|
786
|
+
|
|
787
|
+
// Programmatic dismiss
|
|
788
|
+
toast.dismiss = dismissToast;
|
|
789
|
+
toast.dismissAll = dismissAll;
|
|
790
|
+
toast.update = updateToast;
|
|
791
|
+
return toast;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
export { CloseIcon, ErrorIcon, InfoIcon, LoadingIcon, SuccessIcon, ToastContainer, ToastContext, ToastItem, ToastProvider, WarningIcon, useToast };
|
|
795
|
+
//# sourceMappingURL=index.esm.js.map
|