Toast fixes

This commit is contained in:
2026-07-13 18:06:48 +02:00
parent dda596c50c
commit 9a4420fdc2
3 changed files with 56 additions and 3 deletions
+11 -1
View File
@@ -45,7 +45,9 @@
<div class="pointer-events-none fixed right-3 bottom-8 z-50 flex w-80 flex-col gap-2">
@for (t of toast.toasts(); track t.id) {
<div
class="pointer-events-auto flex items-start gap-2 rounded-lg border border-border bg-card p-3 text-xs text-card-foreground shadow-lg"
(mouseenter)="toast.pause(t.id)"
(mouseleave)="toast.resume(t.id)"
class="group pointer-events-auto relative flex items-start gap-2 overflow-hidden rounded-lg border border-border bg-card p-3 text-xs text-card-foreground shadow-lg"
>
<ng-icon
[name]="toastIcon(t.kind)"
@@ -59,6 +61,14 @@
>
<ng-icon name="mynaX" size="0.9rem" />
</button>
<!-- auto-dismiss countdown; freezes with the timer while hovered -->
<div
class="toast-countdown absolute inset-x-0 bottom-0 h-0.5 origin-left"
[class.bg-destructive]="t.kind === 'error'"
[class.bg-success]="t.kind === 'success'"
[class.bg-primary]="t.kind === 'info'"
[style.animation-duration.ms]="t.ttlMs"
></div>
</div>
}
</div>
+30 -2
View File
@@ -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<Toast[]>([]);
private nextId = 1;
private readonly timers = new Map<
number,
{ timer: ReturnType<typeof setTimeout>; 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,
});
}
}
+15
View File
@@ -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