99 lines
3.0 KiB
TypeScript
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)
|
|
);
|
|
}
|
|
}
|