mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 04:38:11 +00:00
feat(ui): redesign toast with accent rail and action slot (#693)
* feat(ui): redesign toast with accent rail and action slot Align toast with the overlay-surface language from the user menu and notification panel: left accent rail, tracked-mono kicker above the message, lucide icons at 1.5px stroke, status-tinted progress bar. Extend the toast store with an optional options object so callers can pass an inline action button and override the default duration. Guard removeToast against no-op dismissals so subscribers are not notified when nothing changed, and cap the buffered queue at 20 to prevent unbounded growth under spam. * fix(ui): avoid impure Date.now in toast ref initializer useRef(Date.now()) re-evaluates the impure call on every render and trips the react-hooks/purity lint rule, even though only the first render's value is retained. Initialize to 0 since the auto-dismiss effect writes startRef.current = Date.now() before any read path consumes it.
This commit is contained in:
@@ -2,13 +2,27 @@ import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
|
||||
|
||||
export interface ToastAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface ToastOptions {
|
||||
action?: ToastAction;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
createdAt: number;
|
||||
action?: ToastAction;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const MAX_BUFFERED = 20;
|
||||
|
||||
let toasts: Toast[] = [];
|
||||
const listeners: Set<() => void> = new Set();
|
||||
let idCounter = 0;
|
||||
@@ -17,15 +31,28 @@ function notify() {
|
||||
listeners.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
function addToast(type: ToastType, message: string): string {
|
||||
function addToast(type: ToastType, message: string, opts?: ToastOptions): string {
|
||||
const id = `toast-${++idCounter}-${Date.now()}`;
|
||||
toasts = [...toasts, { id, type, message, createdAt: Date.now() }];
|
||||
const next: Toast[] = [
|
||||
...toasts,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
createdAt: Date.now(),
|
||||
action: opts?.action,
|
||||
duration: opts?.duration,
|
||||
},
|
||||
];
|
||||
toasts = next.length > MAX_BUFFERED ? next.slice(-MAX_BUFFERED) : next;
|
||||
notify();
|
||||
return id;
|
||||
}
|
||||
|
||||
export function removeToast(id: string) {
|
||||
toasts = toasts.filter((t) => t.id !== id);
|
||||
const next = toasts.filter((t) => t.id !== id);
|
||||
if (next.length === toasts.length) return;
|
||||
toasts = next;
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -45,10 +72,11 @@ export function useToasts() {
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
success: (message: string) => addToast('success', message),
|
||||
error: (message: string) => addToast('error', message),
|
||||
warning: (message: string) => addToast('warning', message),
|
||||
info: (message: string) => addToast('info', message),
|
||||
loading: (message: string) => addToast('loading', message),
|
||||
success: (message: string, opts?: ToastOptions) => addToast('success', message, opts),
|
||||
error: (message: string, opts?: ToastOptions) => addToast('error', message, opts),
|
||||
warning: (message: string, opts?: ToastOptions) => addToast('warning', message, opts),
|
||||
info: (message: string, opts?: ToastOptions) => addToast('info', message, opts),
|
||||
loading: (message: string, opts?: Omit<ToastOptions, 'duration'>) =>
|
||||
addToast('loading', message, opts),
|
||||
dismiss: (id: string) => removeToast(id),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user