Files
pireon-ui/libs/ui/src/lib/util/theme/theme.service.ts
2026-08-06 20:51:46 +02:00

99 lines
3.0 KiB
TypeScript

import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
Injectable,
PLATFORM_ID,
computed,
effect,
inject,
signal,
} from '@angular/core';
import { ShellClass } from '../../styles';
import { LocalStorageService } from '../local-storage';
export type ThemeVariant = 'default' | 'alternative' | 'minimal' | 'gradient';
const THEME_STORAGE_KEY = 'pi-theme';
const DARK_MODE_STORAGE_KEY = 'pi-dark-mode';
const shellClassByVariant: Record<ThemeVariant, string> = {
default: ShellClass.Default,
alternative: ShellClass.Alternative,
minimal: ShellClass.Minimal,
gradient: ShellClass.Gradient,
};
/**
* Centraliza lo que hasta ahora cada app reimplementaba a mano (ver el
* `app.ts` del playground antes de esta extracción): fijar
* `pi-shell pi-shell__<tema>` en `<body>` — los overlays de Material (diálogo,
* snackbar) cuelgan fuera de `<app-root>` y también necesitan heredarlo — y
* `dark-mode` en `<html>`.
*
* `theme`/`darkMode` son señales normales: se pueden mutar tanto con
* `setTheme()`/`toggleDarkMode()` como con un `[(checked)]`/`[(value)]` dos
* vías directo sobre la señal. La aplicación de clases y la persistencia en
* `localStorage` viven en `effect()`, no en los setters, para que cualquier
* camino de escritura quede cubierto por igual.
*
* El modo oscuro en sí (qué tokens remapea `.dark-mode`) lo sigue definiendo
* cada app consumidora — este servicio solo pone o quita la clase.
*/
@Injectable({ providedIn: 'root' })
export class PiThemeService {
private readonly document = inject(DOCUMENT);
private readonly platformId = inject(PLATFORM_ID);
private readonly localStorage = inject(LocalStorageService);
readonly theme = signal<ThemeVariant>(this.readStoredTheme());
readonly darkMode = signal<boolean>(this.readStoredDarkMode());
readonly shellClass = computed(() => shellClassByVariant[this.theme()]);
constructor() {
effect(() => {
this.document.body.className = this.shellClass();
this.persist(THEME_STORAGE_KEY, this.theme());
});
effect(() => {
this.document.documentElement.classList.toggle(
'dark-mode',
this.darkMode(),
);
this.persist(DARK_MODE_STORAGE_KEY, this.darkMode());
});
}
setTheme(theme: ThemeVariant): void {
this.theme.set(theme);
}
setDarkMode(darkMode: boolean): void {
this.darkMode.set(darkMode);
}
toggleDarkMode(): void {
this.darkMode.update((value) => !value);
}
private persist(key: string, value: unknown): void {
if (isPlatformBrowser(this.platformId)) {
this.localStorage.setItem(key, value);
}
}
private readStoredTheme(): ThemeVariant {
if (!isPlatformBrowser(this.platformId)) return 'default';
const stored = this.localStorage.getItem(THEME_STORAGE_KEY);
return stored && stored in shellClassByVariant ? stored : 'default';
}
private readStoredDarkMode(): boolean {
return (
isPlatformBrowser(this.platformId) &&
!!this.localStorage.getItem(DARK_MODE_STORAGE_KEY)
);
}
}