@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -0,0 +1,227 @@
1
+ import {
2
+ computed,
3
+ inject,
4
+ InjectionToken,
5
+ type Provider,
6
+ type Signal,
7
+ } from '@angular/core';
8
+ import { LynxTheme } from '@blotch/angular-lynx';
9
+
10
+ /**
11
+ * Colors for the components that draw with Lynx's `<svg>`: `<ui-icon>`,
12
+ * `<ui-spinner>`, and the parts built on them (pagination's chevrons, a loading
13
+ * button's spinner).
14
+ *
15
+ * ## Why colors live in TypeScript as well as CSS
16
+ * `<svg>` parses its markup by itself. It never sees the page's CSS: on the web
17
+ * preview the markup becomes an `<img>`, and the native elements in Lynx 4.1
18
+ * read only a literal `current-color`. So `var(--foreground)` and text color
19
+ * classes can't reach a stroke, and Lynx has no computed-style API to read a
20
+ * variable's value from JS. Instead these components take `var(--token)` colors
21
+ * and swap each for its value in the active table below before they write the
22
+ * markup. This mirrors how `<ui-chart>` colors its marks (`chart-theme.ts`).
23
+ *
24
+ * The tables mirror `default.css` and `dark.css` (a test in the theme folder
25
+ * keeps them in step). If you change a color there, change it here too, or pass
26
+ * your own with {@link provideIconTheme}.
27
+ */
28
+
29
+ /** Theme token name (without `--`) to a complete CSS color. */
30
+ export type IconColorTokens = Readonly<Record<string, string>>;
31
+
32
+ export type IconColorScheme = 'light' | 'dark';
33
+
34
+ /**
35
+ * Which table the colors come from: a fixed scheme, `'system'` to follow the
36
+ * device appearance (`LynxTheme`), or a function reading the app's own theme
37
+ * state.
38
+ *
39
+ * It has to match how the app switches its CSS theme, since the text next to an
40
+ * icon reads the CSS variables. The ui theme's dark mode is opt-in (the app
41
+ * toggles the `.dark` class), so the default is `'light'`: an icon in an app
42
+ * without dark mode stays visible when the device is dark.
43
+ */
44
+ export type IconColorSchemeOption =
45
+ | IconColorScheme
46
+ | 'system'
47
+ | (() => IconColorScheme);
48
+
49
+ export type UiIconTheme = {
50
+ /** Token values for light mode; mirrors `default.css`. */
51
+ readonly light: IconColorTokens;
52
+ /** Token values for dark mode; mirrors `dark.css`. */
53
+ readonly dark: IconColorTokens;
54
+ /** See {@link IconColorSchemeOption}; `'light'` when unset. */
55
+ readonly colorScheme?: IconColorSchemeOption;
56
+ };
57
+
58
+ export const ICON_LIGHT_COLORS: IconColorTokens = {
59
+ background: 'rgba(255, 255, 255, 1)',
60
+ foreground: 'rgba(9, 9, 11, 1)',
61
+ card: 'rgba(255, 255, 255, 1)',
62
+ 'card-foreground': 'rgba(9, 9, 11, 1)',
63
+ primary: 'rgba(24, 24, 27, 1)',
64
+ 'primary-foreground': 'rgba(250, 250, 250, 1)',
65
+ secondary: 'rgba(244, 244, 245, 1)',
66
+ 'secondary-foreground': 'rgba(24, 24, 27, 1)',
67
+ muted: 'rgba(244, 244, 245, 1)',
68
+ 'muted-foreground': 'rgba(113, 113, 122, 1)',
69
+ accent: 'rgba(244, 244, 245, 1)',
70
+ 'accent-foreground': 'rgba(24, 24, 27, 1)',
71
+ destructive: 'rgba(239, 68, 68, 1)',
72
+ 'destructive-foreground': 'rgba(250, 250, 250, 1)',
73
+ 'destructive-subtle': 'rgba(239, 68, 68, 0.1)',
74
+ border: 'rgba(228, 228, 231, 1)',
75
+ input: 'rgba(228, 228, 231, 1)',
76
+ ring: 'rgba(24, 24, 27, 1)',
77
+ 'ring-subtle': 'rgba(24, 24, 27, 0.4)',
78
+ 'chart-1': 'rgba(231, 110, 80, 1)',
79
+ 'chart-2': 'rgba(42, 157, 144, 1)',
80
+ 'chart-3': 'rgba(39, 71, 84, 1)',
81
+ 'chart-4': 'rgba(232, 196, 104, 1)',
82
+ 'chart-5': 'rgba(244, 164, 98, 1)',
83
+ 'glass-tint': 'rgba(24, 24, 27, 1)',
84
+ 'glass-tint-foreground': 'rgba(250, 250, 250, 1)',
85
+ 'glass-foreground': 'rgba(9, 9, 11, 1)',
86
+ 'glass-muted-foreground': 'rgba(63, 63, 70, 1)',
87
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
88
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
89
+ };
90
+
91
+ export const ICON_DARK_COLORS: IconColorTokens = {
92
+ background: 'rgba(9, 9, 11, 1)',
93
+ foreground: 'rgba(250, 250, 250, 1)',
94
+ card: 'rgba(9, 9, 11, 1)',
95
+ 'card-foreground': 'rgba(250, 250, 250, 1)',
96
+ primary: 'rgba(250, 250, 250, 1)',
97
+ 'primary-foreground': 'rgba(24, 24, 27, 1)',
98
+ secondary: 'rgba(39, 39, 42, 1)',
99
+ 'secondary-foreground': 'rgba(250, 250, 250, 1)',
100
+ muted: 'rgba(39, 39, 42, 1)',
101
+ 'muted-foreground': 'rgba(161, 161, 170, 1)',
102
+ accent: 'rgba(39, 39, 42, 1)',
103
+ 'accent-foreground': 'rgba(250, 250, 250, 1)',
104
+ destructive: 'rgba(127, 29, 29, 1)',
105
+ 'destructive-foreground': 'rgba(250, 250, 250, 1)',
106
+ 'destructive-subtle': 'rgba(127, 29, 29, 0.1)',
107
+ border: 'rgba(39, 39, 42, 1)',
108
+ input: 'rgba(39, 39, 42, 1)',
109
+ ring: 'rgba(212, 212, 216, 1)',
110
+ 'ring-subtle': 'rgba(212, 212, 216, 0.4)',
111
+ 'chart-1': 'rgba(38, 98, 217, 1)',
112
+ 'chart-2': 'rgba(46, 184, 138, 1)',
113
+ 'chart-3': 'rgba(232, 140, 48, 1)',
114
+ 'chart-4': 'rgba(175, 87, 219, 1)',
115
+ 'chart-5': 'rgba(226, 54, 112, 1)',
116
+ 'glass-tint': 'rgba(250, 250, 250, 1)',
117
+ 'glass-tint-foreground': 'rgba(24, 24, 27, 1)',
118
+ 'glass-foreground': 'rgba(250, 250, 250, 1)',
119
+ 'glass-muted-foreground': 'rgba(212, 212, 216, 1)',
120
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
121
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
122
+ };
123
+
124
+ export const UI_ICON_THEME = new InjectionToken<UiIconTheme>('UI_ICON_THEME', {
125
+ providedIn: 'root',
126
+ factory: () => ({ light: ICON_LIGHT_COLORS, dark: ICON_DARK_COLORS }),
127
+ });
128
+
129
+ /**
130
+ * Overrides icon colors, merged over the defaults: pass only the tokens you
131
+ * changed in your CSS, or just a color scheme.
132
+ *
133
+ * ```ts
134
+ * // An app that switches .dark with the device appearance:
135
+ * providers: [provideIconTheme({ colorScheme: 'system' })]
136
+ * ```
137
+ */
138
+ export const provideIconTheme = (theme: {
139
+ light?: IconColorTokens;
140
+ dark?: IconColorTokens;
141
+ colorScheme?: IconColorSchemeOption;
142
+ }): Provider => ({
143
+ provide: UI_ICON_THEME,
144
+ useValue: {
145
+ light: { ...ICON_LIGHT_COLORS, ...theme.light },
146
+ dark: { ...ICON_DARK_COLORS, ...theme.dark },
147
+ colorScheme: theme.colorScheme,
148
+ } satisfies UiIconTheme,
149
+ });
150
+
151
+ /**
152
+ * The color icons inside an element use when they're given none. A container
153
+ * whose variant recolors its content (a destructive nav drawer item) provides
154
+ * it, and every `<ui-icon>` projected into it picks it up through DI, the way
155
+ * text would inherit `color` on the web.
156
+ */
157
+ export const UI_ICON_COLOR = new InjectionToken<Signal<string | undefined>>(
158
+ 'UI_ICON_COLOR',
159
+ );
160
+
161
+ /** What an icon draws with when nothing sets its color. */
162
+ export const DEFAULT_ICON_COLOR = 'var(--foreground)';
163
+
164
+ /**
165
+ * Turns a color into one an SVG parser can draw: `var(--token)` (with or
166
+ * without a fallback) becomes the token's value. Anything else (`#2563eb`,
167
+ * `rgba(…)`) is already concrete and passes through.
168
+ */
169
+ export const resolveIconColor = (
170
+ color: string,
171
+ tokens: IconColorTokens,
172
+ ): string => {
173
+ const value = color.trim();
174
+ if (!value.startsWith('var(') || !value.endsWith(')')) return value;
175
+
176
+ const inner = value.slice(4, -1);
177
+ // A custom property name can't contain a comma, so the first comma always
178
+ // separates the name from the fallback.
179
+ const comma = inner.indexOf(',');
180
+ const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
181
+ const token = tokens[name.replace(/^--/, '')];
182
+ if (token !== undefined) return token;
183
+ // An unknown token falls back the way CSS does: to the var()'s fallback when
184
+ // it has one, and otherwise to the foreground so the icon stays visible.
185
+ return comma < 0
186
+ ? (tokens['foreground'] ?? value)
187
+ : resolveIconColor(inner.slice(comma + 1), tokens);
188
+ };
189
+
190
+ /**
191
+ * The color scheme the app's theme is in, as {@link provideIconTheme} sets it
192
+ * (`'light'` unless it says otherwise). For what has to match the app's CSS
193
+ * theme but can't read it: icons pick their color table with it, and glass
194
+ * picks a light or dark material.
195
+ */
196
+ export const injectColorScheme = (): Signal<IconColorScheme> => {
197
+ const option = inject(UI_ICON_THEME).colorScheme ?? 'light';
198
+ const appearance = inject(LynxTheme);
199
+ return computed(() =>
200
+ option === 'system'
201
+ ? appearance.isDarkMode()
202
+ ? 'dark'
203
+ : 'light'
204
+ : typeof option === 'function'
205
+ ? option()
206
+ : option,
207
+ );
208
+ };
209
+
210
+ /**
211
+ * For an SVG component's constructor: a function that resolves its `color`
212
+ * input (or, without one, the surrounding {@link UI_ICON_COLOR}, then the
213
+ * foreground) against the active color table. It reads signals, so a
214
+ * `computed()` calling it follows scheme and theme changes.
215
+ */
216
+ export const injectIconColor = (): ((color: string | undefined) => string) => {
217
+ const theme = inject(UI_ICON_THEME);
218
+ const scheme = injectColorScheme();
219
+ const inherited = inject(UI_ICON_COLOR, { optional: true });
220
+
221
+ const tokens = computed(() =>
222
+ scheme() === 'dark' ? theme.dark : theme.light,
223
+ );
224
+
225
+ return (color) =>
226
+ resolveIconColor(color ?? inherited?.() ?? DEFAULT_ICON_COLOR, tokens());
227
+ };
@@ -1,47 +1,107 @@
1
- import { Component } from '@angular/core';
1
+ import { Component, signal } from '@angular/core';
2
2
  import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
3
  import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
6
4
  import { UiIcon } from './icon';
5
+ import {
6
+ ICON_DARK_COLORS,
7
+ ICON_LIGHT_COLORS,
8
+ UI_ICON_COLOR,
9
+ provideIconTheme,
10
+ } from './icon-theme';
7
11
  import { ICONS, type IconName } from './icons';
8
12
 
9
13
  @Component({
10
- standalone: true,
11
- imports: [UiIcon, LYNX_ELEMENTS],
12
- template: `<ui-icon [name]="name" [size]="size" [color]="color" />`,
14
+ imports: [UiIcon],
15
+ template: `
16
+ <ui-icon
17
+ [name]="name()"
18
+ [size]="size()"
19
+ [color]="color()"
20
+ [class]="className()"
21
+ />
22
+ `,
13
23
  })
14
24
  class IconHost {
15
- name: IconName = 'check';
16
- size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
17
- color?: string;
25
+ readonly name = signal<IconName>('check');
26
+ readonly size = signal<'xs' | 'sm' | 'md' | 'lg'>('md');
27
+ readonly color = signal<string | undefined>(undefined);
28
+ readonly className = signal('');
18
29
  }
19
30
 
31
+ const renderIcon = async (providers: Parameters<typeof render>[1] = {}) => {
32
+ const result = await render(IconHost, providers);
33
+ return {
34
+ ...result,
35
+ host: result.componentRef.instance as IconHost,
36
+ svg: () => result.container.querySelector('svg')!,
37
+ };
38
+ };
39
+
20
40
  describe('UiIcon', () => {
21
- it('renders the requested icon at default size', async () => {
22
- const { container } = await render(IconHost);
23
- expect(container).toBeTruthy();
41
+ it('draws the named icon at the default size', async () => {
42
+ const { svg } = await renderIcon();
43
+ expect(svg().getAttribute('style')).toContain('24px');
44
+ expect(svg().getAttribute('content')).toContain('<svg');
45
+ });
46
+
47
+ it('draws another icon when the name changes', async () => {
48
+ const { host, svg } = await renderIcon();
49
+ const check = svg().getAttribute('content');
50
+ host.name.set('x');
51
+ await waitForUpdate();
52
+ expect(svg().getAttribute('content')).not.toBe(check);
24
53
  });
25
54
 
26
55
  it('applies the size input to the width/height style', async () => {
27
- // JIT template bindings are inert against `input()` — drive the signals
28
- // directly so sizeStyle and svgContent both re-run.
29
- const { componentRef, container } = await render(UiIcon);
30
- setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
31
- setInputSignal((componentRef.instance as UiIcon).size, 'xs');
56
+ const { host, svg } = await renderIcon();
57
+ host.size.set('xs');
32
58
  await waitForUpdate();
33
- const svg = container.querySelector('svg');
34
- expect(svg?.getAttribute('style')).toContain('16px');
59
+ expect(svg().getAttribute('style')).toContain('16px');
35
60
  });
36
61
 
37
62
  it('substitutes currentColor with the color input value', async () => {
38
- const { componentRef, container } = await render(UiIcon);
39
- setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
40
- setInputSignal((componentRef.instance as UiIcon).color, 'rgb(255, 0, 0)');
63
+ // Lynx's SVG parser reads neither `currentColor` nor CSS variables.
64
+ const { host, svg } = await renderIcon();
65
+ host.color.set('rgb(255, 0, 0)');
66
+ await waitForUpdate();
67
+ expect(svg().getAttribute('content')).toContain('rgb(255, 0, 0)');
68
+ expect(svg().getAttribute('content')).not.toContain('currentColor');
69
+ });
70
+
71
+ it("draws in the light theme's foreground when no color is set", async () => {
72
+ const { svg } = await renderIcon();
73
+ const content = svg().getAttribute('content');
74
+ expect(content).toContain(ICON_LIGHT_COLORS['foreground']);
75
+ expect(content).not.toContain('currentColor');
76
+ });
77
+
78
+ it('resolves a theme token against the active color scheme', async () => {
79
+ const { host, svg } = await renderIcon({
80
+ providers: [provideIconTheme({ colorScheme: 'dark' })],
81
+ });
82
+ host.color.set('var(--primary)');
83
+ await waitForUpdate();
84
+ expect(svg().getAttribute('content')).toContain(
85
+ ICON_DARK_COLORS['primary'],
86
+ );
87
+ });
88
+
89
+ it("takes its container's color when it sets none", async () => {
90
+ const { svg } = await renderIcon({
91
+ providers: [
92
+ { provide: UI_ICON_COLOR, useValue: signal('var(--destructive)') },
93
+ ],
94
+ });
95
+ expect(svg().getAttribute('content')).toContain(
96
+ ICON_LIGHT_COLORS['destructive'],
97
+ );
98
+ });
99
+
100
+ it('applies the class input to the svg', async () => {
101
+ const { host, svg } = await renderIcon();
102
+ host.className.set('mr-2');
41
103
  await waitForUpdate();
42
- const svg = container.querySelector('svg');
43
- expect(svg?.getAttribute('content')).toContain('rgb(255, 0, 0)');
44
- expect(svg?.getAttribute('content')).not.toContain('currentColor');
104
+ expect(svg().getAttribute('class')).toBe('mr-2');
45
105
  });
46
106
  });
47
107
 
@@ -1,6 +1,8 @@
1
1
  import { Component, ViewEncapsulation, input } from '@angular/core';
2
2
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
3
3
 
4
+ import { cn } from '../../utils/cn';
5
+ import { injectIconColor } from './icon-theme';
4
6
  import { type IconName, ICONS } from './icons';
5
7
 
6
8
  const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
@@ -16,39 +18,54 @@ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
16
18
  imports: [LYNX_ELEMENTS],
17
19
  encapsulation: ViewEncapsulation.None,
18
20
  template: `
19
- <svg [attr.content]="svgContent()" [attr.style]="sizeStyle()" />
21
+ <svg
22
+ [attr.content]="svgContent()"
23
+ [attr.style]="sizeStyle()"
24
+ [class]="svgClass()"
25
+ />
20
26
  `,
21
27
  })
22
28
  export class UiIcon {
23
29
  readonly name = input.required<IconName>();
24
30
  readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
31
+ /**
32
+ * A literal color or a theme token (`var(--primary)`). Without one, the icon
33
+ * takes the color its container sets (see `UI_ICON_COLOR`), then the theme's
34
+ * foreground.
35
+ */
25
36
  readonly color = input<string | undefined>(undefined);
37
+ readonly userClass = input<string>('', { alias: 'class' });
38
+
39
+ readonly #resolveColor = injectIconColor();
26
40
 
27
41
  protected svgContent(): string {
28
- let svg = ICONS[this.name()];
29
- const c = this.color();
30
- if (c) {
31
- // Lynx SVG elements don't propagate the CSS `color` property into SVG
32
- // stroke attributes, so we substitute the literal `currentColor` in the
33
- // markup string rather than relying on `currentColor` CSS propagation.
34
- //
35
- // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
36
- // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
37
- // because it's a runtime method (not syntax) the build's down-leveling does
38
- // not polyfill it, so it simply doesn't exist there. Calling it throws
39
- // "main-thread.js exception: not a function" from inside Angular change
40
- // detection, which aborts the whole tick before Lynx flushes the element
41
- // tree — so NO icon paints (not just a colored one), and the error count
42
- // climbs on every render/interaction as each tick re-runs and re-throws.
43
- // split/join is ES2019-safe and is preferred over `replace(/.../g)` here
44
- // because the search term is a plain literal and needs no regex escaping.
45
- svg = svg.split('currentColor').join(c);
46
- }
47
- return svg;
42
+ // Lynx SVG elements don't propagate the CSS `color` property into SVG
43
+ // stroke attributes, so we substitute the literal `currentColor` in the
44
+ // markup string rather than relying on `currentColor` CSS propagation.
45
+ // The color is resolved first: the SVG parser can't read `var(--x)` either
46
+ // (see icon-theme.ts).
47
+ //
48
+ // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
49
+ // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
50
+ // because it's a runtime method (not syntax) the build's down-leveling does
51
+ // not polyfill it, so it simply doesn't exist there. Calling it throws
52
+ // "main-thread.js exception: not a function" from inside Angular change
53
+ // detection, which aborts the whole tick before Lynx flushes the element
54
+ // tree — so NO icon paints (not just a colored one), and the error count
55
+ // climbs on every render/interaction as each tick re-runs and re-throws.
56
+ // split/join is ES2019-safe and is preferred over `replace(/.../g)` here
57
+ // because the search term is a plain literal and needs no regex escaping.
58
+ return ICONS[this.name()]
59
+ .split('currentColor')
60
+ .join(this.#resolveColor(this.color()));
48
61
  }
49
62
 
50
63
  protected sizeStyle(): string {
51
64
  const px = SIZE_MAP[this.size()];
52
65
  return `width: ${px}px; height: ${px}px;`;
53
66
  }
67
+
68
+ protected svgClass(): string {
69
+ return cn(this.userClass());
70
+ }
54
71
  }