Merge pull request 'fix(ui): hold info toast ~1s before it rises and fades' (#181) from feature/toast-hold-before-fade into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m7s
CI / conformance (push) Successful in 9s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m44s

This commit was merged in pull request #181.
This commit is contained in:
2026-07-05 12:24:08 +00:00
2 changed files with 16 additions and 13 deletions
+13 -10
View File
@@ -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');
</script>
@@ -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% {
+3 -3
View File
@@ -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. */