From 400b6ac2a535a72ac96e2024ec7a6fd80a5c1b24 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sun, 5 Jul 2026 14:10:34 +0200 Subject: [PATCH] fix(ui): hold info toast ~1s before it rises and fades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The info toast began drifting up and fading the instant it finished appearing (the CSS keyframes went straight from the 12% appeared-stop to the 100% risen-and-faded stop), so a glanced message was already leaving. Insert a ~1s hold at full opacity/rest before the rise-and-fade: extend the animation 2s → 3s with keyframe stops at 8% (appeared, ~240ms) and 41% (end of the ~1s hold), keeping the original rise-and-fade pace for the tail. The reduced-motion variant gets the same appear/hold/fade timing (fade only, no travel). The showToast dismissal timer is bumped 2000 → 3000ms to stay in lockstep with the animation (the error toast's 4s dwell is unchanged). --- ui/src/components/Toast.svelte | 23 +++++++++++++---------- ui/src/lib/app.svelte.ts | 6 +++--- 2 files changed, 16 insertions(+), 13 deletions(-) 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. */ -- 2.52.0