ngx-com 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,12 +64,19 @@ Or import individual files for finer control:
64
64
  | `animations.css` | Keyframe animations used by components |
65
65
  | `utilities.css` | Utility classes |
66
66
 
67
- Switch themes at runtime by setting `data-theme` on the `<html>` element:
67
+ Switch themes at runtime by setting `data-theme` on the `<html>` element.
68
+ The library provides a theme service that handles this automatically:
68
69
 
69
- ```html
70
- <html data-theme="dark">
70
+ ```typescript
71
+ import { provideComTheme } from 'ngx-com/theme';
72
+
73
+ export const appConfig: ApplicationConfig = {
74
+ providers: [provideComTheme()],
75
+ };
71
76
  ```
72
77
 
78
+ See the [Theme service](#theme-service) section for details.
79
+
73
80
  ### 2. Add Tailwind source for ngx-com
74
81
 
75
82
  Library components use Tailwind utility classes in their templates. Tell
@@ -164,6 +171,41 @@ import { ComDropdown, ComDropdownOption } from 'ngx-com/components/dropdown';
164
171
  | Toast | `ngx-com/components/toast` | Toast notification service |
165
172
  | Tooltip | `ngx-com/components/tooltip` | Hover/focus tooltip |
166
173
 
174
+ ## Services
175
+
176
+ ### Theme service
177
+
178
+ Import path: `ngx-com/theme`
179
+
180
+ SSR-safe theme management with localStorage persistence and system
181
+ `prefers-color-scheme` detection. Applies the active theme via a `data-theme`
182
+ attribute on the document element.
183
+
184
+ ```typescript
185
+ import { provideComTheme, ComTheme } from 'ngx-com/theme';
186
+
187
+ // 1. Configure in app providers (zero-config works out of the box)
188
+ providers: [provideComTheme()]
189
+
190
+ // 2. Inject and use
191
+ readonly theme = inject(ComTheme);
192
+ this.theme.setTheme('dark'); // explicit override, persisted to localStorage
193
+ this.theme.clearPreference(); // revert to following system preference
194
+ this.theme.theme(); // current theme (Signal<string>)
195
+ this.theme.isAutomatic(); // true when following system preference
196
+ ```
197
+
198
+ Configuration options:
199
+
200
+ | Option | Type | Default | Description |
201
+ | --- | --- | --- | --- |
202
+ | `defaultTheme` | `string` | `'light'` | Fallback when no stored/system preference |
203
+ | `storageKey` | `string \| null` | `'com-theme'` | localStorage key (`null` to disable) |
204
+ | `darkSchemeTheme` | `string \| null` | `'dark'` | Theme for `prefers-color-scheme: dark` (`null` to disable) |
205
+ | `lightSchemeTheme` | `string` | `defaultTheme` | Theme for light system preference |
206
+ | `followSystemPreference` | `boolean` | `true` | Watch for live system preference changes |
207
+ | `attribute` | `string` | `'data-theme'` | HTML attribute applied to `documentElement` |
208
+
167
209
  ## Utilities
168
210
 
169
211
  General-purpose utilities are available from `ngx-com/utils`.
@@ -0,0 +1,234 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, PLATFORM_ID, RendererFactory2, DestroyRef, signal, computed, effect, Injectable } from '@angular/core';
3
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
4
+
5
+ /**
6
+ * Injection token for theme service configuration.
7
+ *
8
+ * Prefer using `provideComTheme()` instead of providing this token directly.
9
+ */
10
+ const COM_THEME_CONFIG = new InjectionToken('COM_THEME_CONFIG');
11
+ /**
12
+ * Provides theme service configuration.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Minimal — defaults to light/dark, localStorage, system preference watching
17
+ * bootstrapApplication(AppComponent, {
18
+ * providers: [provideComTheme()],
19
+ * });
20
+ *
21
+ * // Custom default theme and storage key
22
+ * bootstrapApplication(AppComponent, {
23
+ * providers: [
24
+ * provideComTheme({
25
+ * defaultTheme: 'ocean',
26
+ * storageKey: 'my-app-theme',
27
+ * }),
28
+ * ],
29
+ * });
30
+ * ```
31
+ */
32
+ function provideComTheme(config) {
33
+ return { provide: COM_THEME_CONFIG, useValue: config ?? {} };
34
+ }
35
+
36
+ /** @internal Default configuration values. */
37
+ const COM_THEME_DEFAULTS = {
38
+ defaultTheme: 'light',
39
+ storageKey: 'com-theme',
40
+ darkSchemeTheme: 'dark',
41
+ lightSchemeTheme: 'light',
42
+ followSystemPreference: true,
43
+ attribute: 'data-theme',
44
+ };
45
+
46
+ /**
47
+ * SSR-safe theme management service.
48
+ *
49
+ * Manages the active theme by setting a `data-theme` attribute on the document
50
+ * element. Supports localStorage persistence, system `prefers-color-scheme`
51
+ * detection with optional live watching, and a signal-based reactive API.
52
+ *
53
+ * Configure via `provideComTheme()` in your application providers.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // In your app config
58
+ * providers: [provideComTheme({ defaultTheme: 'ocean' })]
59
+ *
60
+ * // In a component
61
+ * readonly theme = inject(ComTheme);
62
+ * this.theme.setTheme('dark');
63
+ * this.theme.clearPreference(); // revert to following system
64
+ * ```
65
+ */
66
+ class ComTheme {
67
+ document = inject(DOCUMENT);
68
+ platformId = inject(PLATFORM_ID);
69
+ renderer = inject(RendererFactory2).createRenderer(null, null);
70
+ destroyRef = inject(DestroyRef);
71
+ config;
72
+ /**
73
+ * Whether the current theme was automatically determined by system preference
74
+ * rather than explicitly set by the user.
75
+ */
76
+ _isAutomatic;
77
+ /** Current active theme. */
78
+ theme;
79
+ /** Whether the theme is currently following system preference. */
80
+ isAutomatic;
81
+ _theme;
82
+ constructor() {
83
+ const userConfig = inject(COM_THEME_CONFIG, { optional: true });
84
+ this.config = this.resolveConfig(userConfig ?? {});
85
+ const { initial, automatic } = this.determineInitialTheme();
86
+ this._theme = signal(initial, ...(ngDevMode ? [{ debugName: "_theme" }] : []));
87
+ this._isAutomatic = signal(automatic, ...(ngDevMode ? [{ debugName: "_isAutomatic" }] : []));
88
+ this.theme = this._theme.asReadonly();
89
+ this.isAutomatic = computed(() => this._isAutomatic(), ...(ngDevMode ? [{ debugName: "isAutomatic" }] : []));
90
+ // Apply theme + persist on every change
91
+ effect(() => {
92
+ const theme = this._theme();
93
+ this.applyTheme(theme);
94
+ if (!this._isAutomatic()) {
95
+ this.persistTheme(theme);
96
+ }
97
+ });
98
+ this.setupSystemPreferenceWatcher();
99
+ }
100
+ /** Set the active theme explicitly. Persists to localStorage and stops following system preference. */
101
+ setTheme(theme) {
102
+ this._isAutomatic.set(false);
103
+ this._theme.set(theme);
104
+ }
105
+ /**
106
+ * Remove the stored theme preference and revert to following system preference.
107
+ * If system preference watching is enabled, the theme updates to match the current
108
+ * system color scheme. Otherwise, falls back to the configured default theme.
109
+ */
110
+ clearPreference() {
111
+ this.removeStoredTheme();
112
+ this._isAutomatic.set(true);
113
+ if (this.config.followSystemPreference && isPlatformBrowser(this.platformId)) {
114
+ this._theme.set(this.getSystemTheme());
115
+ }
116
+ else {
117
+ this._theme.set(this.config.defaultTheme);
118
+ }
119
+ }
120
+ resolveConfig(userConfig) {
121
+ const defaultTheme = userConfig.defaultTheme ?? COM_THEME_DEFAULTS.defaultTheme;
122
+ return {
123
+ defaultTheme,
124
+ storageKey: userConfig.storageKey !== undefined
125
+ ? userConfig.storageKey
126
+ : COM_THEME_DEFAULTS.storageKey,
127
+ darkSchemeTheme: userConfig.darkSchemeTheme !== undefined
128
+ ? userConfig.darkSchemeTheme
129
+ : COM_THEME_DEFAULTS.darkSchemeTheme,
130
+ lightSchemeTheme: userConfig.lightSchemeTheme ?? defaultTheme,
131
+ followSystemPreference: userConfig.followSystemPreference ?? COM_THEME_DEFAULTS.followSystemPreference,
132
+ attribute: userConfig.attribute ?? COM_THEME_DEFAULTS.attribute,
133
+ };
134
+ }
135
+ determineInitialTheme() {
136
+ // 1. Check localStorage for explicit user choice
137
+ const stored = this.getStoredTheme();
138
+ if (stored !== null) {
139
+ return { initial: stored, automatic: false };
140
+ }
141
+ // 2. Check system preference
142
+ if (this.config.darkSchemeTheme !== null && isPlatformBrowser(this.platformId)) {
143
+ return { initial: this.getSystemTheme(), automatic: true };
144
+ }
145
+ // 3. Fall back to default
146
+ return { initial: this.config.defaultTheme, automatic: true };
147
+ }
148
+ getSystemTheme() {
149
+ const win = this.document.defaultView;
150
+ if (!win) {
151
+ return this.config.lightSchemeTheme;
152
+ }
153
+ const prefersDark = win.matchMedia('(prefers-color-scheme: dark)').matches;
154
+ return prefersDark
155
+ ? (this.config.darkSchemeTheme ?? this.config.lightSchemeTheme)
156
+ : this.config.lightSchemeTheme;
157
+ }
158
+ setupSystemPreferenceWatcher() {
159
+ if (!this.config.followSystemPreference ||
160
+ this.config.darkSchemeTheme === null ||
161
+ !isPlatformBrowser(this.platformId)) {
162
+ return;
163
+ }
164
+ const win = this.document.defaultView;
165
+ if (!win) {
166
+ return;
167
+ }
168
+ const mediaQuery = win.matchMedia('(prefers-color-scheme: dark)');
169
+ const handler = (event) => {
170
+ // Only react if the user hasn't explicitly overridden
171
+ if (!this._isAutomatic()) {
172
+ return;
173
+ }
174
+ this._theme.set(event.matches
175
+ ? (this.config.darkSchemeTheme ?? this.config.lightSchemeTheme)
176
+ : this.config.lightSchemeTheme);
177
+ };
178
+ mediaQuery.addEventListener('change', handler);
179
+ this.destroyRef.onDestroy(() => {
180
+ mediaQuery.removeEventListener('change', handler);
181
+ });
182
+ }
183
+ applyTheme(theme) {
184
+ this.renderer.setAttribute(this.document.documentElement, this.config.attribute, theme);
185
+ }
186
+ persistTheme(theme) {
187
+ if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {
188
+ return;
189
+ }
190
+ try {
191
+ localStorage.setItem(this.config.storageKey, theme);
192
+ }
193
+ catch {
194
+ // Storage full or unavailable — silently ignore
195
+ }
196
+ }
197
+ getStoredTheme() {
198
+ if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {
199
+ return null;
200
+ }
201
+ try {
202
+ return localStorage.getItem(this.config.storageKey);
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }
208
+ removeStoredTheme() {
209
+ if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {
210
+ return;
211
+ }
212
+ try {
213
+ localStorage.removeItem(this.config.storageKey);
214
+ }
215
+ catch {
216
+ // Silently ignore
217
+ }
218
+ }
219
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComTheme, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
220
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComTheme, providedIn: 'root' });
221
+ }
222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComTheme, decorators: [{
223
+ type: Injectable,
224
+ args: [{ providedIn: 'root' }]
225
+ }], ctorParameters: () => [] });
226
+
227
+ // Service
228
+
229
+ /**
230
+ * Generated bundle index. Do not edit.
231
+ */
232
+
233
+ export { COM_THEME_CONFIG, ComTheme, provideComTheme };
234
+ //# sourceMappingURL=ngx-com-theme.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-com-theme.mjs","sources":["../../../projects/com/theme/src/theme.providers.ts","../../../projects/com/theme/src/theme.models.ts","../../../projects/com/theme/src/theme.service.ts","../../../projects/com/theme/src/index.ts","../../../projects/com/theme/src/ngx-com-theme.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport type { Provider } from '@angular/core';\nimport type { ComThemeConfig } from './theme.models';\n\n/**\n * Injection token for theme service configuration.\n *\n * Prefer using `provideComTheme()` instead of providing this token directly.\n */\nexport const COM_THEME_CONFIG: InjectionToken<ComThemeConfig> =\n new InjectionToken<ComThemeConfig>('COM_THEME_CONFIG');\n\n/**\n * Provides theme service configuration.\n *\n * @example\n * ```typescript\n * // Minimal — defaults to light/dark, localStorage, system preference watching\n * bootstrapApplication(AppComponent, {\n * providers: [provideComTheme()],\n * });\n *\n * // Custom default theme and storage key\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideComTheme({\n * defaultTheme: 'ocean',\n * storageKey: 'my-app-theme',\n * }),\n * ],\n * });\n * ```\n */\nexport function provideComTheme(config?: ComThemeConfig): Provider {\n return { provide: COM_THEME_CONFIG, useValue: config ?? {} };\n}\n","/** Consumer-facing configuration for the theme service. */\nexport interface ComThemeConfig {\n /** Default theme when no stored/system preference is found. Default: `'light'`. */\n defaultTheme?: string;\n\n /**\n * localStorage key for persistence. Set to `null` to disable persistence.\n * Default: `'com-theme'`.\n */\n storageKey?: string | null;\n\n /**\n * Theme to apply when the system reports `prefers-color-scheme: dark`.\n * Set to `null` to disable system dark preference detection.\n * Default: `'dark'`.\n */\n darkSchemeTheme?: string | null;\n\n /**\n * Theme to apply when the system reports `prefers-color-scheme: light`\n * (or no preference). Defaults to `defaultTheme`.\n */\n lightSchemeTheme?: string;\n\n /**\n * When `true`, the service listens for live `prefers-color-scheme` changes\n * and applies the mapped theme — unless the user has explicitly set a theme\n * (stored in localStorage). Call `clearPreference()` to revert to system following.\n * Default: `true`.\n */\n followSystemPreference?: boolean;\n\n /**\n * HTML attribute name used to apply the theme on `documentElement`.\n * Default: `'data-theme'`.\n */\n attribute?: string;\n}\n\n/** @internal Resolved config with all defaults applied. */\nexport interface ComThemeResolvedConfig {\n defaultTheme: string;\n storageKey: string | null;\n darkSchemeTheme: string | null;\n lightSchemeTheme: string;\n followSystemPreference: boolean;\n attribute: string;\n}\n\n/** @internal Default configuration values. */\nexport const COM_THEME_DEFAULTS: ComThemeResolvedConfig = {\n defaultTheme: 'light',\n storageKey: 'com-theme',\n darkSchemeTheme: 'dark',\n lightSchemeTheme: 'light',\n followSystemPreference: true,\n attribute: 'data-theme',\n};\n","import {\n Injectable,\n DestroyRef,\n RendererFactory2,\n PLATFORM_ID,\n inject,\n signal,\n computed,\n effect,\n} from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport type { Renderer2, Signal, WritableSignal } from '@angular/core';\n\nimport { COM_THEME_CONFIG } from './theme.providers';\nimport { COM_THEME_DEFAULTS } from './theme.models';\nimport type { ComThemeResolvedConfig } from './theme.models';\n\n/**\n * SSR-safe theme management service.\n *\n * Manages the active theme by setting a `data-theme` attribute on the document\n * element. Supports localStorage persistence, system `prefers-color-scheme`\n * detection with optional live watching, and a signal-based reactive API.\n *\n * Configure via `provideComTheme()` in your application providers.\n *\n * @example\n * ```typescript\n * // In your app config\n * providers: [provideComTheme({ defaultTheme: 'ocean' })]\n *\n * // In a component\n * readonly theme = inject(ComTheme);\n * this.theme.setTheme('dark');\n * this.theme.clearPreference(); // revert to following system\n * ```\n */\n@Injectable({ providedIn: 'root' })\nexport class ComTheme {\n private readonly document = inject(DOCUMENT);\n private readonly platformId = inject(PLATFORM_ID);\n private readonly renderer: Renderer2 = inject(RendererFactory2).createRenderer(null, null);\n private readonly destroyRef = inject(DestroyRef);\n private readonly config: ComThemeResolvedConfig;\n\n /**\n * Whether the current theme was automatically determined by system preference\n * rather than explicitly set by the user.\n */\n private readonly _isAutomatic: WritableSignal<boolean>;\n\n /** Current active theme. */\n readonly theme: Signal<string>;\n\n /** Whether the theme is currently following system preference. */\n readonly isAutomatic: Signal<boolean>;\n\n private readonly _theme: WritableSignal<string>;\n\n constructor() {\n const userConfig = inject(COM_THEME_CONFIG, { optional: true });\n this.config = this.resolveConfig(userConfig ?? {});\n\n const { initial, automatic } = this.determineInitialTheme();\n this._theme = signal(initial);\n this._isAutomatic = signal(automatic);\n this.theme = this._theme.asReadonly();\n this.isAutomatic = computed(() => this._isAutomatic());\n\n // Apply theme + persist on every change\n effect(() => {\n const theme = this._theme();\n this.applyTheme(theme);\n\n if (!this._isAutomatic()) {\n this.persistTheme(theme);\n }\n });\n\n this.setupSystemPreferenceWatcher();\n }\n\n /** Set the active theme explicitly. Persists to localStorage and stops following system preference. */\n setTheme(theme: string): void {\n this._isAutomatic.set(false);\n this._theme.set(theme);\n }\n\n /**\n * Remove the stored theme preference and revert to following system preference.\n * If system preference watching is enabled, the theme updates to match the current\n * system color scheme. Otherwise, falls back to the configured default theme.\n */\n clearPreference(): void {\n this.removeStoredTheme();\n this._isAutomatic.set(true);\n\n if (this.config.followSystemPreference && isPlatformBrowser(this.platformId)) {\n this._theme.set(this.getSystemTheme());\n } else {\n this._theme.set(this.config.defaultTheme);\n }\n }\n\n private resolveConfig(\n userConfig: Partial<ComThemeResolvedConfig>,\n ): ComThemeResolvedConfig {\n const defaultTheme = userConfig.defaultTheme ?? COM_THEME_DEFAULTS.defaultTheme;\n return {\n defaultTheme,\n storageKey: userConfig.storageKey !== undefined\n ? userConfig.storageKey\n : COM_THEME_DEFAULTS.storageKey,\n darkSchemeTheme: userConfig.darkSchemeTheme !== undefined\n ? userConfig.darkSchemeTheme\n : COM_THEME_DEFAULTS.darkSchemeTheme,\n lightSchemeTheme: userConfig.lightSchemeTheme ?? defaultTheme,\n followSystemPreference:\n userConfig.followSystemPreference ?? COM_THEME_DEFAULTS.followSystemPreference,\n attribute: userConfig.attribute ?? COM_THEME_DEFAULTS.attribute,\n };\n }\n\n private determineInitialTheme(): { initial: string; automatic: boolean } {\n // 1. Check localStorage for explicit user choice\n const stored = this.getStoredTheme();\n if (stored !== null) {\n return { initial: stored, automatic: false };\n }\n\n // 2. Check system preference\n if (this.config.darkSchemeTheme !== null && isPlatformBrowser(this.platformId)) {\n return { initial: this.getSystemTheme(), automatic: true };\n }\n\n // 3. Fall back to default\n return { initial: this.config.defaultTheme, automatic: true };\n }\n\n private getSystemTheme(): string {\n const win = this.document.defaultView;\n if (!win) {\n return this.config.lightSchemeTheme;\n }\n const prefersDark = win.matchMedia('(prefers-color-scheme: dark)').matches;\n return prefersDark\n ? (this.config.darkSchemeTheme ?? this.config.lightSchemeTheme)\n : this.config.lightSchemeTheme;\n }\n\n private setupSystemPreferenceWatcher(): void {\n if (\n !this.config.followSystemPreference ||\n this.config.darkSchemeTheme === null ||\n !isPlatformBrowser(this.platformId)\n ) {\n return;\n }\n\n const win = this.document.defaultView;\n if (!win) {\n return;\n }\n\n const mediaQuery = win.matchMedia('(prefers-color-scheme: dark)');\n const handler = (event: MediaQueryListEvent): void => {\n // Only react if the user hasn't explicitly overridden\n if (!this._isAutomatic()) {\n return;\n }\n this._theme.set(\n event.matches\n ? (this.config.darkSchemeTheme ?? this.config.lightSchemeTheme)\n : this.config.lightSchemeTheme,\n );\n };\n\n mediaQuery.addEventListener('change', handler);\n this.destroyRef.onDestroy(() => {\n mediaQuery.removeEventListener('change', handler);\n });\n }\n\n private applyTheme(theme: string): void {\n this.renderer.setAttribute(\n this.document.documentElement,\n this.config.attribute,\n theme,\n );\n }\n\n private persistTheme(theme: string): void {\n if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {\n return;\n }\n try {\n localStorage.setItem(this.config.storageKey, theme);\n } catch {\n // Storage full or unavailable — silently ignore\n }\n }\n\n private getStoredTheme(): string | null {\n if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {\n return null;\n }\n try {\n return localStorage.getItem(this.config.storageKey);\n } catch {\n return null;\n }\n }\n\n private removeStoredTheme(): void {\n if (this.config.storageKey === null || !isPlatformBrowser(this.platformId)) {\n return;\n }\n try {\n localStorage.removeItem(this.config.storageKey);\n } catch {\n // Silently ignore\n }\n }\n}\n","// Service\nexport { ComTheme } from './theme.service';\n\n// Types\nexport type { ComThemeConfig } from './theme.models';\n\n// Providers\nexport { COM_THEME_CONFIG, provideComTheme } from './theme.providers';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-com",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "license": "MIT",
5
5
  "description": "A modern Angular component library built with signals, Tailwind CSS, and semantic design tokens",
6
6
  "keywords": [
@@ -232,6 +232,10 @@
232
232
  "types": "./types/ngx-com-components-tooltip.d.ts",
233
233
  "default": "./fesm2022/ngx-com-components-tooltip.mjs"
234
234
  },
235
+ "./theme": {
236
+ "types": "./types/ngx-com-theme.d.ts",
237
+ "default": "./fesm2022/ngx-com-theme.mjs"
238
+ },
235
239
  "./tokens": {
236
240
  "types": "./types/ngx-com-tokens.d.ts",
237
241
  "default": "./fesm2022/ngx-com-tokens.mjs"
@@ -0,0 +1,125 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Signal, InjectionToken, Provider } from '@angular/core';
3
+
4
+ /**
5
+ * SSR-safe theme management service.
6
+ *
7
+ * Manages the active theme by setting a `data-theme` attribute on the document
8
+ * element. Supports localStorage persistence, system `prefers-color-scheme`
9
+ * detection with optional live watching, and a signal-based reactive API.
10
+ *
11
+ * Configure via `provideComTheme()` in your application providers.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // In your app config
16
+ * providers: [provideComTheme({ defaultTheme: 'ocean' })]
17
+ *
18
+ * // In a component
19
+ * readonly theme = inject(ComTheme);
20
+ * this.theme.setTheme('dark');
21
+ * this.theme.clearPreference(); // revert to following system
22
+ * ```
23
+ */
24
+ declare class ComTheme {
25
+ private readonly document;
26
+ private readonly platformId;
27
+ private readonly renderer;
28
+ private readonly destroyRef;
29
+ private readonly config;
30
+ /**
31
+ * Whether the current theme was automatically determined by system preference
32
+ * rather than explicitly set by the user.
33
+ */
34
+ private readonly _isAutomatic;
35
+ /** Current active theme. */
36
+ readonly theme: Signal<string>;
37
+ /** Whether the theme is currently following system preference. */
38
+ readonly isAutomatic: Signal<boolean>;
39
+ private readonly _theme;
40
+ constructor();
41
+ /** Set the active theme explicitly. Persists to localStorage and stops following system preference. */
42
+ setTheme(theme: string): void;
43
+ /**
44
+ * Remove the stored theme preference and revert to following system preference.
45
+ * If system preference watching is enabled, the theme updates to match the current
46
+ * system color scheme. Otherwise, falls back to the configured default theme.
47
+ */
48
+ clearPreference(): void;
49
+ private resolveConfig;
50
+ private determineInitialTheme;
51
+ private getSystemTheme;
52
+ private setupSystemPreferenceWatcher;
53
+ private applyTheme;
54
+ private persistTheme;
55
+ private getStoredTheme;
56
+ private removeStoredTheme;
57
+ static ɵfac: i0.ɵɵFactoryDeclaration<ComTheme, never>;
58
+ static ɵprov: i0.ɵɵInjectableDeclaration<ComTheme>;
59
+ }
60
+
61
+ /** Consumer-facing configuration for the theme service. */
62
+ interface ComThemeConfig {
63
+ /** Default theme when no stored/system preference is found. Default: `'light'`. */
64
+ defaultTheme?: string;
65
+ /**
66
+ * localStorage key for persistence. Set to `null` to disable persistence.
67
+ * Default: `'com-theme'`.
68
+ */
69
+ storageKey?: string | null;
70
+ /**
71
+ * Theme to apply when the system reports `prefers-color-scheme: dark`.
72
+ * Set to `null` to disable system dark preference detection.
73
+ * Default: `'dark'`.
74
+ */
75
+ darkSchemeTheme?: string | null;
76
+ /**
77
+ * Theme to apply when the system reports `prefers-color-scheme: light`
78
+ * (or no preference). Defaults to `defaultTheme`.
79
+ */
80
+ lightSchemeTheme?: string;
81
+ /**
82
+ * When `true`, the service listens for live `prefers-color-scheme` changes
83
+ * and applies the mapped theme — unless the user has explicitly set a theme
84
+ * (stored in localStorage). Call `clearPreference()` to revert to system following.
85
+ * Default: `true`.
86
+ */
87
+ followSystemPreference?: boolean;
88
+ /**
89
+ * HTML attribute name used to apply the theme on `documentElement`.
90
+ * Default: `'data-theme'`.
91
+ */
92
+ attribute?: string;
93
+ }
94
+
95
+ /**
96
+ * Injection token for theme service configuration.
97
+ *
98
+ * Prefer using `provideComTheme()` instead of providing this token directly.
99
+ */
100
+ declare const COM_THEME_CONFIG: InjectionToken<ComThemeConfig>;
101
+ /**
102
+ * Provides theme service configuration.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Minimal — defaults to light/dark, localStorage, system preference watching
107
+ * bootstrapApplication(AppComponent, {
108
+ * providers: [provideComTheme()],
109
+ * });
110
+ *
111
+ * // Custom default theme and storage key
112
+ * bootstrapApplication(AppComponent, {
113
+ * providers: [
114
+ * provideComTheme({
115
+ * defaultTheme: 'ocean',
116
+ * storageKey: 'my-app-theme',
117
+ * }),
118
+ * ],
119
+ * });
120
+ * ```
121
+ */
122
+ declare function provideComTheme(config?: ComThemeConfig): Provider;
123
+
124
+ export { COM_THEME_CONFIG, ComTheme, provideComTheme };
125
+ export type { ComThemeConfig };