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.
@@ -0,0 +1,343 @@
1
+ import React, { useEffect, useRef, useState, useCallback } from 'react';
2
+ import {
3
+ SuccessIcon,
4
+ ErrorIcon,
5
+ WarningIcon,
6
+ InfoIcon,
7
+ LoadingIcon,
8
+ CloseIcon,
9
+ } from './icons';
10
+
11
+ /**
12
+ * ToastItem Component
13
+ * Renders an individual notification card with animations, timers, actions, and swipe to accept/cancel.
14
+ */
15
+ export const ToastItem = ({
16
+ id,
17
+ type = 'default',
18
+ title,
19
+ message,
20
+ duration = 3000,
21
+ showProgressBar = true,
22
+ pauseOnHover = true,
23
+ closeButton = true,
24
+ icon,
25
+ action,
26
+ onClose,
27
+ onAccept,
28
+ onCancel,
29
+ acceptLabel,
30
+ cancelLabel = 'Dismiss',
31
+ swipeable = true,
32
+ swipeThreshold = 70,
33
+ theme = 'light',
34
+ className = '',
35
+ style = {},
36
+ onDismiss,
37
+ }) => {
38
+ const [isExiting, setIsExiting] = useState(false);
39
+ const [exitDirection, setExitDirection] = useState(null); // 'left' | 'right' | null
40
+ const [isPaused, setIsPaused] = useState(false);
41
+
42
+ // Swipe state
43
+ const [isDragging, setIsDragging] = useState(false);
44
+ const [dragOffset, setDragOffset] = useState(0);
45
+ const startXRef = useRef(0);
46
+ const isPointerDownRef = useRef(false);
47
+
48
+ // Ref to track remaining duration when paused
49
+ const remainingTimeRef = useRef(duration);
50
+ const startTimeRef = useRef(null);
51
+ const timerRef = useRef(null);
52
+
53
+ // Check if this toast supports an "accept" action
54
+ const hasAcceptAction = Boolean(onAccept || (action && action.onClick));
55
+ const resolvedAcceptLabel = acceptLabel || (action && action.label) || 'Accept';
56
+
57
+ // Trigger graceful exit animation before removal
58
+ const handleDismiss = useCallback((direction = null) => {
59
+ if (isExiting) return;
60
+ setIsExiting(true);
61
+ if (direction) {
62
+ setExitDirection(direction);
63
+ }
64
+ if (onClose) {
65
+ onClose(id);
66
+ }
67
+ // Match the CSS exit animation duration
68
+ setTimeout(() => {
69
+ onDismiss(id);
70
+ }, 280);
71
+ }, [id, isExiting, onClose, onDismiss]);
72
+
73
+ // Trigger accept action
74
+ const handleAccept = useCallback(() => {
75
+ if (isExiting) return;
76
+ if (onAccept) {
77
+ onAccept(id);
78
+ } else if (action && action.onClick) {
79
+ action.onClick(id);
80
+ }
81
+ handleDismiss('right');
82
+ }, [action, handleDismiss, id, isExiting, onAccept]);
83
+
84
+ // Handle auto-dismiss timer
85
+ useEffect(() => {
86
+ if (duration === false || duration === Infinity || duration <= 0) {
87
+ return;
88
+ }
89
+
90
+ const startTimer = (time) => {
91
+ startTimeRef.current = Date.now();
92
+ timerRef.current = setTimeout(() => {
93
+ handleDismiss();
94
+ }, time);
95
+ };
96
+
97
+ if (!isPaused && !isDragging) {
98
+ startTimer(remainingTimeRef.current);
99
+ }
100
+
101
+ return () => {
102
+ if (timerRef.current) {
103
+ clearTimeout(timerRef.current);
104
+ }
105
+ };
106
+ }, [duration, isPaused, isDragging, handleDismiss]);
107
+
108
+ // Pause on mouse enter
109
+ const handleMouseEnter = () => {
110
+ if (!pauseOnHover || duration === false || duration === Infinity || duration <= 0) {
111
+ return;
112
+ }
113
+ if (timerRef.current) {
114
+ clearTimeout(timerRef.current);
115
+ const elapsed = Date.now() - startTimeRef.current;
116
+ remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed);
117
+ }
118
+ setIsPaused(true);
119
+ };
120
+
121
+ // Resume on mouse leave
122
+ const handleMouseLeave = () => {
123
+ if (!pauseOnHover || duration === false || duration === Infinity || duration <= 0 || isDragging) {
124
+ return;
125
+ }
126
+ setIsPaused(false);
127
+ };
128
+
129
+ // --- Pointer Swipe Handlers ---
130
+ const handlePointerDown = (e) => {
131
+ if (!swipeable || isExiting) return;
132
+ // Don't drag if clicking buttons or interactive elements
133
+ if (e.target.closest('button') || e.target.closest('a')) return;
134
+ if (e.button !== undefined && e.button !== 0) return; // Only primary mouse button
135
+
136
+ isPointerDownRef.current = true;
137
+ startXRef.current = e.clientX;
138
+ setIsDragging(true);
139
+
140
+ // Pause timer while swiping
141
+ if (timerRef.current) {
142
+ clearTimeout(timerRef.current);
143
+ const elapsed = Date.now() - (startTimeRef.current || Date.now());
144
+ remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed);
145
+ }
146
+
147
+ try {
148
+ e.currentTarget.setPointerCapture(e.pointerId);
149
+ } catch (err) {
150
+ // Ignore if setPointerCapture not supported
151
+ }
152
+ };
153
+
154
+ const handlePointerMove = (e) => {
155
+ if (!isPointerDownRef.current || !isDragging) return;
156
+ const currentX = e.clientX;
157
+ const diff = currentX - startXRef.current;
158
+ setDragOffset(diff);
159
+ };
160
+
161
+ const handlePointerUp = (e) => {
162
+ if (!isPointerDownRef.current) return;
163
+ isPointerDownRef.current = false;
164
+ setIsDragging(false);
165
+
166
+ try {
167
+ e.currentTarget.releasePointerCapture(e.pointerId);
168
+ } catch (err) {
169
+ // Ignore
170
+ }
171
+
172
+ // Check if swipe distance exceeded threshold
173
+ if (dragOffset >= swipeThreshold) {
174
+ // Swiped Right
175
+ if (hasAcceptAction) {
176
+ handleAccept();
177
+ } else {
178
+ if (onCancel) onCancel(id);
179
+ handleDismiss('right');
180
+ }
181
+ } else if (dragOffset <= -swipeThreshold) {
182
+ // Swiped Left
183
+ if (onCancel) onCancel(id);
184
+ handleDismiss('left');
185
+ } else {
186
+ // Reset back to center
187
+ setDragOffset(0);
188
+ setIsPaused(false);
189
+ }
190
+ };
191
+
192
+ const handlePointerCancel = () => {
193
+ isPointerDownRef.current = false;
194
+ setIsDragging(false);
195
+ setDragOffset(0);
196
+ setIsPaused(false);
197
+ };
198
+
199
+ // Resolve appropriate icon
200
+ const renderIcon = () => {
201
+ if (icon === false) return null;
202
+ if (icon && React.isValidElement(icon)) {
203
+ return <span className="easy-toast-custom-icon">{icon}</span>;
204
+ }
205
+
206
+ switch (type) {
207
+ case 'success':
208
+ return <SuccessIcon />;
209
+ case 'error':
210
+ return <ErrorIcon />;
211
+ case 'warning':
212
+ return <WarningIcon />;
213
+ case 'info':
214
+ return <InfoIcon />;
215
+ case 'loading':
216
+ return <LoadingIcon />;
217
+ default:
218
+ return null;
219
+ }
220
+ };
221
+
222
+ const hasIcon = icon !== false && (icon || type !== 'default');
223
+ const hasProgressBar = showProgressBar && duration && duration !== Infinity && duration > 0;
224
+
225
+ // Compute inline drag transform
226
+ const getDragStyle = () => {
227
+ if (isDragging) {
228
+ return {
229
+ transform: `translateX(${dragOffset}px)`,
230
+ transition: 'none',
231
+ opacity: Math.max(0.45, 1 - Math.abs(dragOffset) / 280),
232
+ };
233
+ }
234
+ if (exitDirection === 'left') {
235
+ return {
236
+ transform: 'translateX(-115%) scale(0.9)',
237
+ opacity: 0,
238
+ transition: 'transform 0.28s cubic-bezier(0.4, 0, 1, 1), opacity 0.28s ease',
239
+ };
240
+ }
241
+ if (exitDirection === 'right') {
242
+ return {
243
+ transform: 'translateX(115%) scale(0.9)',
244
+ opacity: 0,
245
+ transition: 'transform 0.28s cubic-bezier(0.4, 0, 1, 1), opacity 0.28s ease',
246
+ };
247
+ }
248
+ return {
249
+ transition: 'transform 0.24s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.24s ease',
250
+ };
251
+ };
252
+
253
+ // Visual cues during swipe
254
+ const isSwipingAccept = dragOffset > 25 && hasAcceptAction;
255
+ const isSwipingDismissRight = dragOffset > 25 && !hasAcceptAction;
256
+ const isSwipingCancel = dragOffset < -25;
257
+
258
+ return (
259
+ <div
260
+ role={type === 'error' ? 'alert' : 'status'}
261
+ aria-live={type === 'error' ? 'assertive' : 'polite'}
262
+ className={`easy-toast-item easy-toast--${type} easy-toast--theme-${theme} ${
263
+ isExiting ? 'easy-toast--exit' : 'easy-toast--enter'
264
+ } ${swipeable ? 'easy-toast--swipeable' : ''} ${
265
+ isDragging ? 'easy-toast--dragging' : ''
266
+ } ${isSwipingAccept ? 'easy-toast--swipe-accept' : ''} ${
267
+ isSwipingCancel || isSwipingDismissRight ? 'easy-toast--swipe-cancel' : ''
268
+ } ${className}`}
269
+ style={{
270
+ ...style,
271
+ ...getDragStyle(),
272
+ }}
273
+ onMouseEnter={handleMouseEnter}
274
+ onMouseLeave={handleMouseLeave}
275
+ onPointerDown={handlePointerDown}
276
+ onPointerMove={handlePointerMove}
277
+ onPointerUp={handlePointerUp}
278
+ onPointerCancel={handlePointerCancel}
279
+ >
280
+ {/* Swipe Badges/Hints */}
281
+ {isSwipingAccept && (
282
+ <div className="easy-toast-swipe-badge easy-toast-swipe-badge--accept">
283
+ ✓ {resolvedAcceptLabel}
284
+ </div>
285
+ )}
286
+
287
+ {(isSwipingCancel || isSwipingDismissRight) && (
288
+ <div className="easy-toast-swipe-badge easy-toast-swipe-badge--cancel">
289
+ ✕ {cancelLabel}
290
+ </div>
291
+ )}
292
+
293
+ <div className="easy-toast-content-wrapper">
294
+ {hasIcon && <div className="easy-toast-icon-container">{renderIcon()}</div>}
295
+
296
+ <div className="easy-toast-text-container">
297
+ {title && <div className="easy-toast-title">{title}</div>}
298
+ <div className="easy-toast-message">{message}</div>
299
+ </div>
300
+
301
+ {action && (
302
+ <div className="easy-toast-action-container">
303
+ <button
304
+ type="button"
305
+ className="easy-toast-action-button"
306
+ onClick={() => {
307
+ action.onClick?.(id);
308
+ if (action.dismissOnClick !== false) {
309
+ handleDismiss('right');
310
+ }
311
+ }}
312
+ >
313
+ {action.label}
314
+ </button>
315
+ </div>
316
+ )}
317
+
318
+ {closeButton && (
319
+ <button
320
+ type="button"
321
+ className="easy-toast-close-button"
322
+ aria-label="Close notification"
323
+ onClick={() => handleDismiss()}
324
+ >
325
+ <CloseIcon />
326
+ </button>
327
+ )}
328
+ </div>
329
+
330
+ {hasProgressBar && (
331
+ <div className="easy-toast-progress-bar-track">
332
+ <div
333
+ className="easy-toast-progress-bar"
334
+ style={{
335
+ animationDuration: `${duration}ms`,
336
+ animationPlayState: isPaused || isDragging ? 'paused' : 'running',
337
+ }}
338
+ />
339
+ </div>
340
+ )}
341
+ </div>
342
+ );
343
+ };
package/src/icons.jsx ADDED
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Success Icon SVG
5
+ */
6
+ export const SuccessIcon = ({ size = 20, className = '', ...props }) => (
7
+ <svg
8
+ width={size}
9
+ height={size}
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ className={`easy-toast-icon easy-toast-icon--success ${className}`}
17
+ aria-hidden="true"
18
+ {...props}
19
+ >
20
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
21
+ <polyline points="22 4 12 14.01 9 11.01" />
22
+ </svg>
23
+ );
24
+
25
+ /**
26
+ * Error Icon SVG
27
+ */
28
+ export const ErrorIcon = ({ size = 20, className = '', ...props }) => (
29
+ <svg
30
+ width={size}
31
+ height={size}
32
+ viewBox="0 0 24 24"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ strokeWidth="2"
36
+ strokeLinecap="round"
37
+ strokeLinejoin="round"
38
+ className={`easy-toast-icon easy-toast-icon--error ${className}`}
39
+ aria-hidden="true"
40
+ {...props}
41
+ >
42
+ <circle cx="12" cy="12" r="10" />
43
+ <line x1="15" y1="9" x2="9" y2="15" />
44
+ <line x1="9" y1="9" x2="15" y2="15" />
45
+ </svg>
46
+ );
47
+
48
+ /**
49
+ * Warning Icon SVG
50
+ */
51
+ export const WarningIcon = ({ size = 20, className = '', ...props }) => (
52
+ <svg
53
+ width={size}
54
+ height={size}
55
+ viewBox="0 0 24 24"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ strokeWidth="2"
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ className={`easy-toast-icon easy-toast-icon--warning ${className}`}
62
+ aria-hidden="true"
63
+ {...props}
64
+ >
65
+ <path 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" />
66
+ <line x1="12" y1="9" x2="12" y2="13" />
67
+ <line x1="12" y1="17" x2="12.01" y2="17" />
68
+ </svg>
69
+ );
70
+
71
+ /**
72
+ * Info Icon SVG
73
+ */
74
+ export const InfoIcon = ({ size = 20, className = '', ...props }) => (
75
+ <svg
76
+ width={size}
77
+ height={size}
78
+ viewBox="0 0 24 24"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth="2"
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ className={`easy-toast-icon easy-toast-icon--info ${className}`}
85
+ aria-hidden="true"
86
+ {...props}
87
+ >
88
+ <circle cx="12" cy="12" r="10" />
89
+ <line x1="12" y1="16" x2="12" y2="12" />
90
+ <line x1="12" y1="8" x2="12.01" y2="8" />
91
+ </svg>
92
+ );
93
+
94
+ /**
95
+ * Loading Spinner SVG
96
+ */
97
+ export const LoadingIcon = ({ size = 20, className = '', ...props }) => (
98
+ <svg
99
+ width={size}
100
+ height={size}
101
+ viewBox="0 0 24 24"
102
+ fill="none"
103
+ stroke="currentColor"
104
+ strokeWidth="2.5"
105
+ strokeLinecap="round"
106
+ strokeLinejoin="round"
107
+ className={`easy-toast-icon easy-toast-icon--loading ${className}`}
108
+ aria-hidden="true"
109
+ {...props}
110
+ >
111
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
112
+ </svg>
113
+ );
114
+
115
+ /**
116
+ * Close (Dismiss) Button Icon SVG
117
+ */
118
+ export const CloseIcon = ({ size = 16, className = '', ...props }) => (
119
+ <svg
120
+ width={size}
121
+ height={size}
122
+ viewBox="0 0 24 24"
123
+ fill="none"
124
+ stroke="currentColor"
125
+ strokeWidth="2"
126
+ strokeLinecap="round"
127
+ strokeLinejoin="round"
128
+ className={`easy-toast-close-icon ${className}`}
129
+ aria-hidden="true"
130
+ {...props}
131
+ >
132
+ <line x1="18" y1="6" x2="6" y2="18" />
133
+ <line x1="6" y1="6" x2="18" y2="18" />
134
+ </svg>
135
+ );
package/src/index.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ import * as React from 'react';
2
+
3
+ export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading';
4
+
5
+ export type ToastPosition =
6
+ | 'top-left'
7
+ | 'top-center'
8
+ | 'top-right'
9
+ | 'bottom-left'
10
+ | 'bottom-center'
11
+ | 'bottom-right';
12
+
13
+ export type ToastTheme = 'light' | 'dark' | 'colored';
14
+
15
+ export interface ToastAction {
16
+ label: string;
17
+ onClick: (id: string) => void;
18
+ dismissOnClick?: boolean;
19
+ }
20
+
21
+ export interface ToastOptions {
22
+ id?: string;
23
+ type?: ToastType;
24
+ title?: React.ReactNode;
25
+ duration?: number | false;
26
+ showProgressBar?: boolean;
27
+ pauseOnHover?: boolean;
28
+ closeButton?: boolean;
29
+ icon?: React.ReactNode | false;
30
+ action?: ToastAction;
31
+ position?: ToastPosition;
32
+ theme?: ToastTheme;
33
+ className?: string;
34
+ style?: React.CSSProperties;
35
+ onClose?: (id: string) => void;
36
+ swipeable?: boolean;
37
+ swipeThreshold?: number;
38
+ onAccept?: (id: string) => void;
39
+ onCancel?: (id: string) => void;
40
+ acceptLabel?: string;
41
+ cancelLabel?: string;
42
+ }
43
+
44
+ export interface ToastItemProps extends ToastOptions {
45
+ id: string;
46
+ message: React.ReactNode;
47
+ onDismiss: (id: string) => void;
48
+ }
49
+
50
+ export interface ToastContainerProps {
51
+ toasts?: ToastItemProps[];
52
+ onDismiss?: (id: string) => void;
53
+ position?: ToastPosition;
54
+ theme?: ToastTheme;
55
+ newestOnTop?: boolean;
56
+ pauseOnHover?: boolean;
57
+ showProgressBar?: boolean;
58
+ swipeable?: boolean;
59
+ swipeThreshold?: number;
60
+ limit?: number;
61
+ className?: string;
62
+ style?: React.CSSProperties;
63
+ }
64
+
65
+ export interface ToastProviderProps {
66
+ children: React.ReactNode;
67
+ position?: ToastPosition;
68
+ autoClose?: number | false;
69
+ pauseOnHover?: boolean;
70
+ showProgressBar?: boolean;
71
+ swipeable?: boolean;
72
+ swipeThreshold?: number;
73
+ theme?: ToastTheme;
74
+ limit?: number;
75
+ newestOnTop?: boolean;
76
+ containerClassName?: string;
77
+ containerStyle?: React.CSSProperties;
78
+ }
79
+
80
+ export interface ToastPromiseOptions {
81
+ loading?: React.ReactNode;
82
+ success?: React.ReactNode | ((data: any) => React.ReactNode);
83
+ error?: React.ReactNode | ((err: any) => React.ReactNode);
84
+ }
85
+
86
+ export interface ToastPromiseExtraOptions {
87
+ duration?: number;
88
+ showProgressBar?: boolean;
89
+ successOptions?: Partial<ToastOptions>;
90
+ errorOptions?: Partial<ToastOptions>;
91
+ }
92
+
93
+ export interface ToastMethods {
94
+ (message: React.ReactNode, options?: ToastOptions): string;
95
+ success: (message: React.ReactNode, options?: ToastOptions) => string;
96
+ error: (message: React.ReactNode, options?: ToastOptions) => string;
97
+ warning: (message: React.ReactNode, options?: ToastOptions) => string;
98
+ info: (message: React.ReactNode, options?: ToastOptions) => string;
99
+ loading: (message: React.ReactNode, options?: ToastOptions) => string;
100
+ promise: <T>(
101
+ promise: Promise<T>,
102
+ messages: ToastPromiseOptions,
103
+ options?: ToastPromiseExtraOptions
104
+ ) => Promise<T>;
105
+ dismiss: (id: string) => void;
106
+ dismissAll: () => void;
107
+ update: (id: string, options: Partial<ToastOptions> & { message?: React.ReactNode }) => void;
108
+ }
109
+
110
+ export declare const ToastContext: React.Context<any>;
111
+
112
+ export declare const ToastProvider: React.FC<ToastProviderProps>;
113
+
114
+ export declare const ToastContainer: React.FC<ToastContainerProps>;
115
+
116
+ export declare const ToastItem: React.FC<ToastItemProps>;
117
+
118
+ export declare const useToast: () => ToastMethods;
119
+
120
+ export declare const SuccessIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
121
+ export declare const ErrorIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
122
+ export declare const WarningIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
123
+ export declare const InfoIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
124
+ export declare const LoadingIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
125
+ export declare const CloseIcon: React.FC<React.SVGProps<SVGSVGElement> & { size?: number }>;
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import './toast.css';
2
+
3
+ export { ToastProvider, useToast, ToastContext } from './ToastContext';
4
+ export { ToastContainer } from './ToastContainer';
5
+ export { ToastItem } from './ToastItem';
6
+ export {
7
+ SuccessIcon,
8
+ ErrorIcon,
9
+ WarningIcon,
10
+ InfoIcon,
11
+ LoadingIcon,
12
+ CloseIcon,
13
+ } from './icons';