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