Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { writable } from 'svelte/store';
|
|
|
|
// Small global toast queue.
|
|
//
|
|
// Any part of the UI can drop a message and it appears in the corner for a
|
|
// couple of seconds, then quietly dismisses. Replaces the ad-hoc "flash"
|
|
// strings scattered across pages when the message doesn't need to live inline
|
|
// with a form.
|
|
|
|
export type ToastVariant = 'info' | 'success' | 'warning' | 'danger';
|
|
|
|
export interface Toast {
|
|
id: number;
|
|
message: string;
|
|
variant: ToastVariant;
|
|
ttl: number;
|
|
href?: string;
|
|
}
|
|
|
|
export const toasts = writable<Toast[]>([]);
|
|
export interface Notice extends Toast { createdAt: string; read: boolean; }
|
|
export const notificationHistory = writable<Notice[]>([]);
|
|
const historyKey = 'electric-cherry:notifications';
|
|
|
|
export function startNotificationHistory(): () => void {
|
|
try {
|
|
const saved: unknown = JSON.parse(sessionStorage.getItem(historyKey) ?? '[]');
|
|
if (Array.isArray(saved)) {
|
|
const valid = saved.filter((item): item is Notice => item && typeof item.id === 'number' && Number.isSafeInteger(item.id)
|
|
&& typeof item.message === 'string' && ['info', 'success', 'warning', 'danger'].includes(item.variant)
|
|
&& typeof item.createdAt === 'string' && Number.isFinite(Date.parse(item.createdAt))
|
|
&& typeof item.read === 'boolean' && (!item.href || (typeof item.href === 'string' && item.href.startsWith('/') && !item.href.startsWith('//')))).slice(0, 50);
|
|
notificationHistory.set(valid);
|
|
nextId = Math.max(nextId, ...valid.map(item => item.id + 1));
|
|
}
|
|
} catch { /* Notifications still work when browser storage is unavailable. */ }
|
|
return notificationHistory.subscribe(items => { try { sessionStorage.setItem(historyKey, JSON.stringify(items)); } catch { /* Optional persistence. */ } });
|
|
}
|
|
|
|
export function markNotificationsRead() { notificationHistory.update(items => items.map(item => ({ ...item, read: true }))); }
|
|
export function clearNotifications() { notificationHistory.set([]); }
|
|
|
|
let nextId = 1;
|
|
|
|
export function toast(
|
|
message: string,
|
|
opts: { variant?: ToastVariant; ttl?: number; href?: string } = {}
|
|
): void {
|
|
const t: Toast = {
|
|
id: nextId++,
|
|
message,
|
|
variant: opts.variant ?? 'info',
|
|
ttl: opts.ttl ?? 3500,
|
|
href: opts.href
|
|
};
|
|
toasts.update((arr) => [...arr, t]);
|
|
if (t.href || t.ttl === 0 || t.variant !== 'info') {
|
|
notificationHistory.update(items => [{ ...t, createdAt: new Date().toISOString(), read: false }, ...items].slice(0, 50));
|
|
}
|
|
if (t.ttl > 0) {
|
|
setTimeout(() => dismiss(t.id), t.ttl);
|
|
}
|
|
}
|
|
|
|
export function dismiss(id: number): void {
|
|
toasts.update((arr) => arr.filter((t) => t.id !== id));
|
|
}
|