Improve operational UX, recovery flows, and action safety

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.
This commit is contained in:
2026-09-19 21:34:34 -04:00
parent 940f81e6ca
commit 8ac420fe61
83 changed files with 2574 additions and 1190 deletions
+24
View File
@@ -18,6 +18,27 @@ export interface Toast {
}
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;
@@ -33,6 +54,9 @@ export function toast(
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);
}