First step of splitting the UI into two skin trees. All 12 services plus platform.ts, format.ts, file-icons.ts, icons.ts and app.config.ts move to ui/src/app/core/ — the layer both skins will share. This is a pure move: git records all 17 as renames, and no file content changed beyond import paths. The premise holds because `git diff main...feat/7css-skin` touches none of these files — the service layer was already skin-agnostic, so there is nothing to untangle, only to relocate. Components now reach it through a new `@core/*` tsconfig alias rather than a relative path. That is deliberate and load-bearing for the next stage: skin components are about to move under app/skins/<skin>/ at varying depths, and an alias keeps every one of these 67 imports correct wherever its file ends up. `@shared/*` (the bun-side src/shared) already set the precedent. Intra-core imports needed no edits at all — every ./sibling reference among the moved files points at another moved file. Verified: tsc clean, `bun run build:ui` clean (AOT resolves every import and template symbol), and the running window's opaque interior is pixel-identical to a pre-refactor capture. Note on that comparison: the Aero frame and title bar do NOT compare equal between launches, and that is not a regression — .window.glass::before runs at opacity .6, so the chrome composites over whatever desktop is behind the window and shifts by a uniform ~8% brightness. The check therefore diffs the opaque interior (inset 30px sides, 80px top), where anything this refactor could break would show. Co-Authored-By: Claude <noreply@anthropic.com>
64 lines
1.7 KiB
TypeScript
64 lines
1.7 KiB
TypeScript
import { Injectable, signal } from '@angular/core';
|
|
|
|
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);
|
|
}
|
|
|
|
info(text: string) {
|
|
this.show('info', text, 3000);
|
|
}
|
|
|
|
success(text: string) {
|
|
this.show('success', text, 3000);
|
|
}
|
|
|
|
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, ttlMs };
|
|
this.toasts.update((list) => [...list, toast]);
|
|
this.timers.set(toast.id, {
|
|
timer: setTimeout(() => this.dismiss(toast.id), ttlMs),
|
|
expiresAt: Date.now() + ttlMs,
|
|
remaining: ttlMs,
|
|
});
|
|
}
|
|
}
|