From 9a4420fdc2f2d1df8cace6dc64b02e9908b69d28 Mon Sep 17 00:00:00 2001 From: Jokin Date: Mon, 13 Jul 2026 18:06:48 +0200 Subject: [PATCH] Toast fixes --- ui/src/app/app.html | 12 +++++++++++- ui/src/app/toast.service.ts | 32 ++++++++++++++++++++++++++++++-- ui/src/styles.css | 15 +++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 267146f..676fb41 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -45,7 +45,9 @@
@for (t of toast.toasts(); track t.id) {
+ +
}
diff --git a/ui/src/app/toast.service.ts b/ui/src/app/toast.service.ts index 5236a68..5f2d984 100644 --- a/ui/src/app/toast.service.ts +++ b/ui/src/app/toast.service.ts @@ -4,12 +4,17 @@ export interface Toast { id: number; kind: 'error' | 'info' | 'success'; text: string; + ttlMs: number; } @Injectable({ providedIn: 'root' }) export class ToastService { readonly toasts = signal([]); private nextId = 1; + private readonly timers = new Map< + number, + { timer: ReturnType; expiresAt: number; remaining: number } + >(); error(text: string) { this.show('error', text, 6000); @@ -24,12 +29,35 @@ export class ToastService { } dismiss(id: number) { + const entry = this.timers.get(id); + if (entry) clearTimeout(entry.timer); + this.timers.delete(id); this.toasts.update((list) => list.filter((t) => t.id !== id)); } + /** Freeze the auto-dismiss countdown (hover). */ + pause(id: number) { + const entry = this.timers.get(id); + if (!entry) return; + clearTimeout(entry.timer); + entry.remaining = Math.max(0, entry.expiresAt - Date.now()); + } + + /** Resume the countdown with whatever time was left when paused. */ + resume(id: number) { + const entry = this.timers.get(id); + if (!entry) return; + entry.expiresAt = Date.now() + entry.remaining; + entry.timer = setTimeout(() => this.dismiss(id), entry.remaining); + } + private show(kind: Toast['kind'], text: string, ttlMs: number) { - const toast = { id: this.nextId++, kind, text }; + const toast = { id: this.nextId++, kind, text, ttlMs }; this.toasts.update((list) => [...list, toast]); - setTimeout(() => this.dismiss(toast.id), ttlMs); + this.timers.set(toast.id, { + timer: setTimeout(() => this.dismiss(toast.id), ttlMs), + expiresAt: Date.now() + ttlMs, + remaining: ttlMs, + }); } } diff --git a/ui/src/styles.css b/ui/src/styles.css index db1af44..07a4b8f 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -198,6 +198,21 @@ animation: panel-fade-in 0.18s ease-out; } +/* Toast auto-dismiss countdown; duration set inline per toast. */ +@keyframes toast-countdown { + to { + transform: scaleX(0); + } +} +.toast-countdown { + animation: toast-countdown linear forwards; +} +/* unlayered on purpose: a Tailwind group-hover utility would lose to the + unlayered animation shorthand above */ +.group:hover > .toast-countdown { + animation-play-state: paused; +} + /* Accent tone (data-accent set by SettingsService; absent = graphite/zinc). Each tone is one hue+chroma line; the formula blocks below derive --primary, --ring and the cursor-row tint for light and both dark states. The dark