@cdevhub/ngx-tw 0.5.0 → 0.6.0

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.
Files changed (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
  49. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  51. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  53. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  54. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  55. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  57. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  59. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  61. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  63. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  65. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  67. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  68. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  69. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  71. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  73. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  75. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  77. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  79. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  81. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  83. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  84. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  85. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  86. package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
  87. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  88. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
  91. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  93. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  95. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  97. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  99. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  101. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  103. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  105. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  107. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  109. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  111. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  112. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  113. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  115. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  117. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  119. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  120. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  121. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  122. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  123. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  124. package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
  125. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  126. package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
  127. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  128. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  129. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  131. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  132. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  133. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  134. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  135. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  136. package/index.json +1 -1
  137. package/package.json +63 -2
  138. package/theme/_base.css +13 -0
  139. package/theme/_dark.css +110 -36
  140. package/theme/_high-contrast-dark.css +367 -0
  141. package/theme/_high-contrast.css +13 -2
  142. package/theme/_light.css +276 -0
  143. package/theme/_semantic.css +47 -12
  144. package/theme/_typography.css +17 -0
  145. package/theme/index.css +86 -7
  146. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  147. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  148. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  149. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  150. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  151. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  152. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  153. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  154. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  155. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  156. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  157. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  158. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  159. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  160. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  161. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  162. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  163. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  164. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  165. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  166. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  167. package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
  168. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  169. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  170. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  171. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  172. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  173. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  174. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  175. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  176. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  177. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  178. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  179. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  180. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  181. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  182. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  183. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  184. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  185. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  186. package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
  187. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  188. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  189. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  190. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  191. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  192. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  193. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  194. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  195. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  196. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  197. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  198. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  199. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  200. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  201. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  202. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  203. package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
  204. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  205. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  206. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  207. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
  208. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
  209. 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 = ['light', 'dark', 'high-contrast', 'system'];
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 = ['light', 'dark', 'high-contrast'];
9
- /** Built-in defaults merged under any user-provided {@link TwThemeConfig} by `provideTheme()`. */
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
- * Registers {@link ThemeService} and a {@link THEME_CONFIG} value built by
21
- * merging `config` over {@link DEFAULT_TW_THEME_CONFIG}. Call once in the
22
- * app's environment providers.
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: THEME_CONFIG,
28
- useValue: { ...DEFAULT_TW_THEME_CONFIG, ...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 the user selection to
37
- * `localStorage`, and writes the resolved theme onto the configured DOM
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'`. Register via
43
- * {@link provideTheme} in the app's environment providers.
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(THEME_CONFIG);
119
+ config = inject(TW_THEME_CONFIG);
47
120
  document = inject(DOCUMENT);
48
121
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
49
122
  ngZone = inject(NgZone);
50
- mediaQuery = null;
51
- mediaListener = (e) => {
52
- this.systemTheme.set(e.matches ? 'dark' : 'light');
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 scheme preference. */
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
- /** True when the resolved theme is `'dark'`. */
67
- isDark = computed(() => this.resolvedTheme() === 'dark', /* @ts-ignore */
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
- /** True when the resolved theme is `'light'`. */
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
- /** True when the resolved theme is `'high-contrast'`. */
73
- isHighContrast = computed(() => this.resolvedTheme() === 'high-contrast', /* @ts-ignore */
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 listener outside the Angular zone — it fires
88
- // on OS theme changes, not user interaction, and we don't want zone.js
89
- // bookkeeping for every system colour-scheme tick. The listener calls
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.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
93
- this.mediaQuery.addEventListener('change', this.mediaListener);
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
- const resolved = this.resolvedTheme();
98
- const selected = this.theme();
99
- this.applyToDocument(resolved);
100
- this.persistTheme(selected);
209
+ this.applyToDocument(this.resolvedTheme());
101
210
  });
102
211
  }
103
- /** Sets the selected theme. Pass `'system'` to follow the OS preference. */
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 current = this.theme();
110
- const idx = TW_THEMES.indexOf(current);
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
- /** Writes the configured theme attribute onto an arbitrary element — used to scope themes to a subtree. */
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.mediaQuery?.removeEventListener('change', this.mediaListener);
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
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
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
- /** Scopes a subtree to a specific resolved theme by writing `data-theme` on the host. Required. */
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":";;;;AAMA;AACO,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,QAAQ;AAEpE;AACO,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe;AA8BnE;AACO,MAAM,uBAAuB,GAAkB;AACpD,IAAA,YAAY,EAAE,QAAQ;AACtB,IAAA,UAAU,EAAE,cAAc;AAC1B,IAAA,SAAS,EAAE,YAAY;AACvB,IAAA,MAAM,EAAE,iBAAiB;;;ACzC3B;MACa,YAAY,GAAG,IAAI,cAAc,CAAgB,cAAc;AAE5E;;;;AAIG;AACG,SAAU,YAAY,CAAC,MAA+B,EAAA;AAC1D,IAAA,OAAO,wBAAwB,CAAC;AAC9B,QAAA;AACE,YAAA,OAAO,EAAE,YAAY;AACrB,YAAA,QAAQ,EAAE,EAAE,GAAG,uBAAuB,EAAE,GAAG,MAAM,EAAE;AACpD,SAAA;QACD,YAAY;AACb,KAAA,CAAC;AACJ;;ACNA;;;;;;;;;;AAUG;MAEU,YAAY,CAAA;AACN,IAAA,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC;AAC7B,IAAA,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC3B,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAClD,IAAA,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAEhC,UAAU,GAA0B,IAAI;AAC/B,IAAA,aAAa,GAAG,CAAC,CAAsB,KAAI;AAC1D,QAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AACpD,IAAA,CAAC;;AAGQ,IAAA,KAAK,GAAG,MAAM,CAAU,IAAI,CAAC,gBAAgB,EAAE;8EAAC;;AAGhD,IAAA,WAAW,GAAG,MAAM,CAAkB,IAAI,CAAC,iBAAiB,EAAE;oFAAC;;AAG/D,IAAA,aAAa,GAAG,QAAQ,CAAkB,MAAK;AACtD,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE;AACtB,QAAA,OAAO,CAAC,KAAK,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC;IAChD,CAAC;sFAAC;;IAGO,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,KAAK,MAAM;+EAAC;;IAExD,OAAO,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,KAAK,OAAO;gFAAC;;IAE1D,cAAc,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,KAAK,eAAe;uFAAC;;AAGzE,IAAA,KAAK,GAAG,QAAQ,CAAe,OAAO;AAC7C,QAAA,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE;AACnB,QAAA,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE;AACnC,QAAA,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE;AAC/B,QAAA,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;AACrB,QAAA,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;AACvB,QAAA,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE;KACtC,CAAC;8EAAC;AAEH,IAAA,WAAA,GAAA;AACE,QAAA,IAAI,IAAI,CAAC,SAAS,EAAE;;;;;AAKlB,YAAA,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,MAAK;gBACjC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC;gBACnE,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,CAAC;AAChE,YAAA,CAAC,CAAC;QACJ;QAEA,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE;AACrC,YAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE;AAC7B,YAAA,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC;AAC9B,YAAA,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;AAC7B,QAAA,CAAC,CAAC;IACJ;;AAGA,IAAA,QAAQ,CAAC,KAAc,EAAA;AACrB,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;IACvB;;IAGA,UAAU,GAAA;AACR,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE;QAC5B,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC;AACtC,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IACzD;;IAGA,cAAc,CAAC,OAAoB,EAAE,KAAsB,EAAA;QACzD,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC;IACpD;IAEA,WAAW,GAAA;QACT,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,CAAC;IACpE;IAEQ,gBAAgB,GAAA;QACtB,IAAI,CAAC,IAAI,CAAC,SAAS;AAAE,YAAA,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY;AACpD,QAAA,IAAI;AACF,YAAA,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;YAC3D,IAAI,MAAM,IAAK,SAA+B,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;AAC/D,gBAAA,OAAO,MAAiB;YAC1B;QACF;AAAE,QAAA,MAAM;;QAER;AACA,QAAA,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY;IACjC;IAEQ,iBAAiB,GAAA;QACvB,IAAI,CAAC,IAAI,CAAC,SAAS;AAAE,YAAA,OAAO,OAAO;AACnC,QAAA,OAAO,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAC,OAAO,GAAG,MAAM,GAAG,OAAO;IACrF;AAEQ,IAAA,eAAe,CAAC,KAAsB,EAAA;QAC5C,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE;QACrB,MAAM,MAAM,GACV,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK;AACrB,cAAE,IAAI,CAAC,QAAQ,CAAC;AAChB,cAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;QACxB,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC;IACnD;AAEQ,IAAA,YAAY,CAAC,KAAc,EAAA;QACjC,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE;AACrB,QAAA,IAAI;YACF,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC;QACrD;AAAE,QAAA,MAAM;;QAER;IACF;uGAnHW,YAAY,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;2GAAZ,YAAY,EAAA,CAAA;;2FAAZ,YAAY,EAAA,UAAA,EAAA,CAAA;kBADxB;;;MChBY,cAAc,CAAA;;IAEhB,OAAO,GAAG,KAAK,CAAC,QAAQ;gFAAmB;uGAFzC,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAd,cAAc,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,WAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAN1B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,WAAW;AACrB,oBAAA,IAAI,EAAE;AACJ,wBAAA,mBAAmB,EAAE,WAAW;AACjC,qBAAA;AACF,iBAAA;;;ACRD;;AAEG;;;;"}
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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"}