Skip to content

Commit ce7d998

Browse files
core(theme): Code separation based on responsibility
1 parent e86f96e commit ce7d998

File tree

9 files changed

+352
-327
lines changed

9 files changed

+352
-327
lines changed

projects/theme/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angularui/theme",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "Modern Theme Management for Angular - A lightweight, feature-rich theme library with automatic dark mode detection, SSR support, and zero configuration required.",
55
"keywords": [
66
"angular",
@@ -47,9 +47,9 @@
4747
"typings": "index.d.ts",
4848
"exports": {
4949
".": {
50+
"types": "./index.d.ts",
5051
"import": "./fesm2022/angularui-theme.mjs",
5152
"require": "./fesm2022/angularui-theme.mjs",
52-
"types": "./index.d.ts",
5353
"default": "./fesm2022/angularui-theme.mjs"
5454
},
5555
"./package.json": {
@@ -58,7 +58,7 @@
5858
},
5959
"files": [
6060
"index.d.ts",
61-
"fesm2022/",
61+
"fesm2022/angularui-theme.mjs",
6262
"README.md",
6363
"LICENSE"
6464
],
@@ -76,4 +76,4 @@
7676
"publishConfig": {
7777
"access": "public"
7878
}
79-
}
79+
}

projects/theme/src/lib/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { ThemeService, THEME_CONFIG } from './theme.service';
2-
export type { ThemeConfig, Theme } from './theme.service';
1+
export { ThemeService } from './theme.service';
32
export { provideUiTheme } from './theme.provider';
3+
export type { Theme, ThemeConfig, ThemeStrategy } from './theme.types';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { inject, InjectionToken } from '@angular/core';
2+
import { ThemeConfig, ValidatedThemeConfig } from './theme.types';
3+
4+
export const THEME_CONFIG = new InjectionToken<ThemeConfig>('ThemeConfig');
5+
6+
export function validateConfig(config: any): void {
7+
const validThemes = ['light', 'dark', 'system'];
8+
const validStrategies = ['attribute', 'class'];
9+
10+
if (config.defaultTheme && !validThemes.includes(config.defaultTheme)) {
11+
console.warn(`Invalid defaultTheme: ${config.defaultTheme}. Using 'system' as fallback.`);
12+
config.defaultTheme = 'system';
13+
}
14+
15+
if (config.strategy && !validStrategies.includes(config.strategy)) {
16+
console.warn(`Invalid strategy: ${config.strategy}. Using 'attribute' as fallback.`);
17+
config.strategy = 'attribute';
18+
}
19+
20+
if (config.storageKey && typeof config.storageKey !== 'string') {
21+
console.warn(`Invalid storageKey: ${config.storageKey}. Using 'theme' as fallback.`);
22+
config.storageKey = 'theme';
23+
}
24+
25+
if (config.forcedTheme && !validThemes.includes(config.forcedTheme)) {
26+
console.warn(`Invalid forcedTheme: ${config.forcedTheme}. Ignoring forced theme.`);
27+
config.forcedTheme = undefined;
28+
}
29+
}
30+
31+
export function initializeConfig(): ValidatedThemeConfig {
32+
const injectedConfig = inject(THEME_CONFIG, { optional: true });
33+
const defaultConfig: ValidatedThemeConfig = {
34+
defaultTheme: 'system',
35+
storageKey: 'theme',
36+
strategy: 'attribute',
37+
enableAutoInit: true,
38+
enableColorScheme: true,
39+
enableSystem: true,
40+
forcedTheme: undefined
41+
};
42+
43+
const mergedConfig = { ...defaultConfig, ...injectedConfig };
44+
validateConfig(mergedConfig);
45+
return mergedConfig;
46+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ValidatedThemeConfig } from './theme.types';
2+
3+
export interface DomManager {
4+
applyTheme(theme: 'light' | 'dark', config: ValidatedThemeConfig): void;
5+
}
6+
7+
export class ThemeDomManager implements DomManager {
8+
applyTheme(theme: 'light' | 'dark', config: ValidatedThemeConfig): void {
9+
try {
10+
const element = document.documentElement;
11+
12+
if (config.strategy === 'class') {
13+
this.applyClassTheme(element, theme);
14+
} else {
15+
this.applyAttributeTheme(element, theme);
16+
}
17+
18+
if (config.enableColorScheme) {
19+
this.applyColorScheme(element, theme);
20+
}
21+
} catch (error) {
22+
console.error('Failed to apply theme:', error);
23+
}
24+
}
25+
26+
private applyClassTheme(element: HTMLElement, theme: 'light' | 'dark'): void {
27+
try {
28+
if (theme === 'dark') {
29+
element.classList.add('dark');
30+
} else {
31+
element.classList.remove('dark');
32+
}
33+
} catch (error) {
34+
console.warn('Failed to apply class theme:', error);
35+
}
36+
}
37+
38+
private applyAttributeTheme(element: HTMLElement, theme: 'light' | 'dark'): void {
39+
try {
40+
if (theme === 'dark') {
41+
element.setAttribute('data-theme', 'dark');
42+
} else {
43+
element.removeAttribute('data-theme');
44+
}
45+
} catch (error) {
46+
console.warn('Failed to apply attribute theme:', error);
47+
}
48+
}
49+
50+
private applyColorScheme(element: HTMLElement, theme: 'light' | 'dark'): void {
51+
try {
52+
element.style.colorScheme = theme;
53+
} catch (error) {
54+
console.warn('Failed to apply color scheme:', error);
55+
}
56+
}
57+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ValidatedThemeConfig } from './theme.types';
2+
3+
export interface MediaManager {
4+
mediaQuery: MediaQueryList | null;
5+
setup(config: ValidatedThemeConfig): void;
6+
updateSystemTheme(): 'light' | 'dark';
7+
cleanup(): void;
8+
}
9+
10+
export class SystemThemeManager implements MediaManager {
11+
mediaQuery: MediaQueryList | null = null;
12+
private isDestroyed = false;
13+
14+
setup(config: ValidatedThemeConfig): void {
15+
try {
16+
if (typeof window !== 'undefined' &&
17+
window.matchMedia &&
18+
config.enableSystem &&
19+
!this.isDestroyed) {
20+
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
21+
this.updateSystemTheme();
22+
}
23+
} catch (error) {
24+
console.warn('Failed to setup media query for system theme detection:', error);
25+
}
26+
}
27+
28+
updateSystemTheme(): 'light' | 'dark' {
29+
try {
30+
if (this.mediaQuery && !this.isDestroyed) {
31+
return this.mediaQuery.matches ? 'dark' : 'light';
32+
}
33+
return 'light';
34+
} catch (error) {
35+
console.warn('Failed to update system theme:', error);
36+
return 'light';
37+
}
38+
}
39+
40+
addChangeListener(callback: () => void): void {
41+
if (this.mediaQuery && !this.isDestroyed) {
42+
this.mediaQuery.addEventListener('change', callback);
43+
}
44+
}
45+
46+
removeChangeListener(callback: () => void): void {
47+
if (this.mediaQuery) {
48+
this.mediaQuery.removeEventListener('change', callback);
49+
}
50+
}
51+
52+
cleanup(): void {
53+
this.isDestroyed = true;
54+
this.mediaQuery = null;
55+
}
56+
}
Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
1-
import { Provider, EnvironmentProviders } from '@angular/core';
2-
import { provideAppInitializer, inject } from '@angular/core';
3-
import { ThemeService, ThemeConfig, THEME_CONFIG } from './theme.service';
1+
import { Provider, EnvironmentProviders, provideAppInitializer, inject } from '@angular/core';
2+
import { ThemeService } from './theme.service';
3+
import { THEME_CONFIG } from './theme.config';
4+
import type { ThemeConfig } from './theme.types';
45

5-
/**
6-
* Provider function to configure the theme service
7-
* @param config Theme configuration options
8-
* @returns Provider array for Angular DI
9-
*/
106
export function provideUiTheme(config?: ThemeConfig): (Provider | EnvironmentProviders)[] {
117
return [
12-
{
13-
provide: THEME_CONFIG,
14-
useValue: config || {}
15-
},
16-
provideAppInitializer(() => {
17-
const themeService = inject(ThemeService);
18-
// Initialize the theme service if enableAutoInit is enabled or not explicitly disabled
19-
if (config?.enableAutoInit !== false) {
8+
...(config ? [{ provide: THEME_CONFIG, useValue: config }] : []),
9+
...(config?.enableAutoInit !== false ? [
10+
provideAppInitializer(() => {
11+
const themeService = inject(ThemeService);
2012
themeService.initialize();
21-
}
22-
23-
return Promise.resolve();
24-
})
13+
return Promise.resolve();
14+
})
15+
] : [])
2516
];
2617
}

0 commit comments

Comments
 (0)