react-cool-toast
Version:
A lightweight, customizable toast notification library for React
508 lines (493 loc) • 23.6 kB
JavaScript
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
// Sound frequencies for different toast types
var soundFrequencies = {
success: [523.25, 659.25, 783.99], // C5, E5, G5 - Major chord
error: [220, 174.61], // A3, F3 - Dissonant
warning: [440, 554.37], // A4, C#5 - Attention grabbing
info: [523.25, 698.46], // C5, F5 - Neutral
loading: [523.25, 659.25], // C5, E5 - Gentle progression
notification: [659.25, 523.25], // E5, C5 - Pleasant
none: []
};
// Audio context for Web Audio API
var audioContext = null;
var getAudioContext = function () {
if (typeof window === 'undefined')
return null;
if (!audioContext) {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
catch (error) {
console.warn('Web Audio API not supported');
return null;
}
}
return audioContext;
};
// Play a tone with given frequency and duration
var playTone = function (frequency, duration, volume) {
var context = getAudioContext();
if (!context)
return;
var oscillator = context.createOscillator();
var gainNode = context.createGain();
oscillator.connect(gainNode);
gainNode.connect(context.destination);
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0, context.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, context.currentTime + 0.01);
gainNode.gain.linearRampToValueAtTime(0, context.currentTime + duration);
oscillator.start(context.currentTime);
oscillator.stop(context.currentTime + duration);
};
// Play sound sequence for different toast types
var playToastSound = function (sound) {
if (sound === false || sound === 'none')
return;
var soundType = 'notification';
if (typeof sound === 'string') {
soundType = sound;
}
var frequencies = soundFrequencies[soundType];
if (!frequencies || frequencies.length === 0)
return;
frequencies.forEach(function (frequency, index) {
setTimeout(function () {
playTone(frequency, 0.15, 0.05);
}, index * 100);
});
};
// Check if user has enabled sounds in their preferences
var shouldPlaySound = function (enableSounds) {
if (enableSounds === void 0) { enableSounds = true; }
if (!enableSounds)
return false;
// Check if user has reduced motion preference (often correlates with sound preference)
if (typeof window !== 'undefined' && window.matchMedia) {
try {
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion)
return false;
}
catch (error) {
// Ignore errors in test environment
console.warn('matchMedia not supported');
}
}
return true;
};
// Global toast functions that will be set by the provider
var addToast = null;
var removeToast = null;
var updateToast = null;
var clearToasts = null;
var enableSounds = true;
// Set the toast functions (called by ToastProvider)
var setToastFunctions = function (functions) {
var _a;
addToast = functions.addToast;
removeToast = functions.removeToast;
updateToast = functions.updateToast;
clearToasts = functions.clearToasts;
enableSounds = (_a = functions.enableSounds) !== null && _a !== void 0 ? _a : true;
};
// Main toast function
var toast = function (message, options) {
var _a, _b, _c, _d, _e;
if (options === void 0) { options = {}; }
if (!addToast) {
console.warn('Toast functions not initialized. Make sure to wrap your app with ToastProvider.');
return '';
}
// Play sound if enabled
if (options.sound !== false && shouldPlaySound(enableSounds)) {
var soundType = options.sound === true || options.sound === undefined
? options.type || 'info'
: options.sound;
playToastSound(soundType);
}
var id = addToast({
message: message,
type: options.type || 'info',
duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 4000,
position: options.position || 'top-right',
theme: options.theme,
icon: options.icon,
className: options.className,
style: options.style,
onClose: options.onClose,
onOpen: options.onOpen,
sound: options.sound,
dismissible: (_b = options.dismissible) !== null && _b !== void 0 ? _b : true,
swipeable: (_c = options.swipeable) !== null && _c !== void 0 ? _c : true,
showProgress: (_d = options.showProgress) !== null && _d !== void 0 ? _d : false,
actions: options.actions,
richContent: (_e = options.richContent) !== null && _e !== void 0 ? _e : false,
});
return id;
};
// Convenience methods
toast.success = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { type: 'success' }));
};
toast.error = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { type: 'error' }));
};
toast.warning = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { type: 'warning' }));
};
toast.info = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { type: 'info' }));
};
toast.loading = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { type: 'loading', duration: 0 })); // Loading toasts don't auto-dismiss
};
// Promise-based toast
toast.promise = function (promise, messages, options) {
if (options === void 0) { options = {}; }
var loadingId = toast.loading(messages.loading, options);
return promise
.then(function (data) {
var successMessage = typeof messages.success === 'function'
? messages.success(data)
: messages.success;
if (updateToast) {
updateToast(loadingId, {
type: 'success',
message: successMessage,
duration: 4000,
});
}
return data;
})
.catch(function (error) {
var errorMessage = typeof messages.error === 'function'
? messages.error(error)
: messages.error;
if (updateToast) {
updateToast(loadingId, {
type: 'error',
message: errorMessage,
duration: 4000,
});
}
throw error;
});
};
// Utility functions
toast.dismiss = function (id) {
if (removeToast) {
removeToast(id);
}
};
toast.dismissAll = function () {
if (clearToasts) {
clearToasts();
}
};
// Custom toast with full control
toast.custom = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, options);
};
// Toast with action buttons
toast.action = function (message, actions, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { actions: actions }));
};
// Rich content toast (HTML, images, etc.)
toast.rich = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { richContent: true }));
};
// Silent toast (no sound)
toast.silent = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { sound: false }));
};
// Toast with progress bar
toast.progress = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { showProgress: true }));
};
// Themed toasts
toast.glass = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { theme: 'glass' }));
};
toast.neon = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { theme: 'neon' }));
};
toast.minimal = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { theme: 'minimal' }));
};
toast.colorful = function (message, options) {
if (options === void 0) { options = {}; }
return toast(message, __assign(__assign({}, options), { theme: 'colorful' }));
};
var ToastContext = react.createContext(undefined);
var ToastProvider = function (_a) {
var children = _a.children, _b = _a.defaultPosition, defaultPosition = _b === void 0 ? 'top-right' : _b, _c = _a.maxToasts, maxToasts = _c === void 0 ? 5 : _c, _d = _a.enableSounds, enableSounds = _d === void 0 ? true : _d, defaultTheme = _a.defaultTheme;
var _e = react.useState([]), toasts = _e[0], setToasts = _e[1];
var removeToast = react.useCallback(function (id) {
setToasts(function (prev) { return prev.filter(function (toast) { return toast.id !== id; }); });
}, []);
var addToast = react.useCallback(function (toast) {
var _a, _b, _c, _d;
var id = Math.random().toString(36).substr(2, 9);
var newToast = __assign(__assign({}, toast), { id: id, createdAt: Date.now(), position: toast.position || defaultPosition, theme: toast.theme || defaultTheme, dismissible: (_a = toast.dismissible) !== null && _a !== void 0 ? _a : true, swipeable: (_b = toast.swipeable) !== null && _b !== void 0 ? _b : true, showProgress: (_c = toast.showProgress) !== null && _c !== void 0 ? _c : false, richContent: (_d = toast.richContent) !== null && _d !== void 0 ? _d : false });
setToasts(function (prev) {
// Limit number of toasts
var filtered = prev.filter(function (t) { return t.position === newToast.position; });
if (filtered.length >= maxToasts) {
// Remove oldest toast
var oldestId_1 = filtered[0].id;
setTimeout(function () { return removeToast(oldestId_1); }, 100);
}
return __spreadArray(__spreadArray([], prev, true), [newToast], false);
});
// Don't auto-remove if duration is 0 or if showProgress is true (handled by component)
if (newToast.duration > 0 && !newToast.showProgress) {
setTimeout(function () {
removeToast(id);
}, newToast.duration);
}
return id;
}, [defaultPosition, defaultTheme, maxToasts, removeToast]);
var updateToast = react.useCallback(function (id, updates) {
setToasts(function (prev) {
return prev.map(function (toast) {
return toast.id === id ? __assign(__assign({}, toast), updates) : toast;
});
});
}, []);
var clearToasts = react.useCallback(function () {
setToasts([]);
}, []);
var value = {
toasts: toasts,
addToast: addToast,
removeToast: removeToast,
updateToast: updateToast,
clearToasts: clearToasts,
};
// Set global toast functions
react.useEffect(function () {
setToastFunctions({
addToast: addToast,
removeToast: removeToast,
updateToast: updateToast,
clearToasts: clearToasts,
enableSounds: enableSounds,
});
}, [addToast, removeToast, updateToast, clearToasts, enableSounds]);
return (jsxRuntime.jsx(ToastContext.Provider, { value: value, children: children }));
};
var useToast = function () {
var context = react.useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
var getIcon = function (type) {
switch (type) {
case 'success':
return (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "M9 12l2 2 4-4" }), jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" })] }));
case 'error':
return (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }), jsxRuntime.jsx("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), jsxRuntime.jsx("line", { x1: "9", y1: "9", x2: "15", y2: "15" })] }));
case 'warning':
return (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("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" }), jsxRuntime.jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }), jsxRuntime.jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })] }));
case 'loading':
return (jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("path", { d: "M21 12a9 9 0 11-6.219-8.56" }) }));
case 'info':
default:
return (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }), jsxRuntime.jsx("line", { x1: "12", y1: "16", x2: "12", y2: "12" }), jsxRuntime.jsx("line", { x1: "12", y1: "8", x2: "12.01", y2: "8" })] }));
}
};
var ToastComponent = function (_a) {
var toast = _a.toast, onRemove = _a.onRemove; _a.onUpdate;
var _b = react.useState(false), isVisible = _b[0], setIsVisible = _b[1];
var _c = react.useState(false), isLeaving = _c[0], setIsLeaving = _c[1];
var _d = react.useState(100), progress = _d[0], setProgress = _d[1];
var _e = react.useState(false), isSwiping = _e[0], setIsSwiping = _e[1];
var _f = react.useState(0), swipeX = _f[0], setSwipeX = _f[1];
var toastRef = react.useRef(null);
var progressIntervalRef = react.useRef(null);
var startXRef = react.useRef(0);
var startTimeRef = react.useRef(0);
react.useEffect(function () {
// Trigger enter animation
var timer = setTimeout(function () { return setIsVisible(true); }, 10);
return function () { return clearTimeout(timer); };
}, []);
react.useEffect(function () {
if (toast.onOpen) {
toast.onOpen();
}
}, [toast]);
// Progress bar animation
react.useEffect(function () {
if (toast.showProgress && toast.duration > 0) {
var interval = 50; // Update every 50ms
var decrement_1 = (interval / toast.duration) * 100;
progressIntervalRef.current = setInterval(function () {
setProgress(function (prev) {
var newProgress = prev - decrement_1;
if (newProgress <= 0) {
handleClose();
return 0;
}
return newProgress;
});
}, interval);
return function () {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
};
}
}, [toast.showProgress, toast.duration]);
// Auto-dismiss timer
react.useEffect(function () {
if (toast.duration > 0 && !toast.showProgress) {
var timer_1 = setTimeout(function () {
handleClose();
}, toast.duration);
return function () { return clearTimeout(timer_1); };
}
}, [toast.duration, toast.showProgress]);
var handleClose = react.useCallback(function () {
setIsLeaving(true);
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
setTimeout(function () {
onRemove(toast.id);
if (toast.onClose) {
toast.onClose();
}
}, 300); // Match animation duration
}, [toast.id, toast.onClose, onRemove]);
// Swipe gesture handlers
var handleTouchStart = function (e) {
if (!toast.swipeable)
return;
startXRef.current = e.touches[0].clientX;
startTimeRef.current = Date.now();
setIsSwiping(true);
};
var handleTouchMove = function (e) {
if (!toast.swipeable || !isSwiping)
return;
var currentX = e.touches[0].clientX;
var diffX = currentX - startXRef.current;
setSwipeX(diffX);
};
var handleTouchEnd = function () {
if (!toast.swipeable || !isSwiping)
return;
var swipeThreshold = 100;
var timeThreshold = 300;
var swipeTime = Date.now() - startTimeRef.current;
if (Math.abs(swipeX) > swipeThreshold || (Math.abs(swipeX) > 50 && swipeTime < timeThreshold)) {
handleClose();
}
else {
setSwipeX(0);
}
setIsSwiping(false);
};
var handleClick = function () {
if (toast.type !== 'loading' && toast.dismissible !== false) {
handleClose();
}
};
var handleActionClick = function (action) {
action.onClick();
handleClose();
};
var icon = toast.icon || getIcon(toast.type);
// Build class names
var classNames = [
'cool-toast',
toast.type,
isVisible ? 'visible' : '',
isLeaving ? 'leaving' : '',
isSwiping ? 'swiping' : '',
Math.abs(swipeX) > 50 ? 'swipe-threshold' : '',
toast.theme ? "theme-".concat(toast.theme) : '',
toast.richContent ? 'rich-content' : '',
toast.className || ''
].filter(Boolean).join(' ');
var toastStyle = __assign(__assign({}, toast.style), { transform: isSwiping ? "translateX(".concat(swipeX, "px)") : undefined });
return (jsxRuntime.jsxs("div", { ref: toastRef, className: classNames, style: toastStyle, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, role: "alert", "aria-live": "polite", "aria-atomic": "true", "aria-describedby": "toast-".concat(toast.id, "-message"), children: [jsxRuntime.jsxs("div", { className: "cool-toast-content", children: [jsxRuntime.jsx("div", { className: "cool-toast-icon", children: icon }), jsxRuntime.jsx("div", { id: "toast-".concat(toast.id, "-message"), className: "cool-toast-message", dangerouslySetInnerHTML: toast.richContent ? { __html: toast.message } : undefined, children: !toast.richContent && toast.message }), toast.dismissible !== false && toast.type !== 'loading' && (jsxRuntime.jsx("button", { className: "cool-toast-close", onClick: function (e) {
e.stopPropagation();
handleClose();
}, "aria-label": "Close notification", children: jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) }))] }), toast.actions && toast.actions.length > 0 && (jsxRuntime.jsx("div", { className: "cool-toast-actions", children: toast.actions.map(function (action, index) { return (jsxRuntime.jsx("button", { className: "cool-toast-action ".concat(action.style || 'secondary'), onClick: function (e) {
e.stopPropagation();
handleActionClick(action);
}, children: action.label }, index)); }) })), toast.showProgress && toast.duration > 0 && (jsxRuntime.jsx("div", { className: "cool-toast-progress", style: { width: "".concat(progress, "%") }, role: "progressbar", "aria-valuenow": progress, "aria-valuemin": 0, "aria-valuemax": 100 }))] }));
};
var Toaster = function (_a) {
var _b = _a.position, position = _b === void 0 ? 'top-right' : _b, _c = _a.reverseOrder, reverseOrder = _c === void 0 ? false : _c, _d = _a.gutter, gutter = _d === void 0 ? 8 : _d, _e = _a.containerClassName, containerClassName = _e === void 0 ? '' : _e, _f = _a.containerStyle, containerStyle = _f === void 0 ? {} : _f, _g = _a.toastOptions, toastOptions = _g === void 0 ? {} : _g;
var _h = useToast(), toasts = _h.toasts, removeToast = _h.removeToast, updateToast = _h.updateToast;
var filteredToasts = toasts.filter(function (toast) { return toast.position === position; });
var sortedToasts = reverseOrder ? __spreadArray([], filteredToasts, true).reverse() : filteredToasts;
return (jsxRuntime.jsx("div", { className: "cool-toaster ".concat(containerClassName), "data-position": position, style: __assign({ '--gutter': "".concat(gutter, "px") }, containerStyle), children: sortedToasts.map(function (toast) { return (jsxRuntime.jsx(ToastComponent, { toast: __assign(__assign({}, toast), toastOptions), onRemove: removeToast, onUpdate: updateToast }, toast.id)); }) }));
};
exports.Toast = ToastComponent;
exports.ToastProvider = ToastProvider;
exports.Toaster = Toaster;
exports.playToastSound = playToastSound;
exports.shouldPlaySound = shouldPlaySound;
exports.toast = toast;
exports.useToast = useToast;
//# sourceMappingURL=index.js.map
;