diff --git a/ui/src/components/Toast.svelte b/ui/src/components/Toast.svelte index e1b7e69..1b46b66 100644 --- a/ui/src/components/Toast.svelte +++ b/ui/src/components/Toast.svelte @@ -3,8 +3,9 @@ import { app, dismissToast } from '../lib/app.svelte'; const dur = $derived(app.reduceMotion ? 0 : 260); - // An info bubble owns its whole 2s life through a CSS rise-and-fade animation, so it takes - // no Svelte enter/leave transition; an error keeps the fly-in / fade-out dwell behaviour. + // An info bubble owns its whole 3s life through a CSS animation — appear, a ~1s hold at rest, + // then rise-and-fade — so it takes no Svelte enter/leave transition; an error keeps the fly-in / + // fade-out dwell behaviour. const isInfo = $derived(app.toast?.kind !== 'error'); @@ -50,17 +51,19 @@ border-color: var(--danger); color: var(--danger); } - /* An info bubble fades in, then drifts up by roughly the tab-bar height while fading out, - all within 2s; the X centring is preserved across the rise. */ + /* An info bubble fades in, holds for ~1s at rest, then drifts up by roughly the tab-bar height + while fading out, all within 3s; the X centring is preserved across the rise. The 8%/41% + stops are the appear (~240ms) and the end of the ~1s hold. */ .toast.rise { - animation: toast-rise 2s ease-out forwards; + animation: toast-rise 3s ease-out forwards; } @keyframes toast-rise { 0% { opacity: 0; transform: translateX(-50%) translateY(8px); } - 12% { + 8%, + 41% { opacity: 1; transform: translateX(-50%) translateY(0); } @@ -69,16 +72,16 @@ transform: translateX(-50%) translateY(-56px); } } - /* Reduced motion: the same 2s life, fade only — no upward travel. */ + /* Reduced motion: the same 3s life (appear, ~1s hold, fade), fade only — no upward travel. */ .toast.rise-reduced { - animation: toast-rise-reduced 2s ease-out forwards; + animation: toast-rise-reduced 3s ease-out forwards; } @keyframes toast-rise-reduced { 0% { opacity: 0; } - 12%, - 70% { + 8%, + 41% { opacity: 1; } 100% { diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 07be39e..c92fd44 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -214,9 +214,9 @@ function goForeground(): void { export function showToast(text: string, kind: Toast['kind'] = 'info'): void { app.toast = { kind, text, seq: ++toastSeq }; if (toastTimer) clearTimeout(toastTimer); - // An info bubble lives exactly as long as its rise-and-fade animation (2s); an error - // dwells longer (4s) so it is not missed. - toastTimer = setTimeout(() => (app.toast = null), kind === 'error' ? 4000 : 2000); + // An info bubble lives exactly as long as its appear + ~1s hold + rise-and-fade animation (3s); + // an error dwells longer (4s) so it is not missed. + toastTimer = setTimeout(() => (app.toast = null), kind === 'error' ? 4000 : 3000); } /** dismissToast hides the current toast at once — a tap on the bubble dismisses it. */