@ojiepermana/angular 22.1.14 → 22.2.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.
Files changed (252) hide show
  1. package/README.md +8 -6
  2. package/SUPPORT.md +3 -3
  3. package/chart/README.md +1 -1
  4. package/component/README.md +2 -2
  5. package/component/calendar/README.md +3 -15
  6. package/component/drawer/README.md +1 -2
  7. package/fesm2022/ojiepermana-angular-chart-area.mjs +352 -137
  8. package/fesm2022/ojiepermana-angular-chart-bar.mjs +744 -524
  9. package/fesm2022/ojiepermana-angular-chart-core.mjs +1140 -1344
  10. package/fesm2022/ojiepermana-angular-chart-line.mjs +393 -147
  11. package/fesm2022/ojiepermana-angular-chart-pie.mjs +408 -179
  12. package/fesm2022/ojiepermana-angular-chart-primitives.mjs +1262 -920
  13. package/fesm2022/ojiepermana-angular-chart-radar.mjs +473 -205
  14. package/fesm2022/ojiepermana-angular-chart-radial.mjs +425 -182
  15. package/fesm2022/ojiepermana-angular-chart-scatter.mjs +386 -210
  16. package/fesm2022/ojiepermana-angular-chart.mjs +3 -19
  17. package/fesm2022/ojiepermana-angular-component-accordion.mjs +485 -243
  18. package/fesm2022/ojiepermana-angular-component-alert-dialog.mjs +963 -325
  19. package/fesm2022/ojiepermana-angular-component-alert.mjs +266 -103
  20. package/fesm2022/ojiepermana-angular-component-aspect-ratio.mjs +80 -35
  21. package/fesm2022/ojiepermana-angular-component-avatar.mjs +402 -135
  22. package/fesm2022/ojiepermana-angular-component-badge.mjs +109 -59
  23. package/fesm2022/ojiepermana-angular-component-breadcrumb.mjs +410 -142
  24. package/fesm2022/ojiepermana-angular-component-button-group.mjs +249 -96
  25. package/fesm2022/ojiepermana-angular-component-button.mjs +153 -75
  26. package/fesm2022/ojiepermana-angular-component-calendar.mjs +531 -418
  27. package/fesm2022/ojiepermana-angular-component-card.mjs +457 -171
  28. package/fesm2022/ojiepermana-angular-component-carousel.mjs +642 -272
  29. package/fesm2022/ojiepermana-angular-component-checkbox.mjs +312 -108
  30. package/fesm2022/ojiepermana-angular-component-collapsible.mjs +299 -115
  31. package/fesm2022/ojiepermana-angular-component-combobox.mjs +396 -183
  32. package/fesm2022/ojiepermana-angular-component-command.mjs +807 -347
  33. package/fesm2022/ojiepermana-angular-component-composer.mjs +807 -330
  34. package/fesm2022/ojiepermana-angular-component-context-menu.mjs +196 -161
  35. package/fesm2022/ojiepermana-angular-component-date-picker.mjs +650 -338
  36. package/fesm2022/ojiepermana-angular-component-dialog.mjs +710 -316
  37. package/fesm2022/ojiepermana-angular-component-drawer.mjs +1 -5
  38. package/fesm2022/ojiepermana-angular-component-dropdown-menu.mjs +1212 -646
  39. package/fesm2022/ojiepermana-angular-component-editor.mjs +987 -502
  40. package/fesm2022/ojiepermana-angular-component-empty.mjs +350 -137
  41. package/fesm2022/ojiepermana-angular-component-form.mjs +813 -434
  42. package/fesm2022/ojiepermana-angular-component-hover-card.mjs +486 -277
  43. package/fesm2022/ojiepermana-angular-component-icon.mjs +241 -140
  44. package/fesm2022/ojiepermana-angular-component-input-group.mjs +449 -176
  45. package/fesm2022/ojiepermana-angular-component-input-otp.mjs +891 -488
  46. package/fesm2022/ojiepermana-angular-component-input.mjs +86 -55
  47. package/fesm2022/ojiepermana-angular-component-item.mjs +713 -261
  48. package/fesm2022/ojiepermana-angular-component-kanban.mjs +714 -216
  49. package/fesm2022/ojiepermana-angular-component-kbd.mjs +124 -52
  50. package/fesm2022/ojiepermana-angular-component-label.mjs +82 -31
  51. package/fesm2022/ojiepermana-angular-component-menubar.mjs +479 -329
  52. package/fesm2022/ojiepermana-angular-component-native-select.mjs +167 -65
  53. package/fesm2022/ojiepermana-angular-component-navigation-menu.mjs +683 -380
  54. package/fesm2022/ojiepermana-angular-component-pagination.mjs +277 -94
  55. package/fesm2022/ojiepermana-angular-component-pillbox.mjs +1283 -607
  56. package/fesm2022/ojiepermana-angular-component-popover.mjs +291 -204
  57. package/fesm2022/ojiepermana-angular-component-progress.mjs +176 -53
  58. package/fesm2022/ojiepermana-angular-component-radio.mjs +449 -142
  59. package/fesm2022/ojiepermana-angular-component-resizable.mjs +659 -453
  60. package/fesm2022/ojiepermana-angular-component-scroll-area.mjs +117 -39
  61. package/fesm2022/ojiepermana-angular-component-segmented-control.mjs +351 -172
  62. package/fesm2022/ojiepermana-angular-component-select.mjs +475 -345
  63. package/fesm2022/ojiepermana-angular-component-separator.mjs +102 -35
  64. package/fesm2022/ojiepermana-angular-component-sheet.mjs +797 -347
  65. package/fesm2022/ojiepermana-angular-component-skeleton.mjs +62 -29
  66. package/fesm2022/ojiepermana-angular-component-slider.mjs +592 -376
  67. package/fesm2022/ojiepermana-angular-component-spinner.mjs +135 -38
  68. package/fesm2022/ojiepermana-angular-component-switch.mjs +315 -103
  69. package/fesm2022/ojiepermana-angular-component-table.mjs +440 -153
  70. package/fesm2022/ojiepermana-angular-component-tabs.mjs +590 -317
  71. package/fesm2022/ojiepermana-angular-component-textarea.mjs +85 -50
  72. package/fesm2022/ojiepermana-angular-component-timeline.mjs +609 -231
  73. package/fesm2022/ojiepermana-angular-component-toast.mjs +229 -243
  74. package/fesm2022/ojiepermana-angular-component-toggle-group.mjs +573 -288
  75. package/fesm2022/ojiepermana-angular-component-toggle.mjs +187 -78
  76. package/fesm2022/ojiepermana-angular-component-tooltip.mjs +798 -517
  77. package/fesm2022/ojiepermana-angular-component-utils-core.mjs +159 -292
  78. package/fesm2022/ojiepermana-angular-component-utils-overlay.mjs +46 -58
  79. package/fesm2022/ojiepermana-angular-component-utils-scale.mjs +76 -38
  80. package/fesm2022/ojiepermana-angular-component-utils.mjs +2 -8
  81. package/fesm2022/ojiepermana-angular-component.mjs +0 -10
  82. package/fesm2022/ojiepermana-angular-navigation-core.mjs +13 -23
  83. package/fesm2022/ojiepermana-angular-navigation-icon.mjs +35 -24
  84. package/fesm2022/ojiepermana-angular-navigation-navigation-badge-count.component-DYuysKVf.mjs +129 -0
  85. package/fesm2022/ojiepermana-angular-navigation-navigation-dockbar-menu.component-COiTaw34.mjs +812 -0
  86. package/fesm2022/{ojiepermana-angular-navigation-navigation-entry-grid.component-COQ-60DS.mjs → ojiepermana-angular-navigation-navigation-entry-grid.component-YIz0IsIb.mjs} +392 -207
  87. package/fesm2022/ojiepermana-angular-navigation-navigation-flyout-menu.component-D2vwEwBY.mjs +650 -0
  88. package/fesm2022/ojiepermana-angular-navigation-navigation-horizontal.component-B5D7yXfb.mjs +662 -0
  89. package/fesm2022/ojiepermana-angular-navigation-navigation-item.component-CUb68MZH.mjs +1219 -0
  90. package/fesm2022/ojiepermana-angular-navigation-navigation-list.component-CrK16trj.mjs +327 -0
  91. package/fesm2022/ojiepermana-angular-navigation-service.mjs +544 -724
  92. package/fesm2022/ojiepermana-angular-navigation-types.mjs +139 -56
  93. package/fesm2022/ojiepermana-angular-navigation.mjs +3256 -4
  94. package/fesm2022/ojiepermana-angular-sdk.mjs +91 -201
  95. package/fesm2022/ojiepermana-angular-theme-component-settings.mjs +1125 -737
  96. package/fesm2022/ojiepermana-angular-theme-layout-services.mjs +245 -328
  97. package/fesm2022/ojiepermana-angular-theme-layout-types.mjs +35 -38
  98. package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +1928 -553
  99. package/fesm2022/ojiepermana-angular-theme-layout.mjs +847 -496
  100. package/fesm2022/ojiepermana-angular-theme-page-core.mjs +288 -358
  101. package/fesm2022/ojiepermana-angular-theme-page-filter.mjs +362 -224
  102. package/fesm2022/ojiepermana-angular-theme-page-overview.mjs +88 -0
  103. package/fesm2022/ojiepermana-angular-theme-page-root.mjs +576 -254
  104. package/fesm2022/ojiepermana-angular-theme-page-side.mjs +317 -177
  105. package/fesm2022/ojiepermana-angular-theme-page-slots.mjs +256 -112
  106. package/fesm2022/ojiepermana-angular-theme-page.mjs +7 -12
  107. package/fesm2022/ojiepermana-angular-theme-shell-services.mjs +215 -326
  108. package/fesm2022/ojiepermana-angular-theme-shell-types.mjs +68 -129
  109. package/fesm2022/ojiepermana-angular-theme-shell.mjs +907 -378
  110. package/fesm2022/ojiepermana-angular-theme-styles.mjs +521 -720
  111. package/fesm2022/ojiepermana-angular-theme.mjs +0 -10
  112. package/fesm2022/ojiepermana-angular.mjs +5 -24
  113. package/navigation/README.md +69 -18
  114. package/package.json +16 -12
  115. package/sdk/README.md +1 -1
  116. package/sdk/src/writer/index.js +2 -2
  117. package/theme/README.md +1 -1
  118. package/theme/layout/README.md +2 -4
  119. package/theme/page/README.md +50 -0
  120. package/theme/styles/README.md +10 -10
  121. package/theme/styles/css/base/prefers-dark.css +1 -1
  122. package/theme/styles/css/base/status.css +1 -1
  123. package/theme/styles/css/color/amber.css +1 -1
  124. package/theme/styles/css/color/blue.css +1 -1
  125. package/theme/styles/css/color/cyan.css +1 -1
  126. package/theme/styles/css/color/emerald.css +1 -1
  127. package/theme/styles/css/color/fuchsia.css +1 -1
  128. package/theme/styles/css/color/green.css +1 -1
  129. package/theme/styles/css/color/indigo.css +1 -1
  130. package/theme/styles/css/color/lime.css +1 -1
  131. package/theme/styles/css/color/orange.css +1 -1
  132. package/theme/styles/css/color/pink.css +1 -1
  133. package/theme/styles/css/color/purple.css +1 -1
  134. package/theme/styles/css/color/red.css +1 -1
  135. package/theme/styles/css/color/rose.css +1 -1
  136. package/theme/styles/css/color/sky.css +1 -1
  137. package/theme/styles/css/color/teal.css +1 -1
  138. package/theme/styles/css/color/violet.css +1 -1
  139. package/theme/styles/css/color/yellow.css +1 -1
  140. package/theme/styles/css/neutral/gray.css +1 -1
  141. package/theme/styles/css/neutral/mauve.css +1 -1
  142. package/theme/styles/css/neutral/mist.css +1 -1
  143. package/theme/styles/css/neutral/neutral.css +1 -1
  144. package/theme/styles/css/neutral/olive.css +1 -1
  145. package/theme/styles/css/neutral/slate.css +1 -1
  146. package/theme/styles/css/neutral/stone.css +1 -1
  147. package/theme/styles/css/neutral/taupe.css +1 -1
  148. package/theme/styles/css/neutral/zinc.css +1 -1
  149. package/types/ojiepermana-angular-chart-area.d.ts +126 -63
  150. package/types/ojiepermana-angular-chart-bar.d.ts +244 -154
  151. package/types/ojiepermana-angular-chart-core.d.ts +409 -398
  152. package/types/ojiepermana-angular-chart-line.d.ts +141 -68
  153. package/types/ojiepermana-angular-chart-pie.d.ts +192 -121
  154. package/types/ojiepermana-angular-chart-primitives.d.ts +283 -214
  155. package/types/ojiepermana-angular-chart-radar.d.ts +189 -108
  156. package/types/ojiepermana-angular-chart-radial.d.ts +177 -106
  157. package/types/ojiepermana-angular-chart-scatter.d.ts +166 -110
  158. package/types/ojiepermana-angular-chart.d.ts +3 -2
  159. package/types/ojiepermana-angular-component-accordion.d.ts +135 -81
  160. package/types/ojiepermana-angular-component-alert-dialog.d.ts +271 -123
  161. package/types/ojiepermana-angular-component-alert.d.ts +69 -39
  162. package/types/ojiepermana-angular-component-aspect-ratio.d.ts +20 -12
  163. package/types/ojiepermana-angular-component-avatar.d.ts +103 -49
  164. package/types/ojiepermana-angular-component-badge.d.ts +36 -24
  165. package/types/ojiepermana-angular-component-breadcrumb.d.ts +79 -40
  166. package/types/ojiepermana-angular-component-button-group.d.ts +48 -24
  167. package/types/ojiepermana-angular-component-button.d.ts +44 -27
  168. package/types/ojiepermana-angular-component-calendar.d.ts +145 -107
  169. package/types/ojiepermana-angular-component-card.d.ts +112 -63
  170. package/types/ojiepermana-angular-component-carousel.d.ts +150 -81
  171. package/types/ojiepermana-angular-component-checkbox.d.ts +96 -38
  172. package/types/ojiepermana-angular-component-collapsible.d.ts +65 -39
  173. package/types/ojiepermana-angular-component-combobox.d.ts +100 -55
  174. package/types/ojiepermana-angular-component-command.d.ts +206 -121
  175. package/types/ojiepermana-angular-component-composer.d.ts +184 -84
  176. package/types/ojiepermana-angular-component-context-menu.d.ts +51 -41
  177. package/types/ojiepermana-angular-component-date-picker.d.ts +199 -109
  178. package/types/ojiepermana-angular-component-dialog.d.ts +200 -105
  179. package/types/ojiepermana-angular-component-drawer.d.ts +2 -1
  180. package/types/ojiepermana-angular-component-dropdown-menu.d.ts +324 -202
  181. package/types/ojiepermana-angular-component-editor.d.ts +215 -117
  182. package/types/ojiepermana-angular-component-empty.d.ts +80 -45
  183. package/types/ojiepermana-angular-component-form.d.ts +185 -121
  184. package/types/ojiepermana-angular-component-hover-card.d.ts +130 -72
  185. package/types/ojiepermana-angular-component-icon.d.ts +87 -57
  186. package/types/ojiepermana-angular-component-input-group.d.ts +93 -46
  187. package/types/ojiepermana-angular-component-input-otp.d.ts +243 -150
  188. package/types/ojiepermana-angular-component-input.d.ts +27 -17
  189. package/types/ojiepermana-angular-component-item.d.ts +160 -84
  190. package/types/ojiepermana-angular-component-kanban.d.ts +157 -62
  191. package/types/ojiepermana-angular-component-kbd.d.ts +24 -15
  192. package/types/ojiepermana-angular-component-label.d.ts +19 -11
  193. package/types/ojiepermana-angular-component-menubar.d.ts +120 -90
  194. package/types/ojiepermana-angular-component-native-select.d.ts +37 -24
  195. package/types/ojiepermana-angular-component-navigation-menu.d.ts +157 -98
  196. package/types/ojiepermana-angular-component-pagination.d.ts +78 -28
  197. package/types/ojiepermana-angular-component-pillbox.d.ts +361 -204
  198. package/types/ojiepermana-angular-component-popover.d.ts +94 -64
  199. package/types/ojiepermana-angular-component-progress.d.ts +45 -17
  200. package/types/ojiepermana-angular-component-radio.d.ts +129 -53
  201. package/types/ojiepermana-angular-component-resizable.d.ts +160 -112
  202. package/types/ojiepermana-angular-component-scroll-area.d.ts +33 -15
  203. package/types/ojiepermana-angular-component-segmented-control.d.ts +94 -50
  204. package/types/ojiepermana-angular-component-select.d.ts +94 -77
  205. package/types/ojiepermana-angular-component-separator.d.ts +26 -14
  206. package/types/ojiepermana-angular-component-sheet.d.ts +226 -116
  207. package/types/ojiepermana-angular-component-skeleton.d.ts +13 -10
  208. package/types/ojiepermana-angular-component-slider.d.ts +140 -75
  209. package/types/ojiepermana-angular-component-spinner.d.ts +31 -13
  210. package/types/ojiepermana-angular-component-switch.d.ts +97 -40
  211. package/types/ojiepermana-angular-component-table.d.ts +102 -50
  212. package/types/ojiepermana-angular-component-tabs.d.ts +174 -115
  213. package/types/ojiepermana-angular-component-textarea.d.ts +27 -17
  214. package/types/ojiepermana-angular-component-timeline.d.ts +141 -59
  215. package/types/ojiepermana-angular-component-toast.d.ts +51 -55
  216. package/types/ojiepermana-angular-component-toggle-group.d.ts +159 -88
  217. package/types/ojiepermana-angular-component-toggle.d.ts +47 -25
  218. package/types/ojiepermana-angular-component-tooltip.d.ts +222 -132
  219. package/types/ojiepermana-angular-component-utils-core.d.ts +81 -90
  220. package/types/ojiepermana-angular-component-utils-overlay.d.ts +7 -11
  221. package/types/ojiepermana-angular-component-utils-scale.d.ts +20 -12
  222. package/types/ojiepermana-angular-component-utils.d.ts +2 -2
  223. package/types/ojiepermana-angular-component.d.ts +1 -2
  224. package/types/ojiepermana-angular-navigation-core.d.ts +46 -50
  225. package/types/ojiepermana-angular-navigation-icon.d.ts +9 -12
  226. package/types/ojiepermana-angular-navigation-service.d.ts +112 -115
  227. package/types/ojiepermana-angular-navigation-types.d.ts +166 -125
  228. package/types/ojiepermana-angular-navigation.d.ts +708 -375
  229. package/types/ojiepermana-angular-sdk.d.ts +69 -71
  230. package/types/ojiepermana-angular-theme-component-settings.d.ts +120 -86
  231. package/types/ojiepermana-angular-theme-layout-services.d.ts +119 -124
  232. package/types/ojiepermana-angular-theme-layout-types.d.ts +27 -31
  233. package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +446 -187
  234. package/types/ojiepermana-angular-theme-layout.d.ts +201 -113
  235. package/types/ojiepermana-angular-theme-page-core.d.ts +136 -142
  236. package/types/ojiepermana-angular-theme-page-filter.d.ts +112 -62
  237. package/types/ojiepermana-angular-theme-page-overview.d.ts +28 -0
  238. package/types/ojiepermana-angular-theme-page-root.d.ts +172 -88
  239. package/types/ojiepermana-angular-theme-page-side.d.ts +89 -49
  240. package/types/ojiepermana-angular-theme-page-slots.d.ts +58 -37
  241. package/types/ojiepermana-angular-theme-page.d.ts +7 -5
  242. package/types/ojiepermana-angular-theme-shell-services.d.ts +119 -125
  243. package/types/ojiepermana-angular-theme-shell-types.d.ts +38 -41
  244. package/types/ojiepermana-angular-theme-shell.d.ts +139 -61
  245. package/types/ojiepermana-angular-theme-styles.d.ts +184 -196
  246. package/types/ojiepermana-angular-theme.d.ts +1 -2
  247. package/types/ojiepermana-angular.d.ts +5 -5
  248. package/fesm2022/ojiepermana-angular-navigation-navigation-badge-count.component-Be3IqzG4.mjs +0 -91
  249. package/fesm2022/ojiepermana-angular-navigation-navigation-dockbar-menu.component-Bc7DWK3R.mjs +0 -522
  250. package/fesm2022/ojiepermana-angular-navigation-navigation-flyout-menu.component-DA8VPEn7.mjs +0 -404
  251. package/fesm2022/ojiepermana-angular-navigation-navigation-horizontal.component-U2TJTzlW.mjs +0 -466
  252. package/fesm2022/ojiepermana-angular-navigation-ojiepermana-angular-navigation-CIUhmytH.mjs +0 -2668
@@ -1,787 +1,588 @@
1
- import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2
- import * as i0 from '@angular/core';
3
- import { inject, signal, effect, computed, DestroyRef, InjectionToken, Service, makeEnvironmentProviders, provideEnvironmentInitializer, PLATFORM_ID } from '@angular/core';
4
- import { documentLocalStorage } from '@ojiepermana/angular/component/utils/core';
5
-
6
- /**
7
- * One place for the "a preference that lives in localStorage and shows up as an
8
- * attribute on `<html>`" pattern that every theme axis needs.
9
- *
10
- * Each axis service used to hand-roll the same four private methods (`storage`,
11
- * `readStorage`, `persist`, plus an effect writing the attribute) around a
12
- * `signal`. That is the whole body of the simpler services, so a fix to any of
13
- * it had to be made five times.
14
- *
15
- * The storage half itself lives in `@ojiepermana/angular/component/utils/core`,
16
- * because `navigation` needs it too and navigation must not depend on this
17
- * package.
18
- */
19
- /**
20
- * A persisted value, without the DOM half.
21
- *
22
- * Useful on its own for a preference that is not an attribute: the brand axis
23
- * stores an object and paints custom properties instead.
24
- *
25
- * Two timing details the theme services depend on:
26
- *
27
- * - the stored value is read SYNCHRONOUSLY here, not in an effect, because
28
- * every service exposes its state to consumers that read it during the same
29
- * change detection pass;
30
- * - the effect writes on its FIRST run, so storage holds the resolved default
31
- * immediately after construction even when nothing was stored.
32
- */
1
+ import { DOCUMENT, isPlatformBrowser } from "@angular/common";
2
+ import * as i0 from "@angular/core";
3
+ import { DestroyRef, InjectionToken, PLATFORM_ID, Service, computed, effect, inject, makeEnvironmentProviders, provideEnvironmentInitializer, signal } from "@angular/core";
4
+ import { documentLocalStorage } from "@ojiepermana/angular/component/utils/core";
33
5
  function persistedSignal(options) {
34
- const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
35
- const storage = documentLocalStorage(documentRef);
36
- const state = signal(options.parse(storage.read(options.key)), /* @ts-ignore */
37
- ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
38
- effect(() => {
39
- const serialized = options.serialize(state());
40
- if (serialized === null) {
41
- storage.remove(options.key);
42
- }
43
- else {
44
- storage.write(options.key, serialized);
45
- }
46
- });
47
- return state;
6
+ const documentRef = options.documentRef !== void 0 ? options.documentRef : inject(DOCUMENT, { optional: true });
7
+ const storage = documentLocalStorage(documentRef);
8
+ const state = signal(options.parse(storage.read(options.key)), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
9
+ effect(() => {
10
+ const serialized = options.serialize(state());
11
+ if (serialized === null) storage.remove(options.key);
12
+ else storage.write(options.key, serialized);
13
+ });
14
+ return state;
48
15
  }
49
- /**
50
- * Mirror a reactive string onto one or more `<html>` attributes. A `null` value
51
- * removes them.
52
- */
53
16
  function htmlAttributeEffect(options) {
54
- const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
55
- const attributes = typeof options.attribute === 'string' ? [options.attribute] : options.attribute;
56
- effect(() => {
57
- const value = options.source();
58
- const element = documentRef?.documentElement;
59
- if (!element)
60
- return;
61
- for (const attribute of attributes) {
62
- if (value === null) {
63
- element.removeAttribute(attribute);
64
- }
65
- else {
66
- element.setAttribute(attribute, value);
67
- }
68
- }
69
- });
17
+ const documentRef = options.documentRef !== void 0 ? options.documentRef : inject(DOCUMENT, { optional: true });
18
+ const attributes = typeof options.attribute === "string" ? [options.attribute] : options.attribute;
19
+ effect(() => {
20
+ const value = options.source();
21
+ const element = documentRef?.documentElement;
22
+ if (!element) return;
23
+ for (const attribute of attributes) if (value === null) element.removeAttribute(attribute);
24
+ else element.setAttribute(attribute, value);
25
+ });
70
26
  }
71
- /** Mirror reactive inline custom properties onto `<html>`. A `null` value removes one. */
72
27
  function htmlStylePropertyEffect(options) {
73
- const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
74
- effect(() => {
75
- const properties = options.source();
76
- const element = documentRef?.documentElement;
77
- if (!element)
78
- return;
79
- for (const [property, value] of Object.entries(properties)) {
80
- if (value === null) {
81
- element.style.removeProperty(property);
82
- }
83
- else {
84
- element.style.setProperty(property, value);
85
- }
86
- }
87
- });
28
+ const documentRef = options.documentRef !== void 0 ? options.documentRef : inject(DOCUMENT, { optional: true });
29
+ effect(() => {
30
+ const properties = options.source();
31
+ const element = documentRef?.documentElement;
32
+ if (!element) return;
33
+ for (const [property, value] of Object.entries(properties)) if (value === null) element.style.removeProperty(property);
34
+ else element.style.setProperty(property, value);
35
+ });
88
36
  }
89
- /**
90
- * A persisted preference that also shows up as an attribute on `<html>`.
91
- *
92
- * The stored value wins over the configured default, which is the rule every
93
- * axis already followed.
94
- */
95
37
  function persistedHtmlAttributeSignal(options) {
96
- const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
97
- const state = persistedSignal({
98
- key: options.key,
99
- documentRef,
100
- // A stored value wins; otherwise fall back to the configured default. Both
101
- // go through `normalize`, so neither can put a junk value into the signal.
102
- //
103
- // An empty stored string counts as ABSENT rather than as a value: no axis
104
- // has a legal `''`, so a key cleared to it (a storage the browser or another
105
- // tab emptied, a hand-edited entry) is a key with nothing in it. Reading it
106
- // as present would send it through `normalize` to the LIBRARY default and
107
- // silently discard the default the consumer configured.
108
- parse: (raw) => {
109
- const stored = raw !== null && raw !== '' ? raw : null;
110
- return options.normalize(stored ?? options.normalize(options.initial));
111
- },
112
- serialize: (value) => value,
113
- });
114
- // A computed, so whatever the resolver reads is tracked alongside the stored
115
- // value. That is what keeps a `'system'` mode following the operating system
116
- // while still persisting `'system'` rather than the resolved light or dark.
117
- const resolveAttributeValue = options.resolveAttributeValue;
118
- const attributeValue = resolveAttributeValue
119
- ? computed(() => resolveAttributeValue(state()))
120
- : computed(() => state());
121
- htmlAttributeEffect({
122
- attribute: options.attribute,
123
- source: () => attributeValue(),
124
- documentRef,
125
- });
126
- return {
127
- value: state.asReadonly(),
128
- attributeValue,
129
- set: (next) => state.set(next),
130
- };
38
+ const documentRef = options.documentRef !== void 0 ? options.documentRef : inject(DOCUMENT, { optional: true });
39
+ const state = persistedSignal({
40
+ key: options.key,
41
+ documentRef,
42
+ parse: (raw) => {
43
+ const stored = raw !== null && raw !== "" ? raw : null;
44
+ return options.normalize(stored ?? options.normalize(options.initial));
45
+ },
46
+ serialize: (value) => value
47
+ });
48
+ const resolveAttributeValue = options.resolveAttributeValue;
49
+ const attributeValue = resolveAttributeValue ? computed(() => resolveAttributeValue(state())) : computed(() => state());
50
+ htmlAttributeEffect({
51
+ attribute: options.attribute,
52
+ source: () => attributeValue(),
53
+ documentRef
54
+ });
55
+ return {
56
+ value: state.asReadonly(),
57
+ attributeValue,
58
+ set: (next) => state.set(next)
59
+ };
131
60
  }
132
- /**
133
- * `(prefers-color-scheme: dark)` as a signal, torn down with the injection
134
- * context that created it.
135
- *
136
- * Lives here because it is the other half of the same story: an axis whose
137
- * PERSISTED value is `'system'` still has to write a concrete `light` or `dark`
138
- * to the DOM, and this is what makes that reactive.
139
- */
140
61
  function prefersDarkSignal(documentRef) {
141
- const resolved = documentRef !== undefined ? documentRef : inject(DOCUMENT, { optional: true });
142
- const mediaQueryList = resolved?.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
143
- const state = signal(mediaQueryList?.matches ?? false, /* @ts-ignore */
144
- ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
145
- if (mediaQueryList) {
146
- const onChange = (event) => state.set(event.matches);
147
- mediaQueryList.addEventListener('change', onChange);
148
- inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener('change', onChange));
149
- }
150
- return state.asReadonly();
62
+ const mediaQueryList = (documentRef !== void 0 ? documentRef : inject(DOCUMENT, { optional: true }))?.defaultView?.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
63
+ const state = signal(mediaQueryList?.matches ?? false, ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
64
+ if (mediaQueryList) {
65
+ const onChange = (event) => state.set(event.matches);
66
+ mediaQueryList.addEventListener("change", onChange);
67
+ inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener("change", onChange));
68
+ }
69
+ return state.asReadonly();
151
70
  }
152
-
153
- /**
154
- * WCAG contrast math for tokens decided at runtime.
155
- *
156
- * A TypeScript port of scripts/theme-contrast.mjs, which is where the built-in
157
- * palettes are measured at generation time. The two halves have to agree: a
158
- * brand a consumer sets through `ThemeBrandService` lands on `--primary` and
159
- * `--ring` exactly like a generated accent does, so it is held to the same
160
- * thresholds by the same formula rather than a second approximation of it.
161
- *
162
- * Everything works on the bare HSL triplet (`H S% L%`) the CSS consumes through
163
- * `hsl(var(--token))`, so the numbers asserted are the numbers shipped. Parsing
164
- * returns `null` instead of throwing, because the caller here is a setter on a
165
- * running app and not a build step.
166
- */
167
- /** WCAG 1.4.3 minimum for normal-size text. */
168
71
  const TEXT_CONTRAST_MIN = 4.5;
169
- /** WCAG 1.4.11 minimum for non-text contrast (focus rings, borders, icons). */
170
- const NON_TEXT_CONTRAST_MIN = 3;
171
- /**
172
- * Extra headroom used when solving for a lightness, so a value that only just
173
- * clears the threshold cannot fail an assertion on a rounding difference.
174
- */
175
- const SOLVE_MARGIN = 0.02;
176
- const NUMBER = String.raw `[+-]?(?:\d+(?:\.\d+)?|\.\d+)`;
177
- // CSS accepts the components separated by commas or by whitespace, and the hue
178
- // as any <angle>. Every one of those forms paints, so every one of them has to
179
- // be measurable — a syntax this rejected used to walk straight past the brand
180
- // contrast guarantee.
181
- const SEPARATOR = String.raw `(?:\s*,\s*|\s+)`;
182
- const HSL_TRIPLET = new RegExp(`^\\s*(${NUMBER})(deg|grad|rad|turn)?${SEPARATOR}(${NUMBER})%${SEPARATOR}(${NUMBER})%\\s*$`, 'i');
72
+ const NUMBER = String.raw`[+-]?(?:\d+(?:\.\d+)?|\.\d+)`;
73
+ const SEPARATOR = String.raw`(?:\s*,\s*|\s+)`;
74
+ const HSL_TRIPLET = new RegExp(`^\\s*(${NUMBER})(deg|grad|rad|turn)?${SEPARATOR}(${NUMBER})%${SEPARATOR}(${NUMBER})%\\s*$`, "i");
183
75
  const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
184
- /** `<angle>` units CSS allows for a hue, as a multiplier onto degrees. */
185
76
  const DEGREES_PER_UNIT = {
186
- deg: 1,
187
- grad: 0.9,
188
- rad: 180 / Math.PI,
189
- turn: 360,
77
+ deg: 1,
78
+ grad: .9,
79
+ rad: 180 / Math.PI,
80
+ turn: 360
190
81
  };
191
82
  const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
192
- /**
193
- * Wrap a hue into 0..360 the way CSS does, so `-240` and `120` are the same
194
- * colour here as well and cannot measure differently.
195
- */
196
- const normalizeHue = (hue) => ((hue % 360) + 360) % 360;
197
- /** Read an HSL triplet string (`'221 83% 48%'`). `null` when it is not one. */
83
+ const normalizeHue = (hue) => (hue % 360 + 360) % 360;
198
84
  function parseHslTriplet(value) {
199
- const match = HSL_TRIPLET.exec(value);
200
- if (!match) {
201
- return null;
202
- }
203
- const [h, s, l] = [Number(match[1]), Number(match[3]), Number(match[4])];
204
- if (![h, s, l].every(Number.isFinite)) {
205
- return null;
206
- }
207
- return { h: normalizeHue(h * DEGREES_PER_UNIT[match[2]?.toLowerCase() ?? 'deg']), s, l };
85
+ const match = HSL_TRIPLET.exec(value);
86
+ if (!match) return null;
87
+ const [h, s, l] = [
88
+ Number(match[1]),
89
+ Number(match[3]),
90
+ Number(match[4])
91
+ ];
92
+ if (![
93
+ h,
94
+ s,
95
+ l
96
+ ].every(Number.isFinite)) return null;
97
+ return {
98
+ h: normalizeHue(h * DEGREES_PER_UNIT[match[2]?.toLowerCase() ?? "deg"]),
99
+ s,
100
+ l
101
+ };
208
102
  }
209
- /** Format numbers back into the triplet form the CSS consumes. */
210
103
  function formatHslTriplet({ h, s, l }) {
211
- return `${h} ${clamp(Math.round(s), 0, 100)}% ${clamp(Math.round(l), 0, 100)}%`;
104
+ return `${h} ${clamp(Math.round(s), 0, 100)}% ${clamp(Math.round(l), 0, 100)}%`;
212
105
  }
213
- /** `#rgb` / `#rrggbb` as an HSL triplet. `null` when the input is not a hex color. */
214
106
  function hexToHslTriplet(value) {
215
- const match = HEX_COLOR.exec(value.trim());
216
- if (!match) {
217
- return null;
218
- }
219
- const digits = match[1];
220
- const expanded = digits.length === 3
221
- ? digits
222
- .split('')
223
- .map((digit) => digit + digit)
224
- .join('')
225
- : digits;
226
- const [red, green, blue] = [0, 2, 4].map((offset) => Number.parseInt(expanded.slice(offset, offset + 2), 16) / 255);
227
- const largest = Math.max(red, green, blue);
228
- const smallest = Math.min(red, green, blue);
229
- const delta = largest - smallest;
230
- const lightness = (largest + smallest) / 2;
231
- let hue = 0;
232
- if (delta !== 0) {
233
- if (largest === red)
234
- hue = 60 * (((green - blue) / delta + 6) % 6);
235
- else if (largest === green)
236
- hue = 60 * ((blue - red) / delta + 2);
237
- else
238
- hue = 60 * ((red - green) / delta + 4);
239
- }
240
- const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
241
- return formatHslTriplet({ h: Math.round(hue), s: saturation * 100, l: lightness * 100 });
107
+ const match = HEX_COLOR.exec(value.trim());
108
+ if (!match) return null;
109
+ const digits = match[1];
110
+ const expanded = digits.length === 3 ? digits.split("").map((digit) => digit + digit).join("") : digits;
111
+ const [red, green, blue] = [
112
+ 0,
113
+ 2,
114
+ 4
115
+ ].map((offset) => Number.parseInt(expanded.slice(offset, offset + 2), 16) / 255);
116
+ const largest = Math.max(red, green, blue);
117
+ const smallest = Math.min(red, green, blue);
118
+ const delta = largest - smallest;
119
+ const lightness = (largest + smallest) / 2;
120
+ let hue = 0;
121
+ if (delta !== 0) {
122
+ if (largest === red) hue = 60 * (((green - blue) / delta + 6) % 6);
123
+ else if (largest === green) hue = 60 * ((blue - red) / delta + 2);
124
+ else hue = 60 * ((red - green) / delta + 4);
125
+ }
126
+ const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
127
+ return formatHslTriplet({
128
+ h: Math.round(hue),
129
+ s: saturation * 100,
130
+ l: lightness * 100
131
+ });
242
132
  }
243
- /** HSL to sRGB, each channel in the 0..1 range (CSS Color 4 algorithm). */
244
133
  function hslToRgb({ h, s, l }) {
245
- const saturation = clamp(s, 0, 100) / 100;
246
- const lightness = clamp(l, 0, 100) / 100;
247
- const k = (n) => (n + h / 30) % 12;
248
- const a = saturation * Math.min(lightness, 1 - lightness);
249
- const channel = (n) => lightness - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
250
- return [channel(0), channel(8), channel(4)];
251
- }
252
- /** sRGB channel to linear light, per the WCAG relative luminance definition. */
253
- const toLinear = (channel) => channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
254
- /** WCAG relative luminance of an HSL triplet. */
134
+ const saturation = clamp(s, 0, 100) / 100;
135
+ const lightness = clamp(l, 0, 100) / 100;
136
+ const k = (n) => (n + h / 30) % 12;
137
+ const a = saturation * Math.min(lightness, 1 - lightness);
138
+ const channel = (n) => lightness - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
139
+ return [
140
+ channel(0),
141
+ channel(8),
142
+ channel(4)
143
+ ];
144
+ }
145
+ const toLinear = (channel) => channel <= .03928 ? channel / 12.92 : Math.pow((channel + .055) / 1.055, 2.4);
255
146
  function relativeLuminance(triplet) {
256
- const parsed = parseHslTriplet(triplet);
257
- if (!parsed) {
258
- return 0;
259
- }
260
- const [r, g, b] = hslToRgb(parsed);
261
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
147
+ const parsed = parseHslTriplet(triplet);
148
+ if (!parsed) return 0;
149
+ const [r, g, b] = hslToRgb(parsed);
150
+ return .2126 * toLinear(r) + .7152 * toLinear(g) + .0722 * toLinear(b);
262
151
  }
263
- /** WCAG contrast ratio between two HSL triplets. Order does not matter. */
264
152
  function contrastRatio(a, b) {
265
- const ya = relativeLuminance(a);
266
- const yb = relativeLuminance(b);
267
- return (Math.max(ya, yb) + 0.05) / (Math.min(ya, yb) + 0.05);
153
+ const ya = relativeLuminance(a);
154
+ const yb = relativeLuminance(b);
155
+ return (Math.max(ya, yb) + .05) / (Math.min(ya, yb) + .05);
268
156
  }
269
- /**
270
- * Walk outward from `from` in 1% lightness steps and return the nearest
271
- * lightness whose color clears `min` against every entry in `against`. Keeping
272
- * hue and saturation fixed means the brand's identity survives; only how light
273
- * it is moves, and it moves as little as the threshold allows.
274
- *
275
- * `against` is a list so a value that has to hold over more than one surface can
276
- * be solved in one pass; the brand ring solves against one `--background` per
277
- * mode, because it is painted per mode.
278
- *
279
- * `null` when no lightness in 0..100 satisfies every background.
280
- */
281
157
  function solveLightnessNearest(options) {
282
- const target = options.min + (options.margin ?? SOLVE_MARGIN);
283
- const start = Math.round(clamp(options.from, 0, 100));
284
- for (let delta = 0; delta <= 100; delta += 1) {
285
- for (const candidate of delta === 0 ? [start] : [start - delta, start + delta]) {
286
- if (candidate < 0 || candidate > 100)
287
- continue;
288
- const triplet = formatHslTriplet({ h: options.h, s: options.s, l: candidate });
289
- if (options.against.every((background) => contrastRatio(triplet, background) >= target)) {
290
- return triplet;
291
- }
292
- }
293
- }
294
- return null;
158
+ const target = options.min + (options.margin ?? .02);
159
+ const start = Math.round(clamp(options.from, 0, 100));
160
+ for (let delta = 0; delta <= 100; delta += 1) for (const candidate of delta === 0 ? [start] : [start - delta, start + delta]) {
161
+ if (candidate < 0 || candidate > 100) continue;
162
+ const triplet = formatHslTriplet({
163
+ h: options.h,
164
+ s: options.s,
165
+ l: candidate
166
+ });
167
+ if (options.against.every((background) => contrastRatio(triplet, background) >= target)) return triplet;
168
+ }
169
+ return null;
295
170
  }
296
- /**
297
- * Keep a designed foreground when it already clears the threshold, otherwise
298
- * nudge its lightness to the nearest value that does. Anything unparsable is
299
- * returned untouched — there is nothing to measure.
300
- */
301
171
  function fitForeground(options) {
302
- const min = options.min ?? TEXT_CONTRAST_MIN;
303
- if (contrastRatio(options.foreground, options.background) >= min) {
304
- return options.foreground;
305
- }
306
- const parsed = parseHslTriplet(options.foreground);
307
- if (!parsed) {
308
- return options.foreground;
309
- }
310
- return (solveLightnessNearest({
311
- h: parsed.h,
312
- s: parsed.s,
313
- against: [options.background],
314
- min,
315
- from: parsed.l,
316
- }) ?? options.foreground);
317
- }
318
-
319
- const THEME_OPTIONS = new InjectionToken('THEME_OPTIONS');
320
-
321
- const THEME_MODES = ['light', 'dark', 'system'];
322
- const RESOLVED_THEME_MODES = ['light', 'dark'];
323
- /**
324
- * FluxUI-style accent palettes (axis `theme-color`). `base` = core theme (no
325
- * override); `brand` = the consumer's `--brand` color. Others re-tint the full
326
- * palette. Order mirrors the FluxUI accent picker (base…rose) + `brand`.
327
- */
172
+ const min = options.min ?? 4.5;
173
+ if (contrastRatio(options.foreground, options.background) >= min) return options.foreground;
174
+ const parsed = parseHslTriplet(options.foreground);
175
+ if (!parsed) return options.foreground;
176
+ return solveLightnessNearest({
177
+ h: parsed.h,
178
+ s: parsed.s,
179
+ against: [options.background],
180
+ min,
181
+ from: parsed.l
182
+ }) ?? options.foreground;
183
+ }
184
+ const THEME_OPTIONS = new InjectionToken("THEME_OPTIONS");
185
+ const THEME_MODES = [
186
+ "light",
187
+ "dark",
188
+ "system"
189
+ ];
190
+ const RESOLVED_THEME_MODES = ["light", "dark"];
328
191
  const THEME_COLORS = [
329
- 'base',
330
- 'red',
331
- 'orange',
332
- 'amber',
333
- 'yellow',
334
- 'lime',
335
- 'green',
336
- 'emerald',
337
- 'teal',
338
- 'cyan',
339
- 'sky',
340
- 'blue',
341
- 'indigo',
342
- 'violet',
343
- 'purple',
344
- 'fuchsia',
345
- 'pink',
346
- 'rose',
347
- 'brand',
192
+ "base",
193
+ "red",
194
+ "orange",
195
+ "amber",
196
+ "yellow",
197
+ "lime",
198
+ "green",
199
+ "emerald",
200
+ "teal",
201
+ "cyan",
202
+ "sky",
203
+ "blue",
204
+ "indigo",
205
+ "violet",
206
+ "purple",
207
+ "fuchsia",
208
+ "pink",
209
+ "rose",
210
+ "brand"
348
211
  ];
349
- /**
350
- * Neutral families (axis `theme-neutral`). `base` = core theme neutrals and lets
351
- * Layout patterns follow the accent. Non-base values override both the gray
352
- * family and Layout's decorative pattern hue, while composing with any accent.
353
- */
354
212
  const THEME_NEUTRALS = [
355
- 'base',
356
- 'slate',
357
- 'gray',
358
- 'zinc',
359
- 'neutral',
360
- 'stone',
361
- 'mauve',
362
- 'olive',
363
- 'mist',
364
- 'taupe',
213
+ "base",
214
+ "slate",
215
+ "gray",
216
+ "zinc",
217
+ "neutral",
218
+ "stone",
219
+ "mauve",
220
+ "olive",
221
+ "mist",
222
+ "taupe"
365
223
  ];
366
- /**
367
- * Corner radius presets (axis `theme-radius`). Each drives the single
368
- * `--radius-base` knob; the whole `--radius-*` scale + `rounded-*` utilities
369
- * follow. Every preset takes Tailwind's stock value for that name — `none` 0,
370
- * `xs` 0.125rem, `sm` 0.25rem, `md` 0.375rem (the default), `xl` 0.75rem — and
371
- * `full` is the pill extreme.
372
- */
373
- const THEME_RADII = ['none', 'xs', 'sm', 'md', 'xl', 'full'];
374
- /**
375
- * Spacing density presets (axis `theme-space`). Each drives the single
376
- * `--spacing-base` knob; every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility follows.
377
- * `normal` = the 0.25rem default (no-op baseline).
378
- */
379
- const THEME_SPACES = ['compact', 'normal'];
380
- const DEFAULT_THEME_MODE = 'system';
381
- const DEFAULT_THEME_COLOR = 'base';
382
- const DEFAULT_THEME_NEUTRAL = 'base';
383
- const DEFAULT_THEME_RADIUS = 'md';
384
- const DEFAULT_THEME_SPACE = 'normal';
385
- const THEME_MODE_STORAGE_KEY = 'theme-mode';
386
- const THEME_COLOR_STORAGE_KEY = 'theme-color';
387
- const THEME_NEUTRAL_STORAGE_KEY = 'theme-neutral';
388
- const THEME_BRAND_STORAGE_KEY = 'theme-brand';
389
- const THEME_RADIUS_STORAGE_KEY = 'theme-radius';
390
- const THEME_SPACE_STORAGE_KEY = 'theme-space';
224
+ const THEME_RADII = [
225
+ "none",
226
+ "xs",
227
+ "sm",
228
+ "md",
229
+ "xl",
230
+ "full"
231
+ ];
232
+ const THEME_SPACES = ["compact", "normal"];
233
+ const DEFAULT_THEME_MODE = "system";
234
+ const DEFAULT_THEME_COLOR = "base";
235
+ const DEFAULT_THEME_NEUTRAL = "base";
236
+ const DEFAULT_THEME_RADIUS = "md";
237
+ const DEFAULT_THEME_SPACE = "normal";
238
+ const THEME_MODE_STORAGE_KEY = "theme-mode";
239
+ const THEME_COLOR_STORAGE_KEY = "theme-color";
240
+ const THEME_NEUTRAL_STORAGE_KEY = "theme-neutral";
241
+ const THEME_BRAND_STORAGE_KEY = "theme-brand";
242
+ const THEME_RADIUS_STORAGE_KEY = "theme-radius";
243
+ const THEME_SPACE_STORAGE_KEY = "theme-space";
391
244
  function isThemeMode(value) {
392
- return typeof value === 'string' && THEME_MODES.includes(value);
245
+ return typeof value === "string" && THEME_MODES.includes(value);
393
246
  }
394
247
  function isResolvedThemeMode(value) {
395
- return typeof value === 'string' && RESOLVED_THEME_MODES.includes(value);
248
+ return typeof value === "string" && RESOLVED_THEME_MODES.includes(value);
396
249
  }
397
250
  function isThemeColor(value) {
398
- return typeof value === 'string' && THEME_COLORS.includes(value);
251
+ return typeof value === "string" && THEME_COLORS.includes(value);
399
252
  }
400
253
  function isThemeNeutral(value) {
401
- return typeof value === 'string' && THEME_NEUTRALS.includes(value);
254
+ return typeof value === "string" && THEME_NEUTRALS.includes(value);
402
255
  }
403
256
  function isThemeRadius(value) {
404
- return typeof value === 'string' && THEME_RADII.includes(value);
257
+ return typeof value === "string" && THEME_RADII.includes(value);
405
258
  }
406
259
  function isThemeSpace(value) {
407
- return typeof value === 'string' && THEME_SPACES.includes(value);
260
+ return typeof value === "string" && THEME_SPACES.includes(value);
408
261
  }
409
262
  function normalizeThemeMode(value) {
410
- return isThemeMode(value) ? value : DEFAULT_THEME_MODE;
263
+ return isThemeMode(value) ? value : DEFAULT_THEME_MODE;
411
264
  }
412
265
  function normalizeThemeColor(value) {
413
- return isThemeColor(value) ? value : DEFAULT_THEME_COLOR;
266
+ return isThemeColor(value) ? value : DEFAULT_THEME_COLOR;
414
267
  }
415
268
  function normalizeThemeNeutral(value) {
416
- return isThemeNeutral(value) ? value : DEFAULT_THEME_NEUTRAL;
269
+ return isThemeNeutral(value) ? value : DEFAULT_THEME_NEUTRAL;
417
270
  }
418
271
  function normalizeThemeRadius(value) {
419
- return isThemeRadius(value) ? value : DEFAULT_THEME_RADIUS;
272
+ return isThemeRadius(value) ? value : "md";
420
273
  }
421
274
  function normalizeThemeSpace(value) {
422
- return isThemeSpace(value) ? value : DEFAULT_THEME_SPACE;
275
+ return isThemeSpace(value) ? value : DEFAULT_THEME_SPACE;
423
276
  }
424
-
425
- const DEFAULT_BRAND_FOREGROUND = '0 0% 100%';
426
- /**
427
- * `--background` per mode, from css/base/tokens.css.
428
- *
429
- * The brand fill is one inline custom property that both modes read, but the
430
- * focus ring is not: `--brand-ring-light` and `--brand-ring-dark` are solved
431
- * separately and css/color/brand.css picks the one the current mode needs, so
432
- * the ring clears the non-text threshold without dragging the fill with it.
433
- */
434
- const RING_BACKGROUNDS = { light: '0 0% 100%', dark: '0 0% 5%' };
435
- /**
436
- * The brand as the canonical HSL triplet the CSS consumes: hex is converted, and
437
- * every HSL form CSS accepts (comma separated, an angle-suffixed hue) is
438
- * normalised, so none of them can paint while skipping the measurement.
439
- *
440
- * `null` for anything else — a `var()` reference is a documented escape hatch,
441
- * and there is nothing in it to measure or convert.
442
- */
277
+ const DEFAULT_BRAND_FOREGROUND = "0 0% 100%";
278
+ const RING_BACKGROUNDS = {
279
+ light: "0 0% 100%",
280
+ dark: "0 0% 5%"
281
+ };
443
282
  function toBrandTriplet(color) {
444
- const trimmed = color.trim();
445
- const parsed = parseHslTriplet(trimmed);
446
- return parsed ? formatHslTriplet(parsed) : hexToHslTriplet(trimmed);
283
+ const trimmed = color.trim();
284
+ const parsed = parseHslTriplet(trimmed);
285
+ return parsed ? formatHslTriplet(parsed) : hexToHslTriplet(trimmed);
447
286
  }
448
- /**
449
- * Hold a brand to the text half of the standard the generator holds every
450
- * built-in palette to: text on the brand fill at least 4.5 to 1. Only the
451
- * FOREGROUND moves, so the fill the consumer chose is the fill that ships —
452
- * exactly as the generator keeps `--primary` at its design lightness and
453
- * darkens a separate `--ring` instead.
454
- *
455
- * The non-text half is the ring's job; see {@link brandRing}.
456
- */
457
287
  function hardenBrand(brand) {
458
- const color = toBrandTriplet(brand.color);
459
- if (!color) {
460
- return brand;
461
- }
462
- return {
463
- color,
464
- foreground: fitForeground({
465
- foreground: brand.foreground,
466
- background: color,
467
- min: TEXT_CONTRAST_MIN,
468
- }),
469
- };
288
+ const color = toBrandTriplet(brand.color);
289
+ if (!color) return brand;
290
+ return {
291
+ color,
292
+ foreground: fitForeground({
293
+ foreground: brand.foreground,
294
+ background: color,
295
+ min: TEXT_CONTRAST_MIN
296
+ })
297
+ };
470
298
  }
471
- /**
472
- * The focus ring for one mode: the brand itself when it already clears the
473
- * non-text threshold against that mode's `--background`, otherwise the nearest
474
- * lightness that does. Hue and saturation never move, so the ring still reads
475
- * as the brand.
476
- */
477
299
  function brandRing(color, background) {
478
- const parsed = parseHslTriplet(color);
479
- if (!parsed) {
480
- return null;
481
- }
482
- if (contrastRatio(color, background) >= NON_TEXT_CONTRAST_MIN) {
483
- return color;
484
- }
485
- return (solveLightnessNearest({
486
- h: parsed.h,
487
- s: parsed.s,
488
- against: [background],
489
- min: NON_TEXT_CONTRAST_MIN,
490
- from: parsed.l,
491
- }) ?? color);
300
+ const parsed = parseHslTriplet(color);
301
+ if (!parsed) return null;
302
+ if (contrastRatio(color, background) >= 3) return color;
303
+ return solveLightnessNearest({
304
+ h: parsed.h,
305
+ s: parsed.s,
306
+ against: [background],
307
+ min: 3,
308
+ from: parsed.l
309
+ }) ?? color;
492
310
  }
493
- /** Extract the hue/saturation pair used by Layout's soft pattern tiers. */
494
311
  function brandPatternHueSaturation(color) {
495
- const parsed = parseHslTriplet(color);
496
- return parsed ? `${parsed.h} ${parsed.s}%` : null;
497
- }
498
- /**
499
- * Consumer-configurable brand color. Writes `--brand`, `--brand-foreground`, the
500
- * derived `--brand-pattern-hs` hue/saturation source, and a `--brand-ring-light`
501
- * / `--brand-ring-dark` pair as inline custom properties on `<html>` so solid
502
- * brand utilities, the focus ring, and the soft Layout pattern tiers can resolve
503
- * without CSS relative-color syntax.
504
- *
505
- * Default comes from `provideUiTheme({ brand })`; `setBrand()` overrides at
506
- * runtime and persists. When unset, `--brand` falls back to `var(--primary)`.
507
- */
508
- class ThemeBrandService {
509
- options = inject(THEME_OPTIONS, { optional: true });
510
- /**
511
- * Storage half shared with the other axes; the DOM half is custom, because the
512
- * brand paints a set of inline custom properties rather than setting an
513
- * attribute. `serialize` returning `null` is what clears the stored key.
514
- */
515
- brandState = persistedSignal({
516
- key: THEME_BRAND_STORAGE_KEY,
517
- parse: (raw) => this.parseStored(raw) ?? this.normalizeOption(this.options?.brand),
518
- serialize: (brand) => (brand ? JSON.stringify(brand) : null),
519
- });
520
- brand = this.brandState.asReadonly();
521
- constructor() {
522
- htmlStylePropertyEffect({ source: () => this.brandProperties(this.brandState()) });
523
- }
524
- /**
525
- * Set the brand color. `color`/`foreground` are HSL triplets (e.g.
526
- * `'221 83% 53%'`); a hex color is converted, since that is what a native
527
- * picker hands back. The fill is kept exactly as chosen — `--brand-foreground`
528
- * is moved until text on it clears 4.5 to 1, and the focus ring is solved
529
- * separately per mode, so a pale brand can never leave the ring invisible.
530
- */
531
- setBrand(color, foreground = DEFAULT_BRAND_FOREGROUND) {
532
- this.brandState.set(hardenBrand({ color, foreground }));
533
- }
534
- /** Clear the consumer brand → `--brand` falls back to `var(--primary)`. */
535
- clearBrand() {
536
- this.brandState.set(null);
537
- }
538
- normalizeOption(brand) {
539
- if (!brand) {
540
- return null;
541
- }
542
- if (typeof brand === 'string') {
543
- return hardenBrand({ color: brand, foreground: DEFAULT_BRAND_FOREGROUND });
544
- }
545
- return hardenBrand({
546
- color: brand.color,
547
- foreground: brand.foreground ?? DEFAULT_BRAND_FOREGROUND,
548
- });
549
- }
550
- /** The custom properties the brand paints. `null` removes one. */
551
- brandProperties(brand) {
552
- if (!brand) {
553
- return {
554
- '--brand': null,
555
- '--brand-foreground': null,
556
- '--brand-pattern-hs': null,
557
- '--brand-ring-light': null,
558
- '--brand-ring-dark': null,
559
- };
560
- }
561
- return {
562
- '--brand': brand.color,
563
- '--brand-foreground': brand.foreground,
564
- '--brand-pattern-hs': brandPatternHueSaturation(brand.color),
565
- '--brand-ring-light': brandRing(brand.color, RING_BACKGROUNDS.light),
566
- '--brand-ring-dark': brandRing(brand.color, RING_BACKGROUNDS.dark),
567
- };
568
- }
569
- /** Stored brands are JSON. Anything unparsable reads as "no stored brand". */
570
- parseStored(raw) {
571
- if (!raw) {
572
- return null;
573
- }
574
- try {
575
- const parsed = JSON.parse(raw);
576
- if (typeof parsed?.color !== 'string') {
577
- return null;
578
- }
579
- // Storage is editable by hand and predates the guarantee, so a restored
580
- // brand is measured exactly like a freshly set one.
581
- return hardenBrand({
582
- color: parsed.color,
583
- foreground: typeof parsed.foreground === 'string' ? parsed.foreground : DEFAULT_BRAND_FOREGROUND,
584
- });
585
- }
586
- catch {
587
- return null;
588
- }
589
- }
590
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Service });
591
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: ThemeBrandService });
592
- }
593
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeBrandService, decorators: [{
594
- type: Service
595
- }], ctorParameters: () => [] });
596
-
597
- /**
598
- * Drives the two FluxUI-style color axes:
599
- * - accent → `<html theme-color>` (full tinted palette per color)
600
- * - neutral → `<html theme-neutral>` (gray family + Layout pattern hue)
601
- *
602
- * Both persist to localStorage and seed from `provideUiTheme({ color, neutral })`
603
- * (a persisted choice wins over the configured default). `base` on either axis
604
- * means "no override" — the core base theme applies.
605
- */
606
- class ThemeColorService {
607
- options = inject(THEME_OPTIONS, { optional: true });
608
- colorAxis = persistedHtmlAttributeSignal({
609
- key: THEME_COLOR_STORAGE_KEY,
610
- attribute: 'theme-color',
611
- normalize: normalizeThemeColor,
612
- // A configured brand with no explicit accent defaults the accent to 'brand'
613
- // so the consumer's brand color becomes the primary (FluxUI-style). The
614
- // helper knows nothing about brand, so this stays here.
615
- initial: this.options?.color ?? (this.options?.brand ? 'brand' : undefined),
616
- });
617
- neutralAxis = persistedHtmlAttributeSignal({
618
- key: THEME_NEUTRAL_STORAGE_KEY,
619
- attribute: 'theme-neutral',
620
- normalize: normalizeThemeNeutral,
621
- initial: this.options?.neutral,
622
- });
623
- color = this.colorAxis.value;
624
- neutral = this.neutralAxis.value;
625
- setColor(color) {
626
- this.colorAxis.set(color);
627
- }
628
- setNeutral(neutral) {
629
- this.neutralAxis.set(neutral);
630
- }
631
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeColorService, deps: [], target: i0.ɵɵFactoryTarget.Service });
632
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: ThemeColorService });
633
- }
634
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeColorService, decorators: [{
635
- type: Service
636
- }] });
637
-
638
- /**
639
- * Color mode axis. Writes both `<html theme-mode>` and `<html data-mode>`.
640
- *
641
- * The persisted value and the written value differ here, which is the whole
642
- * point of the axis: `'system'` is what gets stored, while `light` or `dark` is
643
- * what reaches the DOM, and it keeps following the operating system live.
644
- */
645
- class ThemeModeService {
646
- options = inject(THEME_OPTIONS, { optional: true });
647
- systemPrefersDark = prefersDarkSignal();
648
- axis = persistedHtmlAttributeSignal({
649
- key: THEME_MODE_STORAGE_KEY,
650
- // One value, two attributes: `theme-mode` for the theme CSS and `data-mode`
651
- // for the Tailwind dark variant.
652
- attribute: ['theme-mode', 'data-mode'],
653
- normalize: normalizeThemeMode,
654
- initial: this.options?.mode,
655
- // Read inside the effect, so a system preference change repaints without
656
- // touching what is persisted.
657
- resolveAttributeValue: (mode) => mode === 'system' ? (this.systemPrefersDark() ? 'dark' : 'light') : mode,
658
- });
659
- mode = this.axis.value;
660
- resolvedMode = computed(() => this.axis.attributeValue(), /* @ts-ignore */
661
- ...(ngDevMode ? [{ debugName: "resolvedMode" }] : /* istanbul ignore next */ []));
662
- isDark = computed(() => this.resolvedMode() === 'dark', /* @ts-ignore */
663
- ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
664
- setMode(mode) {
665
- this.axis.set(mode);
666
- }
667
- toggle() {
668
- this.setMode(this.isDark() ? 'light' : 'dark');
669
- }
670
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeModeService, deps: [], target: i0.ɵɵFactoryTarget.Service });
671
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: ThemeModeService });
672
- }
673
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeModeService, decorators: [{
674
- type: Service
675
- }] });
676
-
677
- /** Radius in px @ 16px root; `full` uses a finite SVG-friendly sentinel. */
312
+ const parsed = parseHslTriplet(color);
313
+ return parsed ? `${parsed.h} ${parsed.s}%` : null;
314
+ }
315
+ var ThemeBrandService = class ThemeBrandService {
316
+ options = inject(THEME_OPTIONS, { optional: true });
317
+ brandState = persistedSignal({
318
+ key: THEME_BRAND_STORAGE_KEY,
319
+ parse: (raw) => this.parseStored(raw) ?? this.normalizeOption(this.options?.brand),
320
+ serialize: (brand) => brand ? JSON.stringify(brand) : null
321
+ });
322
+ brand = this.brandState.asReadonly();
323
+ constructor() {
324
+ htmlStylePropertyEffect({ source: () => this.brandProperties(this.brandState()) });
325
+ }
326
+ setBrand(color, foreground = DEFAULT_BRAND_FOREGROUND) {
327
+ this.brandState.set(hardenBrand({
328
+ color,
329
+ foreground
330
+ }));
331
+ }
332
+ clearBrand() {
333
+ this.brandState.set(null);
334
+ }
335
+ normalizeOption(brand) {
336
+ if (!brand) return null;
337
+ if (typeof brand === "string") return hardenBrand({
338
+ color: brand,
339
+ foreground: DEFAULT_BRAND_FOREGROUND
340
+ });
341
+ return hardenBrand({
342
+ color: brand.color,
343
+ foreground: brand.foreground ?? DEFAULT_BRAND_FOREGROUND
344
+ });
345
+ }
346
+ brandProperties(brand) {
347
+ if (!brand) return {
348
+ "--brand": null,
349
+ "--brand-foreground": null,
350
+ "--brand-pattern-hs": null,
351
+ "--brand-ring-light": null,
352
+ "--brand-ring-dark": null
353
+ };
354
+ return {
355
+ "--brand": brand.color,
356
+ "--brand-foreground": brand.foreground,
357
+ "--brand-pattern-hs": brandPatternHueSaturation(brand.color),
358
+ "--brand-ring-light": brandRing(brand.color, RING_BACKGROUNDS.light),
359
+ "--brand-ring-dark": brandRing(brand.color, RING_BACKGROUNDS.dark)
360
+ };
361
+ }
362
+ parseStored(raw) {
363
+ if (!raw) return null;
364
+ try {
365
+ const parsed = JSON.parse(raw);
366
+ if (typeof parsed?.color !== "string") return null;
367
+ return hardenBrand({
368
+ color: parsed.color,
369
+ foreground: typeof parsed.foreground === "string" ? parsed.foreground : DEFAULT_BRAND_FOREGROUND
370
+ });
371
+ } catch {
372
+ return null;
373
+ }
374
+ }
375
+ static ɵfac = i0.ɵɵngDeclareFactory({
376
+ minVersion: "12.0.0",
377
+ version: "22.2.2",
378
+ ngImport: i0,
379
+ type: ThemeBrandService,
380
+ deps: [],
381
+ target: i0.ɵɵFactoryTarget.Service
382
+ });
383
+ static ɵprov = i0.ɵɵngDeclareService({
384
+ minVersion: "22.0.0",
385
+ version: "22.2.2",
386
+ ngImport: i0,
387
+ type: ThemeBrandService
388
+ });
389
+ };
390
+ i0.ɵɵngDeclareClassMetadata({
391
+ minVersion: "12.0.0",
392
+ version: "22.2.2",
393
+ ngImport: i0,
394
+ type: ThemeBrandService,
395
+ decorators: [{ type: Service }],
396
+ ctorParameters: () => []
397
+ });
398
+ var ThemeColorService = class ThemeColorService {
399
+ options = inject(THEME_OPTIONS, { optional: true });
400
+ colorAxis = persistedHtmlAttributeSignal({
401
+ key: THEME_COLOR_STORAGE_KEY,
402
+ attribute: "theme-color",
403
+ normalize: normalizeThemeColor,
404
+ initial: this.options?.color ?? (this.options?.brand ? "brand" : void 0)
405
+ });
406
+ neutralAxis = persistedHtmlAttributeSignal({
407
+ key: THEME_NEUTRAL_STORAGE_KEY,
408
+ attribute: "theme-neutral",
409
+ normalize: normalizeThemeNeutral,
410
+ initial: this.options?.neutral
411
+ });
412
+ color = this.colorAxis.value;
413
+ neutral = this.neutralAxis.value;
414
+ setColor(color) {
415
+ this.colorAxis.set(color);
416
+ }
417
+ setNeutral(neutral) {
418
+ this.neutralAxis.set(neutral);
419
+ }
420
+ static ɵfac = i0.ɵɵngDeclareFactory({
421
+ minVersion: "12.0.0",
422
+ version: "22.2.2",
423
+ ngImport: i0,
424
+ type: ThemeColorService,
425
+ deps: [],
426
+ target: i0.ɵɵFactoryTarget.Service
427
+ });
428
+ static ɵprov = i0.ɵɵngDeclareService({
429
+ minVersion: "22.0.0",
430
+ version: "22.2.2",
431
+ ngImport: i0,
432
+ type: ThemeColorService
433
+ });
434
+ };
435
+ i0.ɵɵngDeclareClassMetadata({
436
+ minVersion: "12.0.0",
437
+ version: "22.2.2",
438
+ ngImport: i0,
439
+ type: ThemeColorService,
440
+ decorators: [{ type: Service }]
441
+ });
442
+ var ThemeModeService = class ThemeModeService {
443
+ options = inject(THEME_OPTIONS, { optional: true });
444
+ systemPrefersDark = prefersDarkSignal();
445
+ axis = persistedHtmlAttributeSignal({
446
+ key: THEME_MODE_STORAGE_KEY,
447
+ attribute: ["theme-mode", "data-mode"],
448
+ normalize: normalizeThemeMode,
449
+ initial: this.options?.mode,
450
+ resolveAttributeValue: (mode) => mode === "system" ? this.systemPrefersDark() ? "dark" : "light" : mode
451
+ });
452
+ mode = this.axis.value;
453
+ resolvedMode = computed(() => this.axis.attributeValue(), ...ngDevMode ? [{ debugName: "resolvedMode" }] : /* istanbul ignore next */ []);
454
+ isDark = computed(() => this.resolvedMode() === "dark", ...ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []);
455
+ setMode(mode) {
456
+ this.axis.set(mode);
457
+ }
458
+ toggle() {
459
+ this.setMode(this.isDark() ? "light" : "dark");
460
+ }
461
+ static ɵfac = i0.ɵɵngDeclareFactory({
462
+ minVersion: "12.0.0",
463
+ version: "22.2.2",
464
+ ngImport: i0,
465
+ type: ThemeModeService,
466
+ deps: [],
467
+ target: i0.ɵɵFactoryTarget.Service
468
+ });
469
+ static ɵprov = i0.ɵɵngDeclareService({
470
+ minVersion: "22.0.0",
471
+ version: "22.2.2",
472
+ ngImport: i0,
473
+ type: ThemeModeService
474
+ });
475
+ };
476
+ i0.ɵɵngDeclareClassMetadata({
477
+ minVersion: "12.0.0",
478
+ version: "22.2.2",
479
+ ngImport: i0,
480
+ type: ThemeModeService,
481
+ decorators: [{ type: Service }]
482
+ });
678
483
  const RADIUS_PX = {
679
- none: 0,
680
- xs: 2,
681
- sm: 4,
682
- md: 6,
683
- xl: 12,
684
- full: 9999,
484
+ none: 0,
485
+ xs: 2,
486
+ sm: 4,
487
+ md: 6,
488
+ xl: 12,
489
+ full: 9999
685
490
  };
686
- /**
687
- * Corner radius axis. Writes `<html theme-radius>`; each preset CSS sets the
688
- * single `--radius-base` knob so the whole `--radius-*` scale + `rounded-*`
689
- * utilities rescale. Persists to localStorage and seeds from
690
- * `provideUiTheme({ radius })` (a persisted choice wins over the default).
691
- */
692
- class ThemeRadiusService {
693
- options = inject(THEME_OPTIONS, { optional: true });
694
- axis = persistedHtmlAttributeSignal({
695
- key: THEME_RADIUS_STORAGE_KEY,
696
- attribute: 'theme-radius',
697
- normalize: normalizeThemeRadius,
698
- initial: this.options?.radius,
699
- });
700
- radius = this.axis.value;
701
- /** Active preset in px @ 16px root. */
702
- radiusPx = computed(() => RADIUS_PX[this.radius()] ?? RADIUS_PX.md, /* @ts-ignore */
703
- ...(ngDevMode ? [{ debugName: "radiusPx" }] : /* istanbul ignore next */ []));
704
- setRadius(radius) {
705
- this.axis.set(radius);
706
- }
707
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeRadiusService, deps: [], target: i0.ɵɵFactoryTarget.Service });
708
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: ThemeRadiusService });
709
- }
710
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeRadiusService, decorators: [{
711
- type: Service
712
- }] });
713
-
714
- /**
715
- * Spacing density axis. Writes `<html theme-space>`; each preset CSS sets the
716
- * single `--spacing-base` knob, which Tailwind's `--spacing` derives from, so
717
- * every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility rescales. Persists to
718
- * localStorage and seeds from `provideUiTheme({ space })` (a persisted choice
719
- * wins over the default).
720
- */
721
- class ThemeSpaceService {
722
- options = inject(THEME_OPTIONS, { optional: true });
723
- axis = persistedHtmlAttributeSignal({
724
- key: THEME_SPACE_STORAGE_KEY,
725
- attribute: 'theme-space',
726
- normalize: normalizeThemeSpace,
727
- initial: this.options?.space,
728
- });
729
- space = this.axis.value;
730
- setSpace(space) {
731
- this.axis.set(space);
732
- }
733
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeSpaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
734
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: ThemeSpaceService });
735
- }
736
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ThemeSpaceService, decorators: [{
737
- type: Service
738
- }] });
739
-
740
- const MATERIAL_SYMBOLS_FONT_ATTR = 'data-icon-font';
741
- const MATERIAL_SYMBOLS_FONT_ID = 'material-symbols';
742
- const MATERIAL_SYMBOLS_FONT_HREF = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block';
491
+ var ThemeRadiusService = class ThemeRadiusService {
492
+ options = inject(THEME_OPTIONS, { optional: true });
493
+ axis = persistedHtmlAttributeSignal({
494
+ key: THEME_RADIUS_STORAGE_KEY,
495
+ attribute: "theme-radius",
496
+ normalize: normalizeThemeRadius,
497
+ initial: this.options?.radius
498
+ });
499
+ radius = this.axis.value;
500
+ radiusPx = computed(() => RADIUS_PX[this.radius()] ?? RADIUS_PX.md, ...ngDevMode ? [{ debugName: "radiusPx" }] : /* istanbul ignore next */ []);
501
+ setRadius(radius) {
502
+ this.axis.set(radius);
503
+ }
504
+ static ɵfac = i0.ɵɵngDeclareFactory({
505
+ minVersion: "12.0.0",
506
+ version: "22.2.2",
507
+ ngImport: i0,
508
+ type: ThemeRadiusService,
509
+ deps: [],
510
+ target: i0.ɵɵFactoryTarget.Service
511
+ });
512
+ static ɵprov = i0.ɵɵngDeclareService({
513
+ minVersion: "22.0.0",
514
+ version: "22.2.2",
515
+ ngImport: i0,
516
+ type: ThemeRadiusService
517
+ });
518
+ };
519
+ i0.ɵɵngDeclareClassMetadata({
520
+ minVersion: "12.0.0",
521
+ version: "22.2.2",
522
+ ngImport: i0,
523
+ type: ThemeRadiusService,
524
+ decorators: [{ type: Service }]
525
+ });
526
+ var ThemeSpaceService = class ThemeSpaceService {
527
+ options = inject(THEME_OPTIONS, { optional: true });
528
+ axis = persistedHtmlAttributeSignal({
529
+ key: THEME_SPACE_STORAGE_KEY,
530
+ attribute: "theme-space",
531
+ normalize: normalizeThemeSpace,
532
+ initial: this.options?.space
533
+ });
534
+ space = this.axis.value;
535
+ setSpace(space) {
536
+ this.axis.set(space);
537
+ }
538
+ static ɵfac = i0.ɵɵngDeclareFactory({
539
+ minVersion: "12.0.0",
540
+ version: "22.2.2",
541
+ ngImport: i0,
542
+ type: ThemeSpaceService,
543
+ deps: [],
544
+ target: i0.ɵɵFactoryTarget.Service
545
+ });
546
+ static ɵprov = i0.ɵɵngDeclareService({
547
+ minVersion: "22.0.0",
548
+ version: "22.2.2",
549
+ ngImport: i0,
550
+ type: ThemeSpaceService
551
+ });
552
+ };
553
+ i0.ɵɵngDeclareClassMetadata({
554
+ minVersion: "12.0.0",
555
+ version: "22.2.2",
556
+ ngImport: i0,
557
+ type: ThemeSpaceService,
558
+ decorators: [{ type: Service }]
559
+ });
560
+ const MATERIAL_SYMBOLS_FONT_ATTR = "data-icon-font";
561
+ const MATERIAL_SYMBOLS_FONT_ID = "material-symbols";
562
+ const MATERIAL_SYMBOLS_FONT_HREF = "https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block";
743
563
  function provideUiTheme(options = {}) {
744
- return makeEnvironmentProviders([
745
- {
746
- provide: THEME_OPTIONS,
747
- useValue: options,
748
- },
749
- provideEnvironmentInitializer(() => {
750
- // Opt-in: the Material Symbols font is only fetched from Google Fonts when
751
- // explicitly enabled, so the default install makes no external network
752
- // request (privacy / offline / CSP friendly). Set `icons.materialSymbols: true`
753
- // or self-host the font and import it yourself.
754
- if (options.icons?.materialSymbols === true) {
755
- loadMaterialSymbolsStylesheet();
756
- }
757
- inject(ThemeModeService);
758
- inject(ThemeColorService);
759
- inject(ThemeBrandService);
760
- inject(ThemeRadiusService);
761
- inject(ThemeSpaceService);
762
- }),
763
- ]);
564
+ return makeEnvironmentProviders([{
565
+ provide: THEME_OPTIONS,
566
+ useValue: options
567
+ }, provideEnvironmentInitializer(() => {
568
+ if (options.icons?.materialSymbols === true) loadMaterialSymbolsStylesheet();
569
+ inject(ThemeModeService);
570
+ inject(ThemeColorService);
571
+ inject(ThemeBrandService);
572
+ inject(ThemeRadiusService);
573
+ inject(ThemeSpaceService);
574
+ })]);
764
575
  }
765
576
  function loadMaterialSymbolsStylesheet() {
766
- const documentRef = inject(DOCUMENT, { optional: true });
767
- const platformId = inject(PLATFORM_ID);
768
- const head = documentRef?.head;
769
- if (!isPlatformBrowser(platformId) || !head) {
770
- return;
771
- }
772
- const existing = head.querySelector(`link[${MATERIAL_SYMBOLS_FONT_ATTR}="${MATERIAL_SYMBOLS_FONT_ID}"]`);
773
- if (existing) {
774
- return;
775
- }
776
- const link = head.ownerDocument.createElement('link');
777
- link.rel = 'stylesheet';
778
- link.href = MATERIAL_SYMBOLS_FONT_HREF;
779
- link.setAttribute(MATERIAL_SYMBOLS_FONT_ATTR, MATERIAL_SYMBOLS_FONT_ID);
780
- head.appendChild(link);
781
- }
782
-
783
- /**
784
- * Generated bundle index. Do not edit.
785
- */
786
-
787
- export { DEFAULT_THEME_COLOR, DEFAULT_THEME_MODE, DEFAULT_THEME_NEUTRAL, DEFAULT_THEME_RADIUS, DEFAULT_THEME_SPACE, RESOLVED_THEME_MODES, THEME_BRAND_STORAGE_KEY, THEME_COLORS, THEME_COLOR_STORAGE_KEY, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_NEUTRALS, THEME_NEUTRAL_STORAGE_KEY, THEME_OPTIONS, THEME_RADII, THEME_RADIUS_STORAGE_KEY, THEME_SPACES, THEME_SPACE_STORAGE_KEY, ThemeBrandService, ThemeColorService, ThemeModeService, ThemeRadiusService, ThemeSpaceService, htmlAttributeEffect, htmlStylePropertyEffect, isResolvedThemeMode, isThemeColor, isThemeMode, isThemeNeutral, isThemeRadius, isThemeSpace, normalizeThemeColor, normalizeThemeMode, normalizeThemeNeutral, normalizeThemeRadius, normalizeThemeSpace, persistedHtmlAttributeSignal, persistedSignal, prefersDarkSignal, provideUiTheme };
577
+ const documentRef = inject(DOCUMENT, { optional: true });
578
+ const platformId = inject(PLATFORM_ID);
579
+ const head = documentRef?.head;
580
+ if (!isPlatformBrowser(platformId) || !head) return;
581
+ if (head.querySelector(`link[${MATERIAL_SYMBOLS_FONT_ATTR}="${MATERIAL_SYMBOLS_FONT_ID}"]`)) return;
582
+ const link = head.ownerDocument.createElement("link");
583
+ link.rel = "stylesheet";
584
+ link.href = MATERIAL_SYMBOLS_FONT_HREF;
585
+ link.setAttribute(MATERIAL_SYMBOLS_FONT_ATTR, MATERIAL_SYMBOLS_FONT_ID);
586
+ head.appendChild(link);
587
+ }
588
+ export { DEFAULT_THEME_COLOR, DEFAULT_THEME_MODE, DEFAULT_THEME_NEUTRAL, DEFAULT_THEME_RADIUS, DEFAULT_THEME_SPACE, RESOLVED_THEME_MODES, THEME_BRAND_STORAGE_KEY, THEME_COLORS, THEME_COLOR_STORAGE_KEY, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_NEUTRALS, THEME_NEUTRAL_STORAGE_KEY, THEME_OPTIONS, THEME_RADII, THEME_RADIUS_STORAGE_KEY, THEME_SPACES, THEME_SPACE_STORAGE_KEY, ThemeBrandService, ThemeColorService, ThemeModeService, ThemeRadiusService, ThemeSpaceService, htmlAttributeEffect, htmlStylePropertyEffect, isResolvedThemeMode, isThemeColor, isThemeMode, isThemeNeutral, isThemeRadius, isThemeSpace, normalizeThemeColor, normalizeThemeMode, normalizeThemeNeutral, normalizeThemeRadius, normalizeThemeSpace, persistedHtmlAttributeSignal, persistedSignal, prefersDarkSignal, provideUiTheme };