All checks were successful
Dev Build / build-test (pull_request) Successful in 1m57s
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
// ============================================================================
|
|
// OpenClaw Control Center — Theme Service
|
|
// ============================================================================
|
|
// Angular service providing programmatic access to design tokens, theme
|
|
// mode switching (dark/light), and runtime CSS custom property manipulation.
|
|
//
|
|
// Usage:
|
|
// constructor(private theme: CcThemeService) {}
|
|
//
|
|
// // Read a token
|
|
// const primary = this.theme.getToken('--cc-color-primary');
|
|
//
|
|
// // Set a token at runtime
|
|
// this.theme.setToken('--cc-color-primary', '#00ff00');
|
|
//
|
|
// // Toggle theme
|
|
// this.theme.setMode('light');
|
|
// ============================================================================
|
|
|
|
import { Injectable, signal, computed, effect } from '@angular/core';
|
|
import { CcCssProps, getStatusColor, setCssToken, getCssToken } from './tokens';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme Mode Types
|
|
// ---------------------------------------------------------------------------
|
|
export type ThemeMode = 'dark' | 'light';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Light theme overrides (future use)
|
|
// ---------------------------------------------------------------------------
|
|
const LIGHT_THEME_OVERRIDES: Record<string, string> = {
|
|
// Surface tokens
|
|
'--cc-surface-darkest': '#F8FAFC',
|
|
'--cc-surface-dark': '#FFFFFF',
|
|
'--cc-surface-medium': '#F1F5F9',
|
|
'--cc-surface-light': '#E2E8F0',
|
|
'--cc-surface-lighter': '#CBD5E1',
|
|
|
|
// On-surface tokens
|
|
'--cc-on-surface': '#0F172A',
|
|
'--cc-on-surface-variant': '#475569',
|
|
'--cc-on-surface-muted': '#94A3B8',
|
|
|
|
// Border
|
|
'--cc-surface-lighter-alt': '#E2E8F0',
|
|
|
|
// M3 system overrides for light
|
|
'--mat-sys-surface': '#FFFFFF',
|
|
'--mat-sys-surface-container': '#F1F5F9',
|
|
'--mat-sys-surface-container-high': '#E2E8F0',
|
|
'--mat-sys-on-surface': '#0F172A',
|
|
'--mat-sys-on-surface-variant': '#475569',
|
|
'--mat-sys-outline': '#CBD5E1',
|
|
'--mat-sys-background': '#F8FAFC',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dark theme (matches the SCSS defaults)
|
|
// ---------------------------------------------------------------------------
|
|
const DARK_THEME_OVERRIDES: Record<string, string> = {
|
|
'--cc-surface-darkest': '#0D0F12',
|
|
'--cc-surface-dark': '#13161A',
|
|
'--cc-surface-medium': '#1C2027',
|
|
'--cc-surface-light': '#252B33',
|
|
'--cc-surface-lighter': '#2D3748',
|
|
|
|
'--cc-on-surface': '#E2E8F0',
|
|
'--cc-on-surface-variant': '#8A9BB0',
|
|
'--cc-on-surface-muted': '#64748B',
|
|
|
|
'--mat-sys-surface': '#13161A',
|
|
'--mat-sys-surface-container': '#1C2027',
|
|
'--mat-sys-surface-container-high': '#252B33',
|
|
'--mat-sys-on-surface': '#E2E8F0',
|
|
'--mat-sys-on-surface-variant': '#8A9BB0',
|
|
'--mat-sys-outline': '#2D3748',
|
|
'--mat-sys-background': '#0D0F12',
|
|
};
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class CcThemeService {
|
|
// ---------------------------------------------------------------------------
|
|
// Signals for reactive theme state
|
|
// ---------------------------------------------------------------------------
|
|
private readonly _mode = signal<ThemeMode>(
|
|
(localStorage.getItem('cc-theme') as ThemeMode) ?? 'dark'
|
|
);
|
|
|
|
/** Current theme mode */
|
|
readonly mode = this._mode.asReadonly();
|
|
|
|
/** Computed: is the current mode dark? */
|
|
readonly isDark = computed(() => this._mode() === 'dark');
|
|
|
|
/** Computed: is the current mode light? */
|
|
readonly isLight = computed(() => this._mode() === 'light');
|
|
|
|
constructor() {
|
|
// Apply theme on init and whenever mode changes
|
|
effect(() => {
|
|
this.applyTheme(this._mode());
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Set the theme mode and persist to localStorage */
|
|
setMode(mode: ThemeMode): void {
|
|
this._mode.set(mode);
|
|
localStorage.setItem('cc-theme', mode);
|
|
}
|
|
|
|
/** Toggle between dark and light mode */
|
|
toggle(): void {
|
|
this.setMode(this._mode() === 'dark' ? 'light' : 'dark');
|
|
}
|
|
|
|
/** Read a CSS custom property from the document root */
|
|
getToken(property: string): string {
|
|
return getCssToken(property);
|
|
}
|
|
|
|
/** Set a CSS custom property on the document root */
|
|
setToken(property: string, value: string): void {
|
|
setCssToken(property, value);
|
|
}
|
|
|
|
/** Get status color set by agent status */
|
|
getStatusColors(status: string): { fg: string; bg: string; border: string } {
|
|
return getStatusColor(status);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Internal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Apply a theme mode by setting all CSS custom properties */
|
|
private applyTheme(mode: ThemeMode): void {
|
|
const overrides = mode === 'dark' ? DARK_THEME_OVERRIDES : LIGHT_THEME_OVERRIDES;
|
|
|
|
// Set color-scheme for native form controls
|
|
document.documentElement.style.setProperty('color-scheme', mode);
|
|
|
|
// Apply all overrides
|
|
for (const [prop, value] of Object.entries(overrides)) {
|
|
document.documentElement.style.setProperty(prop, value);
|
|
}
|
|
}
|
|
} |