@cdevhub/ngx-tw 0.5.0 → 0.6.1
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 +78 -15
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
- package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
- package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
- package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-item.mjs +28 -4
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
- package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
- package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
- package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
- package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
- package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
- package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
- package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
- package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
- package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
- package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
- package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
- package/index.json +1 -1
- package/package.json +63 -2
- package/theme/_base.css +13 -0
- package/theme/_dark.css +111 -36
- package/theme/_high-contrast-dark.css +367 -0
- package/theme/_high-contrast.css +13 -2
- package/theme/_light.css +332 -0
- package/theme/_semantic.css +60 -26
- package/theme/_typography.css +17 -0
- package/theme/index.css +86 -7
- package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
- package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
- package/types/cdevhub-ngx-tw-button.d.ts +1 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
- package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
- package/types/cdevhub-ngx-tw-card.d.ts +11 -3
- package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
- package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
- package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
- package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
- package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
- package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
- package/types/cdevhub-ngx-tw-core.d.ts +152 -21
- package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
- package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
- package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
- package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
- package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
- package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
- package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
- package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
- package/types/cdevhub-ngx-tw-input.d.ts +5 -8
- package/types/cdevhub-ngx-tw-item.d.ts +2 -0
- package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
- package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
- package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
- package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
- package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
- package/types/cdevhub-ngx-tw-select.d.ts +104 -33
- package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
- package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
- package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
- package/types/cdevhub-ngx-tw-split.d.ts +32 -2
- package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
- package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
- package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
- package/types/cdevhub-ngx-tw-table.d.ts +53 -10
- package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
- package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
- package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
- package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
- package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
- package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
- package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
- package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
- package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
- package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
- package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
- package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
- package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
- package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs.map +0 -1
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, PLATFORM_ID, NgZone, signal, computed, effect, Injectable, input, Directive } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, provideEnvironmentInitializer, inject, PLATFORM_ID, NgZone, signal, computed, effect, Injectable, input, Directive } from '@angular/core';
|
|
3
3
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
4
4
|
|
|
5
5
|
/** Ordered list of every {@link TwTheme} value, used by `cycleTheme()` and for UI iteration. */
|
|
6
|
-
const TW_THEMES = [
|
|
6
|
+
const TW_THEMES = [
|
|
7
|
+
'light',
|
|
8
|
+
'dark',
|
|
9
|
+
'high-contrast',
|
|
10
|
+
'high-contrast-dark',
|
|
11
|
+
'system',
|
|
12
|
+
];
|
|
7
13
|
/** Ordered list of every {@link TwResolvedTheme} value (i.e. {@link TW_THEMES} minus `'system'`). */
|
|
8
|
-
const TW_RESOLVED_THEMES = [
|
|
9
|
-
|
|
14
|
+
const TW_RESOLVED_THEMES = [
|
|
15
|
+
'light',
|
|
16
|
+
'dark',
|
|
17
|
+
'high-contrast',
|
|
18
|
+
'high-contrast-dark',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Built-in defaults merged under any user-provided {@link TwThemeConfig} by
|
|
22
|
+
* `provideTheme()`. Typed `Required<TwThemeConfig>` so readers keep a
|
|
23
|
+
* non-optional `string` / `TwTheme` for every field even though the interface
|
|
24
|
+
* itself is all-optional.
|
|
25
|
+
*/
|
|
10
26
|
const DEFAULT_TW_THEME_CONFIG = {
|
|
11
27
|
defaultTheme: 'system',
|
|
12
28
|
storageKey: 'ngx-tw-theme',
|
|
@@ -14,47 +30,110 @@ const DEFAULT_TW_THEME_CONFIG = {
|
|
|
14
30
|
target: 'documentElement',
|
|
15
31
|
};
|
|
16
32
|
|
|
17
|
-
/** Injection token carrying the resolved {@link TwThemeConfig} (storage key, attribute, target element, default theme). */
|
|
18
|
-
const THEME_CONFIG = new InjectionToken('THEME_CONFIG');
|
|
19
33
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
34
|
+
* Injection token carrying the resolved {@link TwThemeConfig} (storage key,
|
|
35
|
+
* attribute, target element, default theme).
|
|
36
|
+
*
|
|
37
|
+
* The value is `Required<TwThemeConfig>`: `TwThemeConfig`'s members are
|
|
38
|
+
* optional so consumers can pass a partial config, but `provideTheme` fills
|
|
39
|
+
* every key from {@link DEFAULT_TW_THEME_CONFIG} before providing it. Injectors
|
|
40
|
+
* therefore still read a non-optional field for each setting, exactly as
|
|
41
|
+
* before this token's type was tightened.
|
|
42
|
+
*/
|
|
43
|
+
const TW_THEME_CONFIG = new InjectionToken('TW_THEME_CONFIG');
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Renamed to {@link TW_THEME_CONFIG} for consistency with every
|
|
46
|
+
* other ngx-tw injection token. This is the *same token instance*, not a copy —
|
|
47
|
+
* providing under either name and injecting under the other resolves — so the
|
|
48
|
+
* rename is safe to adopt incrementally. Removed in the next major.
|
|
49
|
+
*/
|
|
50
|
+
const THEME_CONFIG = TW_THEME_CONFIG;
|
|
51
|
+
/**
|
|
52
|
+
* Registers {@link ThemeService}, a {@link TW_THEME_CONFIG} value built by merging
|
|
53
|
+
* `config` over {@link DEFAULT_TW_THEME_CONFIG}, and an environment initializer
|
|
54
|
+
* that constructs the service at bootstrap. Call once in the app's environment
|
|
55
|
+
* providers.
|
|
56
|
+
*
|
|
57
|
+
* The initializer is what makes the call self-sufficient. Everything that
|
|
58
|
+
* applies a theme lives in `ThemeService`'s field initialisers and constructor
|
|
59
|
+
* effect, so without it the stored preference was read — and `data-theme`
|
|
60
|
+
* written — only once something happened to `inject(ThemeService)`. An app
|
|
61
|
+
* whose theme toggle sits in a lazily-loaded route therefore rendered every
|
|
62
|
+
* other route with no `data-theme` at all, silently falling back to the
|
|
63
|
+
* `prefers-color-scheme` CSS branch; the failure looked intermittent because
|
|
64
|
+
* it disappeared whenever the stored choice agreed with the OS. Injecting the
|
|
65
|
+
* service yourself is still supported and is idempotent.
|
|
66
|
+
*
|
|
67
|
+
* Keys explicitly set to `undefined` are dropped before merging. Root
|
|
68
|
+
* `tsconfig.json` does not set `exactOptionalPropertyTypes`, so
|
|
69
|
+
* `provideTheme({ storageKey: env.themeKey })` type-checks even when
|
|
70
|
+
* `env.themeKey` is `string | undefined` — and a plain spread would then write
|
|
71
|
+
* `undefined` into a field `TwThemeConfig` types as `string`, making
|
|
72
|
+
* `ThemeService` persist under the literal `localStorage` key `"undefined"`
|
|
73
|
+
* (and, for `attribute`, call `setAttribute("undefined", …)`).
|
|
74
|
+
*
|
|
75
|
+
* Providing the service does **not** write to `localStorage`; only an explicit
|
|
76
|
+
* `setTheme()` / `cycleTheme()` does. For a flash-free first paint when the
|
|
77
|
+
* stored choice disagrees with the OS, pair this with
|
|
78
|
+
* {@link TW_THEME_BOOTSTRAP_SCRIPT} in `index.html`.
|
|
23
79
|
*/
|
|
24
80
|
function provideTheme(config) {
|
|
81
|
+
const overrides = Object.fromEntries(Object.entries(config ?? {}).filter(([, value]) => value !== undefined));
|
|
25
82
|
return makeEnvironmentProviders([
|
|
26
83
|
{
|
|
27
|
-
provide:
|
|
28
|
-
useValue: { ...DEFAULT_TW_THEME_CONFIG, ...
|
|
84
|
+
provide: TW_THEME_CONFIG,
|
|
85
|
+
useValue: { ...DEFAULT_TW_THEME_CONFIG, ...overrides },
|
|
29
86
|
},
|
|
30
87
|
ThemeService,
|
|
88
|
+
provideEnvironmentInitializer(() => {
|
|
89
|
+
inject(ThemeService);
|
|
90
|
+
}),
|
|
31
91
|
]);
|
|
32
92
|
}
|
|
33
93
|
|
|
94
|
+
/** OS colour-scheme query backing `'system'` resolution. */
|
|
95
|
+
const DARK_QUERY = '(prefers-color-scheme: dark)';
|
|
96
|
+
/** OS contrast query backing `'system'` → `'high-contrast'` / `'high-contrast-dark'` resolution. */
|
|
97
|
+
const CONTRAST_QUERY = '(prefers-contrast: more)';
|
|
34
98
|
/**
|
|
35
99
|
* Stateful runtime service that owns the active theme, reacts to OS
|
|
36
|
-
* `prefers-color-scheme` changes, persists
|
|
37
|
-
* `localStorage`, and writes the resolved theme onto the
|
|
38
|
-
* target as a `data-theme` attribute.
|
|
100
|
+
* `prefers-color-scheme` / `prefers-contrast` changes, persists an explicit
|
|
101
|
+
* user selection to `localStorage`, and writes the resolved theme onto the
|
|
102
|
+
* configured DOM target as a `data-theme` attribute.
|
|
39
103
|
*
|
|
40
104
|
* The selected {@link theme} may be `'system'` (defer to the OS); the
|
|
41
105
|
* {@link resolvedTheme} computed from it is always one of `'light'`,
|
|
42
|
-
* `'dark'`, or `'high-contrast'` — never `'system'`.
|
|
43
|
-
*
|
|
106
|
+
* `'dark'`, `'high-contrast'`, or `'high-contrast-dark'` — never `'system'`.
|
|
107
|
+
* Register via
|
|
108
|
+
* {@link provideTheme} in the app's environment providers; `provideTheme`
|
|
109
|
+
* also constructs the service at bootstrap, so injecting it is only needed
|
|
110
|
+
* to read or change the theme.
|
|
111
|
+
*
|
|
112
|
+
* Storage is written **only** by {@link setTheme} / {@link cycleTheme}.
|
|
113
|
+
* Merely providing the service never touches `localStorage`, so the
|
|
114
|
+
* configured `defaultTheme` keeps applying until the user actually picks
|
|
115
|
+
* something, and apps under a storage-consent flow can provide the service
|
|
116
|
+
* before consent is granted.
|
|
44
117
|
*/
|
|
45
118
|
class ThemeService {
|
|
46
|
-
config = inject(
|
|
119
|
+
config = inject(TW_THEME_CONFIG);
|
|
47
120
|
document = inject(DOCUMENT);
|
|
48
121
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
49
122
|
ngZone = inject(NgZone);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
123
|
+
darkQuery = null;
|
|
124
|
+
contrastQuery = null;
|
|
125
|
+
// One listener for both queries: the resolution rule reads them together, so
|
|
126
|
+
// a handler that only knew its own query would clobber the other's decision
|
|
127
|
+
// on the next OS tick. The event payload is deliberately ignored — a
|
|
128
|
+
// `MediaQueryList`'s `matches` is already updated when `change` fires, so
|
|
129
|
+
// re-reading both is the single source of truth.
|
|
130
|
+
mediaListener = () => {
|
|
131
|
+
this.systemTheme.set(this.detectSystemTheme());
|
|
53
132
|
};
|
|
54
133
|
/** The user-selected theme (may be `'system'`). */
|
|
55
134
|
theme = signal(this.loadInitialTheme(), /* @ts-ignore */
|
|
56
135
|
...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
57
|
-
/** The OS color
|
|
136
|
+
/** The OS appearance preference — `prefers-color-scheme` plus `prefers-contrast`. */
|
|
58
137
|
systemTheme = signal(this.detectSystemTheme(), /* @ts-ignore */
|
|
59
138
|
...(ngDevMode ? [{ debugName: "systemTheme" }] : /* istanbul ignore next */ []));
|
|
60
139
|
/** The resolved theme actually applied to the DOM (never `'system'`). */
|
|
@@ -63,14 +142,42 @@ class ThemeService {
|
|
|
63
142
|
return t === 'system' ? this.systemTheme() : t;
|
|
64
143
|
}, /* @ts-ignore */
|
|
65
144
|
...(ngDevMode ? [{ debugName: "resolvedTheme" }] : /* istanbul ignore next */ []));
|
|
66
|
-
/**
|
|
67
|
-
|
|
145
|
+
/**
|
|
146
|
+
* True when the resolved theme is a **dark** scheme — `'dark'` or
|
|
147
|
+
* `'high-contrast-dark'`.
|
|
148
|
+
*
|
|
149
|
+
* It answers the appearance question, not "which scheme": the canonical use
|
|
150
|
+
* is picking a colour that has to sit on the page background (a chart grid,
|
|
151
|
+
* a canvas fill), and dark high contrast needs the dark answer there just as
|
|
152
|
+
* much as plain dark does. It is therefore **not** mutually exclusive with
|
|
153
|
+
* {@link isHighContrast}; branch on {@link resolvedTheme} for one case.
|
|
154
|
+
*/
|
|
155
|
+
isDark = computed(() => {
|
|
156
|
+
const t = this.resolvedTheme();
|
|
157
|
+
return t === 'dark' || t === 'high-contrast-dark';
|
|
158
|
+
}, /* @ts-ignore */
|
|
68
159
|
...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
69
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* True when the resolved theme is exactly `'light'`.
|
|
162
|
+
*
|
|
163
|
+
* Deliberately narrower than {@link isDark}'s mirror image: the light-based
|
|
164
|
+
* `'high-contrast'` does **not** set it, because widening it would change
|
|
165
|
+
* what a shipped scheme reports to consumers already reading this flag.
|
|
166
|
+
* Use `!isDark()` for the appearance question.
|
|
167
|
+
*/
|
|
70
168
|
isLight = computed(() => this.resolvedTheme() === 'light', /* @ts-ignore */
|
|
71
169
|
...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
|
|
72
|
-
/**
|
|
73
|
-
|
|
170
|
+
/**
|
|
171
|
+
* True when the resolved theme is an **increased-contrast** scheme —
|
|
172
|
+
* `'high-contrast'` or `'high-contrast-dark'`.
|
|
173
|
+
*
|
|
174
|
+
* It answers the contrast question, so it can be true at the same time as
|
|
175
|
+
* {@link isDark}.
|
|
176
|
+
*/
|
|
177
|
+
isHighContrast = computed(() => {
|
|
178
|
+
const t = this.resolvedTheme();
|
|
179
|
+
return t === 'high-contrast' || t === 'high-contrast-dark';
|
|
180
|
+
}, /* @ts-ignore */
|
|
74
181
|
...(ngDevMode ? [{ debugName: "isHighContrast" }] : /* istanbul ignore next */ []));
|
|
75
182
|
/** Snapshot of the full theme state — selected, resolved, system, and boolean flags. */
|
|
76
183
|
state = computed(() => ({
|
|
@@ -84,38 +191,61 @@ class ThemeService {
|
|
|
84
191
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
85
192
|
constructor() {
|
|
86
193
|
if (this.isBrowser) {
|
|
87
|
-
// Register the media-query
|
|
88
|
-
// on OS
|
|
89
|
-
// bookkeeping for every system
|
|
194
|
+
// Register the media-query listeners outside the Angular zone — they fire
|
|
195
|
+
// on OS preference changes, not user interaction, and we don't want
|
|
196
|
+
// zone.js bookkeeping for every system tick. The listener calls
|
|
90
197
|
// `signal.set()`, which schedules its own change detection.
|
|
198
|
+
// Both `MediaQueryList`s already exist: the `systemTheme` field
|
|
199
|
+
// initialiser above ran `detectSystemTheme()`, which mints them. The
|
|
200
|
+
// `??=` keeps this correct if that ever stops being true.
|
|
91
201
|
this.ngZone.runOutsideAngular(() => {
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
202
|
+
this.darkQuery ??= window.matchMedia(DARK_QUERY);
|
|
203
|
+
this.contrastQuery ??= window.matchMedia(CONTRAST_QUERY);
|
|
204
|
+
this.darkQuery.addEventListener('change', this.mediaListener);
|
|
205
|
+
this.contrastQuery.addEventListener('change', this.mediaListener);
|
|
94
206
|
});
|
|
95
207
|
}
|
|
96
208
|
effect(() => {
|
|
97
|
-
|
|
98
|
-
const selected = this.theme();
|
|
99
|
-
this.applyToDocument(resolved);
|
|
100
|
-
this.persistTheme(selected);
|
|
209
|
+
this.applyToDocument(this.resolvedTheme());
|
|
101
210
|
});
|
|
102
211
|
}
|
|
103
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Sets the selected theme and persists it. Pass `'system'` to follow the OS
|
|
214
|
+
* preference.
|
|
215
|
+
*
|
|
216
|
+
* This is the only entry point that writes to `localStorage` — see the note
|
|
217
|
+
* on the class.
|
|
218
|
+
*/
|
|
104
219
|
setTheme(theme) {
|
|
105
220
|
this.theme.set(theme);
|
|
221
|
+
this.persistTheme(theme);
|
|
106
222
|
}
|
|
107
|
-
/** Advances the selected theme to the next entry in {@link TW_THEMES}, wrapping around. */
|
|
223
|
+
/** Advances the selected theme to the next entry in {@link TW_THEMES}, wrapping around, and persists it. */
|
|
108
224
|
cycleTheme() {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
this.theme.set(TW_THEMES[(idx + 1) % TW_THEMES.length]);
|
|
225
|
+
const idx = TW_THEMES.indexOf(this.theme());
|
|
226
|
+
this.setTheme(TW_THEMES[(idx + 1) % TW_THEMES.length]);
|
|
112
227
|
}
|
|
113
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Writes the configured theme attribute onto an arbitrary element, scoping
|
|
230
|
+
* that subtree to the given theme.
|
|
231
|
+
*
|
|
232
|
+
* Each of the four schemes ships an element-agnostic `[data-theme=…]` block
|
|
233
|
+
* (`_light.css`, `_dark.css`, `_high-contrast.css`,
|
|
234
|
+
* `_high-contrast-dark.css`), so the tokens really do re-resolve on the
|
|
235
|
+
* element and cascade into its descendants — including back to `'light'`
|
|
236
|
+
* from inside a dark page.
|
|
237
|
+
*
|
|
238
|
+
* Caveat: those CSS blocks key off the literal `data-theme` attribute. If
|
|
239
|
+
* `provideTheme({ attribute })` renamed it, this method writes the renamed
|
|
240
|
+
* attribute and the shipped stylesheet will not react — a custom attribute
|
|
241
|
+
* only works with matching custom CSS.
|
|
242
|
+
*/
|
|
114
243
|
applyToElement(element, theme) {
|
|
115
244
|
element.setAttribute(this.config.attribute, theme);
|
|
116
245
|
}
|
|
117
246
|
ngOnDestroy() {
|
|
118
|
-
this.
|
|
247
|
+
this.darkQuery?.removeEventListener('change', this.mediaListener);
|
|
248
|
+
this.contrastQuery?.removeEventListener('change', this.mediaListener);
|
|
119
249
|
}
|
|
120
250
|
loadInitialTheme() {
|
|
121
251
|
if (!this.isBrowser)
|
|
@@ -131,10 +261,35 @@ class ThemeService {
|
|
|
131
261
|
}
|
|
132
262
|
return this.config.defaultTheme;
|
|
133
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Resolves `'system'` against the OS.
|
|
266
|
+
*
|
|
267
|
+
* The two preferences are treated as independent axes and composed, not
|
|
268
|
+
* ranked: `prefers-color-scheme` picks the appearance and
|
|
269
|
+
* `prefers-contrast: more` picks the contrast, giving the full 2×2 —
|
|
270
|
+
* `light` / `dark` / `high-contrast` / `high-contrast-dark`.
|
|
271
|
+
*
|
|
272
|
+
* This is only correct because the library ships **both** high-contrast
|
|
273
|
+
* ramps. While `_high-contrast.css` was the only one, contrast had to be
|
|
274
|
+
* suppressed whenever the OS also asked for dark, or a user running dark +
|
|
275
|
+
* increased contrast — a common pairing — would have been moved from a dark
|
|
276
|
+
* surface onto a white one. `_high-contrast-dark.css` removed that
|
|
277
|
+
* constraint, so the ranking is gone; if a future change ever drops one of
|
|
278
|
+
* the two ramps, this method has to go back to ranking them.
|
|
279
|
+
*
|
|
280
|
+
* Both queries are minted here (not in the constructor) because the
|
|
281
|
+
* `systemTheme` field initialiser calls this before the constructor body
|
|
282
|
+
* runs; the constructor then attaches `change` listeners to the same two
|
|
283
|
+
* objects, so the resolution stays live rather than read-once.
|
|
284
|
+
*/
|
|
134
285
|
detectSystemTheme() {
|
|
135
286
|
if (!this.isBrowser)
|
|
136
287
|
return 'light';
|
|
137
|
-
|
|
288
|
+
const prefersDark = (this.darkQuery ??= window.matchMedia(DARK_QUERY)).matches;
|
|
289
|
+
const prefersMoreContrast = (this.contrastQuery ??= window.matchMedia(CONTRAST_QUERY)).matches;
|
|
290
|
+
if (prefersMoreContrast)
|
|
291
|
+
return prefersDark ? 'high-contrast-dark' : 'high-contrast';
|
|
292
|
+
return prefersDark ? 'dark' : 'light';
|
|
138
293
|
}
|
|
139
294
|
applyToDocument(theme) {
|
|
140
295
|
if (!this.isBrowser)
|
|
@@ -162,7 +317,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
162
317
|
}], ctorParameters: () => [] });
|
|
163
318
|
|
|
164
319
|
class ThemeDirective {
|
|
165
|
-
/**
|
|
320
|
+
/**
|
|
321
|
+
* Scopes a subtree to a specific resolved theme by writing `data-theme` on
|
|
322
|
+
* the host. Required.
|
|
323
|
+
*
|
|
324
|
+
* Works in every direction: each of the four schemes ships an
|
|
325
|
+
* element-agnostic `[data-theme="…"]` block (`_light.css`, `_dark.css`,
|
|
326
|
+
* `_high-contrast.css`, `_high-contrast-dark.css`), so a `'light'` pane
|
|
327
|
+
* inside a dark page re-resolves the tokens rather than inheriting the
|
|
328
|
+
* ancestor's.
|
|
329
|
+
*
|
|
330
|
+
* The attribute name is the literal `data-theme` that the shipped CSS keys
|
|
331
|
+
* off — it deliberately ignores `provideTheme({ attribute })`, since a
|
|
332
|
+
* renamed attribute matches none of those blocks.
|
|
333
|
+
*/
|
|
166
334
|
twTheme = input.required(/* @ts-ignore */
|
|
167
335
|
...(ngDevMode ? [{ debugName: "twTheme" }] : /* istanbul ignore next */ []));
|
|
168
336
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ThemeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
@@ -178,9 +346,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
178
346
|
}]
|
|
179
347
|
}], propDecorators: { twTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "twTheme", required: true }] }] } });
|
|
180
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Body of an inline `<head>` script that applies a previously persisted theme
|
|
351
|
+
* before the app bundle runs, eliminating the flash of the wrong theme on
|
|
352
|
+
* reload.
|
|
353
|
+
*
|
|
354
|
+
* `ThemeService` cannot cover this case: it is JavaScript in the application
|
|
355
|
+
* bundle, so the browser has already painted by the time it writes
|
|
356
|
+
* `data-theme` — and `provideTheme`'s environment initializer does not change
|
|
357
|
+
* that. The CSS `@media (prefers-color-scheme: dark)` fallback covers users
|
|
358
|
+
* who never chose a theme; the flash is what an *explicit* choice that
|
|
359
|
+
* disagrees with the OS looks like — light chosen on a dark machine, or
|
|
360
|
+
* either high-contrast scheme chosen on a machine not already asking for
|
|
361
|
+
* increased contrast (the CSS has a `prefers-color-scheme` branch but
|
|
362
|
+
* deliberately no `prefers-contrast` one, so neither contrast scheme is
|
|
363
|
+
* reachable without an explicit `data-theme`).
|
|
364
|
+
*
|
|
365
|
+
* The string is built from {@link DEFAULT_TW_THEME_CONFIG}, so the storage key
|
|
366
|
+
* and attribute cannot drift from what `ThemeService` actually uses — that
|
|
367
|
+
* drift is the reason this ships as code rather than as a README snippet
|
|
368
|
+
* alone. Copy the literal script into `index.html`, or interpolate this
|
|
369
|
+
* constant during an SSR / index transform:
|
|
370
|
+
*
|
|
371
|
+
* ```html
|
|
372
|
+
* <head>
|
|
373
|
+
* <script>try{var t=localStorage.getItem('ngx-tw-theme');if(t&&t!=='system')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
|
|
374
|
+
* </head>
|
|
375
|
+
* ```
|
|
376
|
+
*
|
|
377
|
+
* Notes on the behaviour it deliberately does *not* have:
|
|
378
|
+
* - A stored `'system'` writes nothing, so the CSS `prefers-color-scheme`
|
|
379
|
+
* fallback keeps deciding — which is exactly what `'system'` means.
|
|
380
|
+
* - It assumes the default `target: 'documentElement'`. A `<head>` script runs
|
|
381
|
+
* before `<body>` exists, so `target: 'body'` cannot be bootstrapped this
|
|
382
|
+
* way.
|
|
383
|
+
* - If you overrode `storageKey` or `attribute` via `provideTheme`, adapt the
|
|
384
|
+
* literal snippet to match; this constant only encodes the defaults.
|
|
385
|
+
* - It is dependency-free and wrapped in `try`/`catch`, so a browser with
|
|
386
|
+
* storage blocked falls through to the CSS behaviour instead of throwing
|
|
387
|
+
* before the app loads.
|
|
388
|
+
*/
|
|
389
|
+
const TW_THEME_BOOTSTRAP_SCRIPT = `try{var t=localStorage.getItem(${JSON.stringify(DEFAULT_TW_THEME_CONFIG.storageKey)});` +
|
|
390
|
+
`if(t&&t!=='system')document.documentElement.setAttribute(` +
|
|
391
|
+
`${JSON.stringify(DEFAULT_TW_THEME_CONFIG.attribute)},t)}catch(e){}`;
|
|
392
|
+
|
|
181
393
|
/**
|
|
182
394
|
* Generated bundle index. Do not edit.
|
|
183
395
|
*/
|
|
184
396
|
|
|
185
|
-
export { DEFAULT_TW_THEME_CONFIG, THEME_CONFIG, TW_RESOLVED_THEMES, TW_THEMES, ThemeDirective, ThemeService, provideTheme };
|
|
397
|
+
export { DEFAULT_TW_THEME_CONFIG, THEME_CONFIG, TW_RESOLVED_THEMES, TW_THEMES, TW_THEME_BOOTSTRAP_SCRIPT, TW_THEME_CONFIG, ThemeDirective, ThemeService, provideTheme };
|
|
186
398
|
//# sourceMappingURL=cdevhub-ngx-tw-theme.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdevhub-ngx-tw-theme.mjs","sources":["../../../projects/ngx-tw/theme/theme.types.ts","../../../projects/ngx-tw/theme/theme.config.ts","../../../projects/ngx-tw/theme/theme.service.ts","../../../projects/ngx-tw/theme/theme.directive.ts","../../../projects/ngx-tw/theme/cdevhub-ngx-tw-theme.ts"],"sourcesContent":["/** The user-selectable theme. `'system'` defers to the OS `prefers-color-scheme` setting. */\nexport type TwTheme = 'light' | 'dark' | 'high-contrast' | 'system';\n\n/** The theme actually applied to the DOM after resolving `'system'` against the OS preference. */\nexport type TwResolvedTheme = 'light' | 'dark' | 'high-contrast';\n\n/** Ordered list of every {@link TwTheme} value, used by `cycleTheme()` and for UI iteration. */\nexport const TW_THEMES = ['light', 'dark', 'high-contrast', 'system'] as const satisfies readonly TwTheme[];\n\n/** Ordered list of every {@link TwResolvedTheme} value (i.e. {@link TW_THEMES} minus `'system'`). */\nexport const TW_RESOLVED_THEMES = ['light', 'dark', 'high-contrast'] as const satisfies readonly TwResolvedTheme[];\n\n/** Runtime configuration for {@link provideTheme}; controls storage, attribute name, target element, and default. */\nexport interface TwThemeConfig {\n /** The default theme when no preference is stored. Defaults to `'system'`. */\n defaultTheme: TwTheme;\n /** localStorage key for persisting theme preference. Defaults to `'ngx-tw-theme'`. */\n storageKey: string;\n /** The HTML attribute written to the target element. Defaults to `'data-theme'`. */\n attribute: string;\n /** Which element receives the theme attribute. Defaults to `'documentElement'`. */\n target: 'documentElement' | 'body';\n}\n\n/** Composite snapshot of `ThemeService` state — selected, resolved, system, and boolean flags. */\nexport interface TwThemeState {\n /** The user-selected theme — may be `'system'`. */\n readonly theme: TwTheme;\n /** The theme actually applied to the DOM — never `'system'`. */\n readonly resolvedTheme: TwResolvedTheme;\n /** The OS color-scheme preference detected via `prefers-color-scheme`. */\n readonly systemTheme: TwResolvedTheme;\n /** True when {@link resolvedTheme} is `'dark'`. */\n readonly isDark: boolean;\n /** True when {@link resolvedTheme} is `'light'`. */\n readonly isLight: boolean;\n /** True when {@link resolvedTheme} is `'high-contrast'`. */\n readonly isHighContrast: boolean;\n}\n\n/** Built-in defaults merged under any user-provided {@link TwThemeConfig} by `provideTheme()`. */\nexport const DEFAULT_TW_THEME_CONFIG: TwThemeConfig = {\n defaultTheme: 'system',\n storageKey: 'ngx-tw-theme',\n attribute: 'data-theme',\n target: 'documentElement',\n};\n","import { InjectionToken, makeEnvironmentProviders, type EnvironmentProviders } from '@angular/core';\nimport { type TwThemeConfig, DEFAULT_TW_THEME_CONFIG } from './theme.types';\nimport { ThemeService } from './theme.service';\n\n/** Injection token carrying the resolved {@link TwThemeConfig} (storage key, attribute, target element, default theme). */\nexport const THEME_CONFIG = new InjectionToken<TwThemeConfig>('THEME_CONFIG');\n\n/**\n * Registers {@link ThemeService} and a {@link THEME_CONFIG} value built by\n * merging `config` over {@link DEFAULT_TW_THEME_CONFIG}. Call once in the\n * app's environment providers.\n */\nexport function provideTheme(config?: Partial<TwThemeConfig>): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: THEME_CONFIG,\n useValue: { ...DEFAULT_TW_THEME_CONFIG, ...config },\n },\n ThemeService,\n ]);\n}\n","import {\n Injectable,\n NgZone,\n computed,\n effect,\n inject,\n signal,\n PLATFORM_ID,\n type OnDestroy,\n} from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { THEME_CONFIG } from './theme.config';\nimport { TW_THEMES, type TwTheme, type TwResolvedTheme, type TwThemeState } from './theme.types';\n\n/**\n * Stateful runtime service that owns the active theme, reacts to OS\n * `prefers-color-scheme` changes, persists the user selection to\n * `localStorage`, and writes the resolved theme onto the configured DOM\n * target as a `data-theme` attribute.\n *\n * The selected {@link theme} may be `'system'` (defer to the OS); the\n * {@link resolvedTheme} computed from it is always one of `'light'`,\n * `'dark'`, or `'high-contrast'` — never `'system'`. Register via\n * {@link provideTheme} in the app's environment providers.\n */\n@Injectable()\nexport class ThemeService implements OnDestroy {\n private readonly config = inject(THEME_CONFIG);\n private readonly document = inject(DOCUMENT);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n private readonly ngZone = inject(NgZone);\n\n private mediaQuery: MediaQueryList | null = null;\n private readonly mediaListener = (e: MediaQueryListEvent) => {\n this.systemTheme.set(e.matches ? 'dark' : 'light');\n };\n\n /** The user-selected theme (may be `'system'`). */\n readonly theme = signal<TwTheme>(this.loadInitialTheme());\n\n /** The OS color scheme preference. */\n readonly systemTheme = signal<TwResolvedTheme>(this.detectSystemTheme());\n\n /** The resolved theme actually applied to the DOM (never `'system'`). */\n readonly resolvedTheme = computed<TwResolvedTheme>(() => {\n const t = this.theme();\n return t === 'system' ? this.systemTheme() : t;\n });\n\n /** True when the resolved theme is `'dark'`. */\n readonly isDark = computed(() => this.resolvedTheme() === 'dark');\n /** True when the resolved theme is `'light'`. */\n readonly isLight = computed(() => this.resolvedTheme() === 'light');\n /** True when the resolved theme is `'high-contrast'`. */\n readonly isHighContrast = computed(() => this.resolvedTheme() === 'high-contrast');\n\n /** Snapshot of the full theme state — selected, resolved, system, and boolean flags. */\n readonly state = computed<TwThemeState>(() => ({\n theme: this.theme(),\n resolvedTheme: this.resolvedTheme(),\n systemTheme: this.systemTheme(),\n isDark: this.isDark(),\n isLight: this.isLight(),\n isHighContrast: this.isHighContrast(),\n }));\n\n constructor() {\n if (this.isBrowser) {\n // Register the media-query listener outside the Angular zone — it fires\n // on OS theme changes, not user interaction, and we don't want zone.js\n // bookkeeping for every system colour-scheme tick. The listener calls\n // `signal.set()`, which schedules its own change detection.\n this.ngZone.runOutsideAngular(() => {\n this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n this.mediaQuery.addEventListener('change', this.mediaListener);\n });\n }\n\n effect(() => {\n const resolved = this.resolvedTheme();\n const selected = this.theme();\n this.applyToDocument(resolved);\n this.persistTheme(selected);\n });\n }\n\n /** Sets the selected theme. Pass `'system'` to follow the OS preference. */\n setTheme(theme: TwTheme): void {\n this.theme.set(theme);\n }\n\n /** Advances the selected theme to the next entry in {@link TW_THEMES}, wrapping around. */\n cycleTheme(): void {\n const current = this.theme();\n const idx = TW_THEMES.indexOf(current);\n this.theme.set(TW_THEMES[(idx + 1) % TW_THEMES.length]);\n }\n\n /** Writes the configured theme attribute onto an arbitrary element — used to scope themes to a subtree. */\n applyToElement(element: HTMLElement, theme: TwResolvedTheme): void {\n element.setAttribute(this.config.attribute, theme);\n }\n\n ngOnDestroy(): void {\n this.mediaQuery?.removeEventListener('change', this.mediaListener);\n }\n\n private loadInitialTheme(): TwTheme {\n if (!this.isBrowser) return this.config.defaultTheme;\n try {\n const stored = localStorage.getItem(this.config.storageKey);\n if (stored && (TW_THEMES as readonly string[]).includes(stored)) {\n return stored as TwTheme;\n }\n } catch {\n /* localStorage unavailable */\n }\n return this.config.defaultTheme;\n }\n\n private detectSystemTheme(): TwResolvedTheme {\n if (!this.isBrowser) return 'light';\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n\n private applyToDocument(theme: TwResolvedTheme): void {\n if (!this.isBrowser) return;\n const target =\n this.config.target === 'documentElement'\n ? this.document.documentElement\n : this.document.body;\n target.setAttribute(this.config.attribute, theme);\n }\n\n private persistTheme(theme: TwTheme): void {\n if (!this.isBrowser) return;\n try {\n localStorage.setItem(this.config.storageKey, theme);\n } catch {\n /* localStorage unavailable */\n }\n }\n}\n","import { Directive, input } from '@angular/core';\nimport type { TwResolvedTheme } from './theme.types';\n\n@Directive({\n selector: '[twTheme]',\n host: {\n '[attr.data-theme]': 'twTheme()',\n },\n})\nexport class ThemeDirective {\n /** Scopes a subtree to a specific resolved theme by writing `data-theme` on the host. Required. */\n readonly twTheme = input.required<TwResolvedTheme>();\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cdevhub-ngx-tw-theme.mjs","sources":["../../../projects/ngx-tw/theme/theme.types.ts","../../../projects/ngx-tw/theme/theme.config.ts","../../../projects/ngx-tw/theme/theme.service.ts","../../../projects/ngx-tw/theme/theme.directive.ts","../../../projects/ngx-tw/theme/theme.bootstrap.ts","../../../projects/ngx-tw/theme/cdevhub-ngx-tw-theme.ts"],"sourcesContent":["/**\n * The user-selectable theme. `'system'` defers to the OS, resolving\n * `prefers-color-scheme` and `prefers-contrast` as two independent axes: dark\n * plus increased contrast lands on `'high-contrast-dark'`, light plus increased\n * contrast on `'high-contrast'`.\n */\nexport type TwTheme = 'light' | 'dark' | 'high-contrast' | 'high-contrast-dark' | 'system';\n\n/**\n * The theme actually applied to the DOM after resolving `'system'` against the\n * OS preference.\n *\n * The two axes are appearance (light / dark) and contrast (normal / increased),\n * so the four values are their product: `'high-contrast'` is the light-based\n * increased-contrast scheme and `'high-contrast-dark'` the dark-based one.\n */\nexport type TwResolvedTheme = 'light' | 'dark' | 'high-contrast' | 'high-contrast-dark';\n\n/** Ordered list of every {@link TwTheme} value, used by `cycleTheme()` and for UI iteration. */\nexport const TW_THEMES = [\n 'light',\n 'dark',\n 'high-contrast',\n 'high-contrast-dark',\n 'system',\n] as const satisfies readonly TwTheme[];\n\n/** Ordered list of every {@link TwResolvedTheme} value (i.e. {@link TW_THEMES} minus `'system'`). */\nexport const TW_RESOLVED_THEMES = [\n 'light',\n 'dark',\n 'high-contrast',\n 'high-contrast-dark',\n] as const satisfies readonly TwResolvedTheme[];\n\n/**\n * Runtime configuration for {@link provideTheme}; controls storage, attribute\n * name, target element, and default.\n *\n * Every member is optional. This interface only ever reaches consumers through\n * `provideTheme(config?: Partial<TwThemeConfig>)`, which fills each unset key\n * from {@link DEFAULT_TW_THEME_CONFIG} — so a consumer holding a config object\n * typed as `TwThemeConfig` must not be forced to restate keys they do not\n * override, and adding a member in a future minor must not break them. The\n * resolved value handed to {@link TW_THEME_CONFIG} is `Required<TwThemeConfig>`.\n */\nexport interface TwThemeConfig {\n /** The default theme when no preference is stored. Defaults to `'system'`. */\n defaultTheme?: TwTheme;\n /**\n * localStorage key for persisting an explicit theme choice. Defaults to\n * `'ngx-tw-theme'`. Only `setTheme()` / `cycleTheme()` write it — providing\n * the service never does.\n */\n storageKey?: string;\n /** The HTML attribute written to the target element. Defaults to `'data-theme'`. */\n attribute?: string;\n /** Which element receives the theme attribute. Defaults to `'documentElement'`. */\n target?: 'documentElement' | 'body';\n}\n\n/**\n * Composite snapshot of `ThemeService` state — selected, resolved, system, and\n * boolean flags.\n *\n * The three flags are **not** mutually exclusive: `'high-contrast-dark'` sets\n * both {@link isDark} and {@link isHighContrast}, because it is dark *and*\n * high contrast. Branch on {@link resolvedTheme} when you need one case.\n */\nexport interface TwThemeState {\n /** The user-selected theme — may be `'system'`. */\n readonly theme: TwTheme;\n /** The theme actually applied to the DOM — never `'system'`. */\n readonly resolvedTheme: TwResolvedTheme;\n /** The OS appearance preference detected via `prefers-color-scheme` and `prefers-contrast`. */\n readonly systemTheme: TwResolvedTheme;\n /** True when {@link resolvedTheme} is a dark scheme — `'dark'` or `'high-contrast-dark'`. */\n readonly isDark: boolean;\n /** True when {@link resolvedTheme} is exactly `'light'`; the light-based `'high-contrast'` does not set it. */\n readonly isLight: boolean;\n /** True when {@link resolvedTheme} is an increased-contrast scheme — `'high-contrast'` or `'high-contrast-dark'`. */\n readonly isHighContrast: boolean;\n}\n\n/**\n * Built-in defaults merged under any user-provided {@link TwThemeConfig} by\n * `provideTheme()`. Typed `Required<TwThemeConfig>` so readers keep a\n * non-optional `string` / `TwTheme` for every field even though the interface\n * itself is all-optional.\n */\nexport const DEFAULT_TW_THEME_CONFIG: Required<TwThemeConfig> = {\n defaultTheme: 'system',\n storageKey: 'ngx-tw-theme',\n attribute: 'data-theme',\n target: 'documentElement',\n};\n","import {\n InjectionToken,\n inject,\n makeEnvironmentProviders,\n provideEnvironmentInitializer,\n type EnvironmentProviders,\n} from '@angular/core';\nimport { type TwThemeConfig, DEFAULT_TW_THEME_CONFIG } from './theme.types';\nimport { ThemeService } from './theme.service';\n\n/**\n * Injection token carrying the resolved {@link TwThemeConfig} (storage key,\n * attribute, target element, default theme).\n *\n * The value is `Required<TwThemeConfig>`: `TwThemeConfig`'s members are\n * optional so consumers can pass a partial config, but `provideTheme` fills\n * every key from {@link DEFAULT_TW_THEME_CONFIG} before providing it. Injectors\n * therefore still read a non-optional field for each setting, exactly as\n * before this token's type was tightened.\n */\nexport const TW_THEME_CONFIG = new InjectionToken<Required<TwThemeConfig>>('TW_THEME_CONFIG');\n\n/**\n * @deprecated Renamed to {@link TW_THEME_CONFIG} for consistency with every\n * other ngx-tw injection token. This is the *same token instance*, not a copy —\n * providing under either name and injecting under the other resolves — so the\n * rename is safe to adopt incrementally. Removed in the next major.\n */\nexport const THEME_CONFIG = TW_THEME_CONFIG;\n\n/**\n * Registers {@link ThemeService}, a {@link TW_THEME_CONFIG} value built by merging\n * `config` over {@link DEFAULT_TW_THEME_CONFIG}, and an environment initializer\n * that constructs the service at bootstrap. Call once in the app's environment\n * providers.\n *\n * The initializer is what makes the call self-sufficient. Everything that\n * applies a theme lives in `ThemeService`'s field initialisers and constructor\n * effect, so without it the stored preference was read — and `data-theme`\n * written — only once something happened to `inject(ThemeService)`. An app\n * whose theme toggle sits in a lazily-loaded route therefore rendered every\n * other route with no `data-theme` at all, silently falling back to the\n * `prefers-color-scheme` CSS branch; the failure looked intermittent because\n * it disappeared whenever the stored choice agreed with the OS. Injecting the\n * service yourself is still supported and is idempotent.\n *\n * Keys explicitly set to `undefined` are dropped before merging. Root\n * `tsconfig.json` does not set `exactOptionalPropertyTypes`, so\n * `provideTheme({ storageKey: env.themeKey })` type-checks even when\n * `env.themeKey` is `string | undefined` — and a plain spread would then write\n * `undefined` into a field `TwThemeConfig` types as `string`, making\n * `ThemeService` persist under the literal `localStorage` key `\"undefined\"`\n * (and, for `attribute`, call `setAttribute(\"undefined\", …)`).\n *\n * Providing the service does **not** write to `localStorage`; only an explicit\n * `setTheme()` / `cycleTheme()` does. For a flash-free first paint when the\n * stored choice disagrees with the OS, pair this with\n * {@link TW_THEME_BOOTSTRAP_SCRIPT} in `index.html`.\n */\nexport function provideTheme(config?: Partial<TwThemeConfig>): EnvironmentProviders {\n const overrides = Object.fromEntries(\n Object.entries(config ?? {}).filter(([, value]) => value !== undefined),\n );\n return makeEnvironmentProviders([\n {\n provide: TW_THEME_CONFIG,\n useValue: { ...DEFAULT_TW_THEME_CONFIG, ...overrides } as Required<TwThemeConfig>,\n },\n ThemeService,\n provideEnvironmentInitializer(() => {\n inject(ThemeService);\n }),\n ]);\n}\n","import {\n Injectable,\n NgZone,\n computed,\n effect,\n inject,\n signal,\n PLATFORM_ID,\n type OnDestroy,\n} from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { TW_THEME_CONFIG } from './theme.config';\nimport { TW_THEMES, type TwTheme, type TwResolvedTheme, type TwThemeState } from './theme.types';\n\n/** OS colour-scheme query backing `'system'` resolution. */\nconst DARK_QUERY = '(prefers-color-scheme: dark)';\n\n/** OS contrast query backing `'system'` → `'high-contrast'` / `'high-contrast-dark'` resolution. */\nconst CONTRAST_QUERY = '(prefers-contrast: more)';\n\n/**\n * Stateful runtime service that owns the active theme, reacts to OS\n * `prefers-color-scheme` / `prefers-contrast` changes, persists an explicit\n * user selection to `localStorage`, and writes the resolved theme onto the\n * configured DOM target as a `data-theme` attribute.\n *\n * The selected {@link theme} may be `'system'` (defer to the OS); the\n * {@link resolvedTheme} computed from it is always one of `'light'`,\n * `'dark'`, `'high-contrast'`, or `'high-contrast-dark'` — never `'system'`.\n * Register via\n * {@link provideTheme} in the app's environment providers; `provideTheme`\n * also constructs the service at bootstrap, so injecting it is only needed\n * to read or change the theme.\n *\n * Storage is written **only** by {@link setTheme} / {@link cycleTheme}.\n * Merely providing the service never touches `localStorage`, so the\n * configured `defaultTheme` keeps applying until the user actually picks\n * something, and apps under a storage-consent flow can provide the service\n * before consent is granted.\n */\n@Injectable()\nexport class ThemeService implements OnDestroy {\n private readonly config = inject(TW_THEME_CONFIG);\n private readonly document = inject(DOCUMENT);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n private readonly ngZone = inject(NgZone);\n\n private darkQuery: MediaQueryList | null = null;\n private contrastQuery: MediaQueryList | null = null;\n\n // One listener for both queries: the resolution rule reads them together, so\n // a handler that only knew its own query would clobber the other's decision\n // on the next OS tick. The event payload is deliberately ignored — a\n // `MediaQueryList`'s `matches` is already updated when `change` fires, so\n // re-reading both is the single source of truth.\n private readonly mediaListener = () => {\n this.systemTheme.set(this.detectSystemTheme());\n };\n\n /** The user-selected theme (may be `'system'`). */\n readonly theme = signal<TwTheme>(this.loadInitialTheme());\n\n /** The OS appearance preference — `prefers-color-scheme` plus `prefers-contrast`. */\n readonly systemTheme = signal<TwResolvedTheme>(this.detectSystemTheme());\n\n /** The resolved theme actually applied to the DOM (never `'system'`). */\n readonly resolvedTheme = computed<TwResolvedTheme>(() => {\n const t = this.theme();\n return t === 'system' ? this.systemTheme() : t;\n });\n\n /**\n * True when the resolved theme is a **dark** scheme — `'dark'` or\n * `'high-contrast-dark'`.\n *\n * It answers the appearance question, not \"which scheme\": the canonical use\n * is picking a colour that has to sit on the page background (a chart grid,\n * a canvas fill), and dark high contrast needs the dark answer there just as\n * much as plain dark does. It is therefore **not** mutually exclusive with\n * {@link isHighContrast}; branch on {@link resolvedTheme} for one case.\n */\n readonly isDark = computed(() => {\n const t = this.resolvedTheme();\n return t === 'dark' || t === 'high-contrast-dark';\n });\n /**\n * True when the resolved theme is exactly `'light'`.\n *\n * Deliberately narrower than {@link isDark}'s mirror image: the light-based\n * `'high-contrast'` does **not** set it, because widening it would change\n * what a shipped scheme reports to consumers already reading this flag.\n * Use `!isDark()` for the appearance question.\n */\n readonly isLight = computed(() => this.resolvedTheme() === 'light');\n /**\n * True when the resolved theme is an **increased-contrast** scheme —\n * `'high-contrast'` or `'high-contrast-dark'`.\n *\n * It answers the contrast question, so it can be true at the same time as\n * {@link isDark}.\n */\n readonly isHighContrast = computed(() => {\n const t = this.resolvedTheme();\n return t === 'high-contrast' || t === 'high-contrast-dark';\n });\n\n /** Snapshot of the full theme state — selected, resolved, system, and boolean flags. */\n readonly state = computed<TwThemeState>(() => ({\n theme: this.theme(),\n resolvedTheme: this.resolvedTheme(),\n systemTheme: this.systemTheme(),\n isDark: this.isDark(),\n isLight: this.isLight(),\n isHighContrast: this.isHighContrast(),\n }));\n\n constructor() {\n if (this.isBrowser) {\n // Register the media-query listeners outside the Angular zone — they fire\n // on OS preference changes, not user interaction, and we don't want\n // zone.js bookkeeping for every system tick. The listener calls\n // `signal.set()`, which schedules its own change detection.\n // Both `MediaQueryList`s already exist: the `systemTheme` field\n // initialiser above ran `detectSystemTheme()`, which mints them. The\n // `??=` keeps this correct if that ever stops being true.\n this.ngZone.runOutsideAngular(() => {\n this.darkQuery ??= window.matchMedia(DARK_QUERY);\n this.contrastQuery ??= window.matchMedia(CONTRAST_QUERY);\n this.darkQuery.addEventListener('change', this.mediaListener);\n this.contrastQuery.addEventListener('change', this.mediaListener);\n });\n }\n\n effect(() => {\n this.applyToDocument(this.resolvedTheme());\n });\n }\n\n /**\n * Sets the selected theme and persists it. Pass `'system'` to follow the OS\n * preference.\n *\n * This is the only entry point that writes to `localStorage` — see the note\n * on the class.\n */\n setTheme(theme: TwTheme): void {\n this.theme.set(theme);\n this.persistTheme(theme);\n }\n\n /** Advances the selected theme to the next entry in {@link TW_THEMES}, wrapping around, and persists it. */\n cycleTheme(): void {\n const idx = TW_THEMES.indexOf(this.theme());\n this.setTheme(TW_THEMES[(idx + 1) % TW_THEMES.length]);\n }\n\n /**\n * Writes the configured theme attribute onto an arbitrary element, scoping\n * that subtree to the given theme.\n *\n * Each of the four schemes ships an element-agnostic `[data-theme=…]` block\n * (`_light.css`, `_dark.css`, `_high-contrast.css`,\n * `_high-contrast-dark.css`), so the tokens really do re-resolve on the\n * element and cascade into its descendants — including back to `'light'`\n * from inside a dark page.\n *\n * Caveat: those CSS blocks key off the literal `data-theme` attribute. If\n * `provideTheme({ attribute })` renamed it, this method writes the renamed\n * attribute and the shipped stylesheet will not react — a custom attribute\n * only works with matching custom CSS.\n */\n applyToElement(element: HTMLElement, theme: TwResolvedTheme): void {\n element.setAttribute(this.config.attribute, theme);\n }\n\n ngOnDestroy(): void {\n this.darkQuery?.removeEventListener('change', this.mediaListener);\n this.contrastQuery?.removeEventListener('change', this.mediaListener);\n }\n\n private loadInitialTheme(): TwTheme {\n if (!this.isBrowser) return this.config.defaultTheme;\n try {\n const stored = localStorage.getItem(this.config.storageKey);\n if (stored && (TW_THEMES as readonly string[]).includes(stored)) {\n return stored as TwTheme;\n }\n } catch {\n /* localStorage unavailable */\n }\n return this.config.defaultTheme;\n }\n\n /**\n * Resolves `'system'` against the OS.\n *\n * The two preferences are treated as independent axes and composed, not\n * ranked: `prefers-color-scheme` picks the appearance and\n * `prefers-contrast: more` picks the contrast, giving the full 2×2 —\n * `light` / `dark` / `high-contrast` / `high-contrast-dark`.\n *\n * This is only correct because the library ships **both** high-contrast\n * ramps. While `_high-contrast.css` was the only one, contrast had to be\n * suppressed whenever the OS also asked for dark, or a user running dark +\n * increased contrast — a common pairing — would have been moved from a dark\n * surface onto a white one. `_high-contrast-dark.css` removed that\n * constraint, so the ranking is gone; if a future change ever drops one of\n * the two ramps, this method has to go back to ranking them.\n *\n * Both queries are minted here (not in the constructor) because the\n * `systemTheme` field initialiser calls this before the constructor body\n * runs; the constructor then attaches `change` listeners to the same two\n * objects, so the resolution stays live rather than read-once.\n */\n private detectSystemTheme(): TwResolvedTheme {\n if (!this.isBrowser) return 'light';\n const prefersDark = (this.darkQuery ??= window.matchMedia(DARK_QUERY)).matches;\n const prefersMoreContrast = (this.contrastQuery ??= window.matchMedia(CONTRAST_QUERY)).matches;\n if (prefersMoreContrast) return prefersDark ? 'high-contrast-dark' : 'high-contrast';\n return prefersDark ? 'dark' : 'light';\n }\n\n private applyToDocument(theme: TwResolvedTheme): void {\n if (!this.isBrowser) return;\n const target =\n this.config.target === 'documentElement'\n ? this.document.documentElement\n : this.document.body;\n target.setAttribute(this.config.attribute, theme);\n }\n\n private persistTheme(theme: TwTheme): void {\n if (!this.isBrowser) return;\n try {\n localStorage.setItem(this.config.storageKey, theme);\n } catch {\n /* localStorage unavailable */\n }\n }\n}\n","import { Directive, input } from '@angular/core';\nimport type { TwResolvedTheme } from './theme.types';\n\n@Directive({\n selector: '[twTheme]',\n host: {\n '[attr.data-theme]': 'twTheme()',\n },\n})\nexport class ThemeDirective {\n /**\n * Scopes a subtree to a specific resolved theme by writing `data-theme` on\n * the host. Required.\n *\n * Works in every direction: each of the four schemes ships an\n * element-agnostic `[data-theme=\"…\"]` block (`_light.css`, `_dark.css`,\n * `_high-contrast.css`, `_high-contrast-dark.css`), so a `'light'` pane\n * inside a dark page re-resolves the tokens rather than inheriting the\n * ancestor's.\n *\n * The attribute name is the literal `data-theme` that the shipped CSS keys\n * off — it deliberately ignores `provideTheme({ attribute })`, since a\n * renamed attribute matches none of those blocks.\n */\n readonly twTheme = input.required<TwResolvedTheme>();\n}\n","import { DEFAULT_TW_THEME_CONFIG } from './theme.types';\n\n/**\n * Body of an inline `<head>` script that applies a previously persisted theme\n * before the app bundle runs, eliminating the flash of the wrong theme on\n * reload.\n *\n * `ThemeService` cannot cover this case: it is JavaScript in the application\n * bundle, so the browser has already painted by the time it writes\n * `data-theme` — and `provideTheme`'s environment initializer does not change\n * that. The CSS `@media (prefers-color-scheme: dark)` fallback covers users\n * who never chose a theme; the flash is what an *explicit* choice that\n * disagrees with the OS looks like — light chosen on a dark machine, or\n * either high-contrast scheme chosen on a machine not already asking for\n * increased contrast (the CSS has a `prefers-color-scheme` branch but\n * deliberately no `prefers-contrast` one, so neither contrast scheme is\n * reachable without an explicit `data-theme`).\n *\n * The string is built from {@link DEFAULT_TW_THEME_CONFIG}, so the storage key\n * and attribute cannot drift from what `ThemeService` actually uses — that\n * drift is the reason this ships as code rather than as a README snippet\n * alone. Copy the literal script into `index.html`, or interpolate this\n * constant during an SSR / index transform:\n *\n * ```html\n * <head>\n * <script>try{var t=localStorage.getItem('ngx-tw-theme');if(t&&t!=='system')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>\n * </head>\n * ```\n *\n * Notes on the behaviour it deliberately does *not* have:\n * - A stored `'system'` writes nothing, so the CSS `prefers-color-scheme`\n * fallback keeps deciding — which is exactly what `'system'` means.\n * - It assumes the default `target: 'documentElement'`. A `<head>` script runs\n * before `<body>` exists, so `target: 'body'` cannot be bootstrapped this\n * way.\n * - If you overrode `storageKey` or `attribute` via `provideTheme`, adapt the\n * literal snippet to match; this constant only encodes the defaults.\n * - It is dependency-free and wrapped in `try`/`catch`, so a browser with\n * storage blocked falls through to the CSS behaviour instead of throwing\n * before the app loads.\n */\nexport const TW_THEME_BOOTSTRAP_SCRIPT =\n `try{var t=localStorage.getItem(${JSON.stringify(DEFAULT_TW_THEME_CONFIG.storageKey)});` +\n `if(t&&t!=='system')document.documentElement.setAttribute(` +\n `${JSON.stringify(DEFAULT_TW_THEME_CONFIG.attribute)},t)}catch(e){}`;\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|