Toast fixes
This commit is contained in:
+11
-1
@@ -45,7 +45,9 @@
|
|||||||
<div class="pointer-events-none fixed right-3 bottom-8 z-50 flex w-80 flex-col gap-2">
|
<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) {
|
@for (t of toast.toasts(); track t.id) {
|
||||||
<div
|
<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
|
<ng-icon
|
||||||
[name]="toastIcon(t.kind)"
|
[name]="toastIcon(t.kind)"
|
||||||
@@ -59,6 +61,14 @@
|
|||||||
>
|
>
|
||||||
<ng-icon name="mynaX" size="0.9rem" />
|
<ng-icon name="mynaX" size="0.9rem" />
|
||||||
</button>
|
</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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ export interface Toast {
|
|||||||
id: number;
|
id: number;
|
||||||
kind: 'error' | 'info' | 'success';
|
kind: 'error' | 'info' | 'success';
|
||||||
text: string;
|
text: string;
|
||||||
|
ttlMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ToastService {
|
export class ToastService {
|
||||||
readonly toasts = signal<Toast[]>([]);
|
readonly toasts = signal<Toast[]>([]);
|
||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
|
private readonly timers = new Map<
|
||||||
|
number,
|
||||||
|
{ timer: ReturnType<typeof setTimeout>; expiresAt: number; remaining: number }
|
||||||
|
>();
|
||||||
|
|
||||||
error(text: string) {
|
error(text: string) {
|
||||||
this.show('error', text, 6000);
|
this.show('error', text, 6000);
|
||||||
@@ -24,12 +29,35 @@ export class ToastService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dismiss(id: number) {
|
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));
|
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) {
|
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]);
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,21 @@
|
|||||||
animation: panel-fade-in 0.18s ease-out;
|
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).
|
/* Accent tone (data-accent set by SettingsService; absent = graphite/zinc).
|
||||||
Each tone is one hue+chroma line; the formula blocks below derive --primary,
|
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
|
--ring and the cursor-row tint for light and both dark states. The dark
|
||||||
|
|||||||
Reference in New Issue
Block a user