Stage 1: extract the skin-agnostic UI layer into app/core/
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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user