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 +45 -3
- package/fesm2022/ngx-com-theme.mjs +234 -0
- package/fesm2022/ngx-com-theme.mjs.map +1 -0
- package/package.json +5 -1
- package/types/ngx-com-theme.d.ts +125 -0
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
|
-
```
|
|
70
|
-
|
|
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.
|
|
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 };
|