@blotch/dolan 22.2.3 → 22.2.5

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 (227) hide show
  1. package/dist/commands/add.js +9 -1
  2. package/dist/commands/add.js.map +1 -1
  3. package/dist/commands/add.test.js +38 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +258 -21
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +425 -11
  8. package/dist/commands/doctor.test.js.map +1 -1
  9. package/dist/commands/init.d.ts +2 -2
  10. package/dist/commands/init.js +38 -18
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/init.test.js +105 -2
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/theme.js +1 -1
  15. package/dist/commands/theme.js.map +1 -1
  16. package/dist/commands/update.d.ts +7 -0
  17. package/dist/commands/update.js +194 -50
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +570 -3
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +17 -0
  22. package/dist/lockfile.js +56 -1
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +65 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +119 -11
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +61 -6
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +27 -0
  33. package/dist/test-utils.js +45 -1
  34. package/dist/test-utils.js.map +1 -1
  35. package/dist/testing/fire-lynx-event.d.ts +14 -0
  36. package/dist/testing/fire-lynx-event.js +19 -0
  37. package/dist/testing/fire-lynx-event.js.map +1 -0
  38. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  39. package/dist/testing/fire-lynx-event.test.js +44 -0
  40. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  41. package/dist/testing/lynx-main-thread.d.ts +22 -0
  42. package/dist/testing/lynx-main-thread.js +11 -0
  43. package/dist/testing/lynx-main-thread.js.map +1 -0
  44. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  45. package/dist/testing/lynx-main-thread.test.js +24 -0
  46. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  47. package/dist/testing/set-input-signal.d.ts +5 -4
  48. package/dist/testing/set-input-signal.js +5 -4
  49. package/dist/testing/set-input-signal.js.map +1 -1
  50. package/dist/ui/boolean-inputs.test.ts +76 -0
  51. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  52. package/dist/ui/components/accordion/accordion.ts +19 -14
  53. package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
  54. package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
  55. package/dist/ui/components/action-sheet/index.ts +1 -0
  56. package/dist/ui/components/alert/alert.test.ts +67 -19
  57. package/dist/ui/components/alert/alert.ts +14 -16
  58. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
  59. package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
  60. package/dist/ui/components/alert-dialog/index.ts +5 -0
  61. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  62. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  63. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  64. package/dist/ui/components/badge/badge.test.ts +57 -23
  65. package/dist/ui/components/badge/badge.ts +14 -15
  66. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  67. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
  68. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
  69. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  70. package/dist/ui/components/button/button.test.ts +646 -2
  71. package/dist/ui/components/button/button.ts +443 -64
  72. package/dist/ui/components/button/index.ts +2 -0
  73. package/dist/ui/components/button-group/button-group.test.ts +528 -2
  74. package/dist/ui/components/button-group/button-group.ts +428 -70
  75. package/dist/ui/components/button-group/index.ts +3 -0
  76. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  77. package/dist/ui/components/card/card.test.ts +97 -61
  78. package/dist/ui/components/card/card.ts +2 -1
  79. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  80. package/dist/ui/components/chart/chart-presets.ts +14 -7
  81. package/dist/ui/components/chart/chart-theme.ts +12 -0
  82. package/dist/ui/components/chart/chart.test.ts +1031 -0
  83. package/dist/ui/components/chart/chart.ts +66 -46
  84. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  85. package/dist/ui/components/checkbox/checkbox.ts +25 -24
  86. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  87. package/dist/ui/components/collapsible/collapsible.ts +21 -15
  88. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  89. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  90. package/dist/ui/components/context-menu/index.ts +1 -0
  91. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  92. package/dist/ui/components/dialog/dialog.ts +35 -29
  93. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  94. package/dist/ui/components/dock/dock-scroll.ts +142 -0
  95. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  96. package/dist/ui/components/dock/dock.test.ts +752 -0
  97. package/dist/ui/components/dock/dock.ts +1438 -0
  98. package/dist/ui/components/dock/index.ts +2 -0
  99. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  100. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  101. package/dist/ui/components/dynamic-color/index.ts +5 -0
  102. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  103. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  104. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  105. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  106. package/dist/ui/components/glass/glass-design.ts +119 -0
  107. package/dist/ui/components/glass/glass.test.ts +376 -0
  108. package/dist/ui/components/glass/glass.ts +430 -0
  109. package/dist/ui/components/glass/index.ts +19 -0
  110. package/dist/ui/components/icon/icon-theme.test.ts +64 -0
  111. package/dist/ui/components/icon/icon-theme.ts +70 -16
  112. package/dist/ui/components/icon/icon.test.ts +52 -41
  113. package/dist/ui/components/input/input.test.ts +178 -101
  114. package/dist/ui/components/input/input.ts +17 -13
  115. package/dist/ui/components/label/label.test.ts +11 -24
  116. package/dist/ui/components/label/label.ts +8 -2
  117. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  118. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  119. package/dist/ui/components/list/list.test.ts +80 -49
  120. package/dist/ui/components/list/list.ts +5 -4
  121. package/dist/ui/components/menu/index.ts +25 -0
  122. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  123. package/dist/ui/components/menu/menu-placement.ts +159 -0
  124. package/dist/ui/components/menu/menu-popup.test.ts +636 -0
  125. package/dist/ui/components/menu/menu-popup.ts +565 -0
  126. package/dist/ui/components/menu/menu.test.ts +523 -0
  127. package/dist/ui/components/menu/menu.ts +455 -0
  128. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  129. package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
  130. package/dist/ui/components/navigation-bar/index.ts +1 -0
  131. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  132. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  133. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  134. package/dist/ui/components/popover/index.ts +9 -0
  135. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  136. package/dist/ui/components/popover/popover-panel.ts +603 -0
  137. package/dist/ui/components/popover/popover.test.ts +286 -0
  138. package/dist/ui/components/popover/popover.ts +207 -0
  139. package/dist/ui/components/progress/progress.test.ts +102 -46
  140. package/dist/ui/components/progress/progress.ts +54 -55
  141. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  142. package/dist/ui/components/radio-group/radio-group.ts +26 -25
  143. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  144. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  145. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  146. package/dist/ui/components/search-field/index.ts +15 -0
  147. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  148. package/dist/ui/components/search-field/keyboard.ts +123 -0
  149. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  150. package/dist/ui/components/search-field/search-field.ts +628 -0
  151. package/dist/ui/components/segmented-control/index.ts +8 -0
  152. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  153. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  154. package/dist/ui/components/select/select.test.ts +138 -78
  155. package/dist/ui/components/select/select.ts +19 -12
  156. package/dist/ui/components/separator/separator.test.ts +18 -17
  157. package/dist/ui/components/sheet/sheet.test.ts +151 -11
  158. package/dist/ui/components/sheet/sheet.ts +68 -5
  159. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  160. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  161. package/dist/ui/components/slider/index.ts +1 -0
  162. package/dist/ui/components/slider/slider.test.ts +695 -0
  163. package/dist/ui/components/slider/slider.ts +901 -0
  164. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  165. package/dist/ui/components/spinner/spinner.ts +16 -19
  166. package/dist/ui/components/switch/index.ts +1 -1
  167. package/dist/ui/components/switch/switch.test.ts +534 -48
  168. package/dist/ui/components/switch/switch.ts +613 -48
  169. package/dist/ui/components/tab-bar/index.ts +1 -0
  170. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  171. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  172. package/dist/ui/components/tabs/index.ts +7 -1
  173. package/dist/ui/components/tabs/tabs.test.ts +484 -150
  174. package/dist/ui/components/tabs/tabs.ts +344 -149
  175. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  176. package/dist/ui/components/textarea/textarea.ts +16 -13
  177. package/dist/ui/components/toast/toast.test.ts +434 -350
  178. package/dist/ui/components/toast/toast.ts +42 -51
  179. package/dist/ui/components/toggle/index.ts +1 -0
  180. package/dist/ui/components/toggle/toggle.test.ts +559 -146
  181. package/dist/ui/components/toggle/toggle.ts +241 -26
  182. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  183. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  184. package/dist/ui/components/toolbar/index.ts +12 -0
  185. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  186. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  187. package/dist/ui/theme/chart-colors.test.ts +8 -3
  188. package/dist/ui/theme/dark.css +38 -2
  189. package/dist/ui/theme/default.css +55 -7
  190. package/dist/ui/theme/icon-colors.test.ts +8 -3
  191. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  192. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  193. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  194. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  195. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  196. package/dist/ui/theme/tailwind.css +113 -0
  197. package/dist/ui/utils/animate.test.ts +13 -1
  198. package/dist/utils/analyze.d.ts +19 -0
  199. package/dist/utils/analyze.js +16 -0
  200. package/dist/utils/analyze.js.map +1 -1
  201. package/dist/utils/analyze.test.js +14 -1
  202. package/dist/utils/analyze.test.js.map +1 -1
  203. package/dist/utils/installed.d.ts +25 -1
  204. package/dist/utils/installed.js +33 -0
  205. package/dist/utils/installed.js.map +1 -1
  206. package/dist/utils/installed.test.js +55 -1
  207. package/dist/utils/installed.test.js.map +1 -1
  208. package/dist/utils/resolve-paths.d.ts +4 -4
  209. package/dist/utils/resolve-paths.js +5 -7
  210. package/dist/utils/resolve-paths.js.map +1 -1
  211. package/dist/utils/resolve-paths.test.js +9 -5
  212. package/dist/utils/resolve-paths.test.js.map +1 -1
  213. package/dist/utils/tailwind.d.ts +45 -0
  214. package/dist/utils/tailwind.js +90 -0
  215. package/dist/utils/tailwind.js.map +1 -0
  216. package/dist/utils/tailwind.test.d.ts +1 -0
  217. package/dist/utils/tailwind.test.js +134 -0
  218. package/dist/utils/tailwind.test.js.map +1 -0
  219. package/package.json +9 -1
  220. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  221. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  222. package/dist/ui/components/button/button-render.test.ts +0 -132
  223. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  224. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  225. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  226. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  227. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -13,52 +13,100 @@
13
13
  * rgba() values and referencing them directly sidesteps color-function parsing
14
14
  * entirely and renders correctly on Lynx.
15
15
  *
16
- * Tradeoff: Tailwind opacity modifiers on semantic colors (e.g. bg-primary/50)
17
- * do NOT work, because the color is an opaque value with no separable channels.
18
- * Where a translucent semantic color is genuinely needed, add a dedicated
19
- * rgba() variable (see --destructive-subtle) or use a solid token.
16
+ * Each opaque color also has its channels in a `-rgb` twin
17
+ * (`--primary-rgb: 24, 24, 27`) for Tailwind's opacity modifiers:
18
+ * `bg-primary/50` becomes `rgba(var(--primary-rgb), 0.5)`. Lynx substitutes
19
+ * var() as text before it parses the color, so the comma-separated list fills
20
+ * three arguments. Keep each pair the same color when you edit one: plain
21
+ * classes read `--x` and modifiers read `--x-rgb`.
20
22
  *
21
23
  * Apply to `page` (Lynx root element) or import globally.
22
24
  */
23
25
 
24
26
  page {
25
27
  --background: rgba(255, 255, 255, 1);
28
+ --background-rgb: 255, 255, 255;
26
29
  --foreground: rgba(9, 9, 11, 1);
30
+ --foreground-rgb: 9, 9, 11;
27
31
 
28
32
  --card: rgba(255, 255, 255, 1);
33
+ --card-rgb: 255, 255, 255;
29
34
  --card-foreground: rgba(9, 9, 11, 1);
35
+ --card-foreground-rgb: 9, 9, 11;
30
36
 
31
37
  --primary: rgba(24, 24, 27, 1);
38
+ --primary-rgb: 24, 24, 27;
32
39
  --primary-foreground: rgba(250, 250, 250, 1);
40
+ --primary-foreground-rgb: 250, 250, 250;
33
41
 
34
42
  --secondary: rgba(244, 244, 245, 1);
43
+ --secondary-rgb: 244, 244, 245;
35
44
  --secondary-foreground: rgba(24, 24, 27, 1);
45
+ --secondary-foreground-rgb: 24, 24, 27;
36
46
 
37
47
  --muted: rgba(244, 244, 245, 1);
48
+ --muted-rgb: 244, 244, 245;
38
49
  --muted-foreground: rgba(113, 113, 122, 1);
50
+ --muted-foreground-rgb: 113, 113, 122;
39
51
 
40
52
  --accent: rgba(244, 244, 245, 1);
53
+ --accent-rgb: 244, 244, 245;
41
54
  --accent-foreground: rgba(24, 24, 27, 1);
55
+ --accent-foreground-rgb: 24, 24, 27;
42
56
 
43
57
  --destructive: rgba(239, 68, 68, 1);
58
+ --destructive-rgb: 239, 68, 68;
44
59
  --destructive-foreground: rgba(250, 250, 250, 1);
60
+ --destructive-foreground-rgb: 250, 250, 250;
45
61
  --destructive-subtle: rgba(239, 68, 68, 0.1);
46
62
 
47
63
  --border: rgba(228, 228, 231, 1);
64
+ --border-rgb: 228, 228, 231;
48
65
  --input: rgba(228, 228, 231, 1);
66
+ --input-rgb: 228, 228, 231;
49
67
  --ring: rgba(24, 24, 27, 1);
50
- /* Translucent variant of --ring for a softer focus outline. A separate
51
- pre-composed value is needed because Tailwind opacity modifiers can't
52
- apply to the opaque --ring, and Lynx can't inject alpha into it either. */
68
+ --ring-rgb: 24, 24, 27;
69
+ /* Translucent variant of --ring for the soft outer focus ring. Input and
70
+ textarea set it in an inline box-shadow, where no Tailwind modifier
71
+ reaches, so it stays a complete color. */
53
72
  --ring-subtle: rgba(24, 24, 27, 0.4);
54
73
 
55
74
  /* Series palette for charts (bg-chart-1, and chart-theme.ts in the chart
56
75
  component, which mirrors these values for SVG). */
57
76
  --chart-1: rgba(231, 110, 80, 1);
77
+ --chart-1-rgb: 231, 110, 80;
58
78
  --chart-2: rgba(42, 157, 144, 1);
79
+ --chart-2-rgb: 42, 157, 144;
59
80
  --chart-3: rgba(39, 71, 84, 1);
81
+ --chart-3-rgb: 39, 71, 84;
60
82
  --chart-4: rgba(232, 196, 104, 1);
83
+ --chart-4-rgb: 232, 196, 104;
61
84
  --chart-5: rgba(244, 164, 98, 1);
85
+ --chart-5-rgb: 244, 164, 98;
86
+
87
+ /* Liquid Glass (the glass component). Glass draws its own material, so
88
+ these color what sits on it, and tint it:
89
+ - glass-tint: the color tinted glass takes, such as a prominent glass
90
+ button's, and glass-tint-foreground what sits on it.
91
+ - glass-foreground and glass-muted-foreground: text and icons on regular
92
+ glass. Darker than muted-foreground, since what shows through the
93
+ glass varies.
94
+ - glass-clear-foreground: on clear glass, which sits over photos and
95
+ video, so it's white in both themes.
96
+ - glass-dim: the dimming layer under clear glass over bright content,
97
+ black at about a third, as Apple suggests. Already translucent, so it
98
+ has no channels. */
99
+ --glass-tint: rgba(24, 24, 27, 1);
100
+ --glass-tint-rgb: 24, 24, 27;
101
+ --glass-tint-foreground: rgba(250, 250, 250, 1);
102
+ --glass-tint-foreground-rgb: 250, 250, 250;
103
+ --glass-foreground: rgba(9, 9, 11, 1);
104
+ --glass-foreground-rgb: 9, 9, 11;
105
+ --glass-muted-foreground: rgba(63, 63, 70, 1);
106
+ --glass-muted-foreground-rgb: 63, 63, 70;
107
+ --glass-clear-foreground: rgba(255, 255, 255, 1);
108
+ --glass-clear-foreground-rgb: 255, 255, 255;
109
+ --glass-dim: rgba(0, 0, 0, 0.35);
62
110
 
63
111
  --radius: 8px;
64
112
  }
@@ -1,3 +1,6 @@
1
+ /// <reference types="node" />
2
+ // Not copied into apps (it checks the theme against a component), so it can
3
+ // read the theme files with Node.
1
4
  import { readFileSync } from 'node:fs';
2
5
  import { describe, expect, it } from 'vitest';
3
6
  import {
@@ -11,7 +14,9 @@ import {
11
14
  * theme's colors. This keeps the copy in step with default.css and dark.css.
12
15
  */
13
16
  const tokens = (file: string): Record<string, string> => {
14
- const css = readFileSync(new URL(file, import.meta.url), 'utf8');
17
+ // Relative to the package, where `ng test` runs: the test runs from a build
18
+ // of it elsewhere, so `import.meta.url` doesn't point next to the CSS.
19
+ const css = readFileSync(`src/lib/theme/${file}`, 'utf8');
15
20
  const values: Record<string, string> = {};
16
21
  for (const match of css.matchAll(/--([\w-]+):\s*(rgba\([^)]*\))/g)) {
17
22
  values[match[1]!] = match[2]!;
@@ -21,10 +26,10 @@ const tokens = (file: string): Record<string, string> => {
21
26
 
22
27
  describe('icon color tables', () => {
23
28
  it('match default.css', () => {
24
- expect(ICON_LIGHT_COLORS).toEqual(tokens('./default.css'));
29
+ expect(ICON_LIGHT_COLORS).toEqual(tokens('default.css'));
25
30
  });
26
31
 
27
32
  it('match dark.css', () => {
28
- expect(ICON_DARK_COLORS).toEqual(tokens('./dark.css'));
33
+ expect(ICON_DARK_COLORS).toEqual(tokens('dark.css'));
29
34
  });
30
35
  });
@@ -0,0 +1,84 @@
1
+ /// <reference types="node" />
2
+ // Not copied into apps (it reads the theme's CSS with Node), like the other
3
+ // theme checks next to it.
4
+ import { readFileSync } from 'node:fs';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { blotchPlugin } from './tailwind-plugin';
8
+
9
+ /**
10
+ * Each opaque theme color is written twice: complete (`--primary:
11
+ * rgba(24, 24, 27, 1)`) for plain classes, and as channels (`--primary-rgb:
12
+ * 24, 24, 27`) for opacity modifiers. Nothing at runtime ties the two
13
+ * together, so a typo would make `bg-primary/50` a different color from
14
+ * `bg-primary`. These tests keep the pairs in step.
15
+ */
16
+ const variables = (file: string): Map<string, string> => {
17
+ // Comments mention variables too, so they go first.
18
+ // Relative to the package, where `ng test` runs: the test runs from a build
19
+ // of it elsewhere, so `import.meta.url` doesn't point next to the CSS.
20
+ const css = readFileSync(`src/lib/theme/${file}`, 'utf8').replace(
21
+ /\/\*[\s\S]*?\*\//g,
22
+ '',
23
+ );
24
+ return new Map(
25
+ [...css.matchAll(/(--[\w-]+):\s*([^;{}]+);/g)].map((match) => [
26
+ match[1]!,
27
+ match[2]!.trim(),
28
+ ]),
29
+ );
30
+ };
31
+
32
+ /**
33
+ * The `--x-rgb` variables the plugin's opacity modifiers read.
34
+ */
35
+ const channelVariables = (): string[] => {
36
+ const factory = blotchPlugin as unknown as {
37
+ config: { theme: { extend: { colors: Record<string, unknown> } } };
38
+ };
39
+ const names: string[] = [];
40
+ const walk = (value: unknown) => {
41
+ if (typeof value === 'function') {
42
+ const css = (value as (options: object) => string)({
43
+ opacityValue: '0.5',
44
+ });
45
+ names.push(
46
+ ...[...css.matchAll(/var\((--[\w-]+-rgb)\)/g)].map((m) => m[1]!),
47
+ );
48
+ } else if (typeof value === 'object' && value !== null) {
49
+ Object.values(value).forEach(walk);
50
+ }
51
+ };
52
+ walk(factory.config.theme.extend.colors);
53
+ return names;
54
+ };
55
+
56
+ describe.each(['./default.css', './dark.css'])('%s', (file) => {
57
+ const vars = variables(file);
58
+
59
+ it('has the channels of every color the plugin takes a modifier on', () => {
60
+ const names = channelVariables();
61
+ expect(names.length).toBeGreaterThan(0);
62
+ for (const name of names) {
63
+ expect(vars.get(name), name).toMatch(/^\d+, \d+, \d+$/);
64
+ }
65
+ });
66
+
67
+ it("gives each color's channels the same values as the color", () => {
68
+ for (const [name, value] of vars) {
69
+ if (!name.endsWith('-rgb')) continue;
70
+ const color = vars.get(name.slice(0, -'-rgb'.length));
71
+ expect(color, `${name} has no color`).toBe(`rgba(${value}, 1)`);
72
+ }
73
+ });
74
+
75
+ it('has channels for every opaque color', () => {
76
+ // A new token added without its channels would draw nothing with a
77
+ // modifier.
78
+ for (const [name, value] of vars) {
79
+ const channels = /^rgba\((\d+, \d+, \d+), 1\)$/.exec(value);
80
+ if (!channels) continue;
81
+ expect(vars.get(`${name}-rgb`), `${name}-rgb`).toBe(channels[1]);
82
+ }
83
+ });
84
+ });
@@ -0,0 +1,114 @@
1
+ /// <reference types="node" />
2
+ // Not copied into apps, unlike tailwind-plugin.test.ts, which covers the
3
+ // plugin there: this runs Tailwind's own build, through Node and postcss, which
4
+ // an app's `ng test` can't count on. It checks the classes the plugin's colors
5
+ // make, with the Lynx preset, as an app's build does.
6
+ import { createRequire } from 'node:module';
7
+ import preset from '@lynx-js/tailwind-preset';
8
+ import tailwindcss from 'tailwindcss';
9
+ import { describe, expect, it } from 'vitest';
10
+
11
+ import { blotchPlugin } from './tailwind-plugin';
12
+
13
+ /**
14
+ * The part of postcss these tests use.
15
+ */
16
+ type PostCss = (plugins: unknown[]) => {
17
+ process: (
18
+ css: string,
19
+ options: { from: undefined },
20
+ ) => Promise<{ css: string }>;
21
+ };
22
+
23
+ // postcss runs Tailwind. It's Tailwind's own dependency, so it's loaded from
24
+ // there rather than added to this package.
25
+ const postcss = createRequire(
26
+ createRequire(import.meta.url).resolve('tailwindcss'),
27
+ )('postcss') as PostCss;
28
+
29
+ /**
30
+ * Compiles `classes` with Tailwind, the Lynx preset and the plugin, as an
31
+ * app's build does, and returns the CSS.
32
+ */
33
+ const compile = async (classes: string): Promise<string> => {
34
+ const result = await postcss([
35
+ tailwindcss({
36
+ content: [{ raw: classes, extension: 'html' }],
37
+ presets: [preset],
38
+ plugins: [blotchPlugin],
39
+ }),
40
+ ]).process('@tailwind utilities;', { from: undefined });
41
+ return result.css;
42
+ };
43
+
44
+ describe('blotchPlugin colors in a Tailwind build', () => {
45
+ it('keeps plain color classes on the complete color', async () => {
46
+ const css = await compile('bg-primary text-foreground border-border');
47
+
48
+ expect(css).toContain('background-color: var(--primary)');
49
+ expect(css).toContain('color: var(--foreground)');
50
+ expect(css).toContain('border-color: var(--border)');
51
+ // No class reads the channels, so a theme without them still draws.
52
+ expect(css).not.toContain('-rgb');
53
+ });
54
+
55
+ it('turns opacity modifiers into rgba() over the channels', async () => {
56
+ const css = await compile(
57
+ 'bg-primary/50 text-foreground/70 border-border/60 bg-destructive/10 bg-chart-1/[.35]',
58
+ );
59
+
60
+ expect(css).toContain('background-color: rgba(var(--primary-rgb), 0.5)');
61
+ expect(css).toContain('color: rgba(var(--foreground-rgb), 0.7)');
62
+ expect(css).toContain('border-color: rgba(var(--border-rgb), 0.6)');
63
+ expect(css).toContain(
64
+ 'background-color: rgba(var(--destructive-rgb), 0.1)',
65
+ );
66
+ expect(css).toContain('background-color: rgba(var(--chart-1-rgb), .35)');
67
+ });
68
+
69
+ it('gives every mapped color a modifier, except the translucent destructive-subtle and glass-dim', async () => {
70
+ const names = [
71
+ 'background',
72
+ 'foreground',
73
+ 'card',
74
+ 'card-foreground',
75
+ 'primary',
76
+ 'primary-foreground',
77
+ 'secondary',
78
+ 'secondary-foreground',
79
+ 'muted',
80
+ 'muted-foreground',
81
+ 'accent',
82
+ 'accent-foreground',
83
+ 'destructive',
84
+ 'destructive-foreground',
85
+ 'border',
86
+ 'input',
87
+ 'ring',
88
+ 'chart-1',
89
+ 'chart-2',
90
+ 'chart-3',
91
+ 'chart-4',
92
+ 'chart-5',
93
+ 'glass-tint',
94
+ 'glass-tint-foreground',
95
+ 'glass-foreground',
96
+ 'glass-muted-foreground',
97
+ 'glass-clear-foreground',
98
+ ];
99
+ const css = await compile(
100
+ [
101
+ ...names.map((name) => `bg-${name}/50`),
102
+ 'bg-destructive-subtle/50',
103
+ 'bg-glass-dim/50',
104
+ ].join(' '),
105
+ );
106
+
107
+ for (const name of names) {
108
+ expect(css).toContain(`rgba(var(--${name}-rgb), 0.5)`);
109
+ }
110
+ // Tailwind can't add alpha to a complete color, so it makes no class.
111
+ expect(css).not.toContain('destructive-subtle');
112
+ expect(css).not.toContain('glass-dim');
113
+ });
114
+ });
@@ -0,0 +1,83 @@
1
+ /// <reference types="node" />
2
+ // Not copied into apps (it reads the theme's CSS with Node), like the other
3
+ // theme checks next to it.
4
+ import { readFileSync } from 'node:fs';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { blotchPlugin } from './tailwind-plugin';
8
+
9
+ /**
10
+ * tailwind.css is tailwind-plugin.ts for Tailwind 4. Nothing ties the two
11
+ * together, so these tests keep them mapping the same colors and radii, and
12
+ * keep every color it maps backed by the theme files.
13
+ */
14
+ const read = (file: string): string =>
15
+ // Relative to the package, where `ng test` runs: the test runs from a build
16
+ // of it elsewhere, so `import.meta.url` doesn't point next to the CSS.
17
+ readFileSync(`src/lib/theme/${file}`, 'utf8');
18
+
19
+ const tailwindCss = (): string =>
20
+ read('./tailwind.css').replace(/\/\*[\s\S]*?\*\//g, '');
21
+
22
+ /**
23
+ * `--color-x` → the theme variable it reads, from the `@theme` block.
24
+ */
25
+ const mappedColors = (): Map<string, string> =>
26
+ new Map(
27
+ [...tailwindCss().matchAll(/--color-([\w-]+):\s*var\((--[\w-]+)\);/g)].map(
28
+ (match) => [match[1]!, match[2]!],
29
+ ),
30
+ );
31
+
32
+ /**
33
+ * The plugin's color classes, flattened as Tailwind names them.
34
+ */
35
+ const pluginColors = (): Map<string, string> => {
36
+ const factory = blotchPlugin as unknown as {
37
+ config: { theme: { extend: { colors: Record<string, unknown> } } };
38
+ };
39
+ const colors = new Map<string, string>();
40
+ const walk = (value: unknown, path: string[]) => {
41
+ if (typeof value === 'object' && value !== null) {
42
+ for (const [key, child] of Object.entries(value)) {
43
+ walk(child, key === 'DEFAULT' ? path : [...path, key]);
44
+ }
45
+ return;
46
+ }
47
+ const css =
48
+ typeof value === 'function'
49
+ ? (value as (options: object) => string)({})
50
+ : String(value);
51
+ colors.set(path.join('-'), /var\((--[\w-]+)\)/.exec(css)![1]!);
52
+ };
53
+ walk(factory.config.theme.extend.colors, []);
54
+ return colors;
55
+ };
56
+
57
+ describe('tailwind.css', () => {
58
+ it('maps the same colors to the same variables as the plugin', () => {
59
+ expect(mappedColors()).toEqual(pluginColors());
60
+ });
61
+
62
+ it('maps the radii as the plugin does', () => {
63
+ const css = tailwindCss();
64
+ expect(css).toContain('--radius-lg: var(--radius);');
65
+ expect(css).toContain('--radius-md: calc(var(--radius) - 2px);');
66
+ expect(css).toContain('--radius-sm: calc(var(--radius) - 4px);');
67
+ });
68
+
69
+ it.each(['./default.css', './dark.css'])(
70
+ 'reads only variables %s sets, each opaque one with its channels',
71
+ (file) => {
72
+ const css = read(file);
73
+ for (const variable of mappedColors().values()) {
74
+ const value = new RegExp(`${variable}:\\s*([^;]+);`).exec(css)?.[1];
75
+ expect(value, variable).toBeDefined();
76
+ if (value!.endsWith(', 1)')) {
77
+ // The build writes bg-primary/50 as rgba(var(--primary-rgb), 0.5).
78
+ expect(css, `${variable}-rgb`).toContain(`${variable}-rgb:`);
79
+ }
80
+ }
81
+ },
82
+ );
83
+ });
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { addSafeAreaUtilities, blotchPlugin } from './tailwind-plugin';
3
+ import {
4
+ addSafeAreaUtilities,
5
+ blotchPlugin,
6
+ themeColor,
7
+ } from './tailwind-plugin';
4
8
 
5
9
  /**
6
10
  * These tests exercise the utility generator in isolation with mock Tailwind
@@ -119,6 +123,42 @@ describe('blotchPlugin', () => {
119
123
  };
120
124
  const colors = factory.config?.theme?.extend?.colors;
121
125
  expect(colors).toBeDefined();
122
- expect(colors!['background']).toBe('var(--background)');
126
+ const background = colors!['background'] as (options?: object) => string;
127
+ expect(background()).toBe('var(--background)');
128
+ });
129
+ });
130
+
131
+ describe('themeColor', () => {
132
+ const primary = themeColor('primary');
133
+
134
+ it('reads the complete color when no alpha is asked for', () => {
135
+ // caret-primary, shadow-primary: Tailwind passes no options.
136
+ expect(primary()).toBe('var(--primary)');
137
+ expect(primary({})).toBe('var(--primary)');
138
+ });
139
+
140
+ it("reads the complete color for a plain class with Tailwind's opacity variable", () => {
141
+ // bg-primary: the opacity variable is always 1 unless a deprecated
142
+ // bg-opacity-* utility changes it, and that never worked on these colors.
143
+ expect(
144
+ primary({
145
+ opacityVariable: '--tw-bg-opacity',
146
+ opacityValue: 'var(--tw-bg-opacity, 1)',
147
+ }),
148
+ ).toBe('var(--primary)');
149
+ });
150
+
151
+ it('reads the complete color for an alpha of 1', () => {
152
+ // bg-primary/100 passes '1'; theme('colors.primary') passes the number 1.
153
+ expect(primary({ opacityValue: '1' })).toBe('var(--primary)');
154
+ expect(primary({ opacityValue: 1 })).toBe('var(--primary)');
155
+ });
156
+
157
+ it('reads the channels for an opacity modifier', () => {
158
+ expect(primary({ opacityValue: '0.5' })).toBe(
159
+ 'rgba(var(--primary-rgb), 0.5)',
160
+ );
161
+ // A gradient's transparent end stop asks for 0.
162
+ expect(primary({ opacityValue: 0 })).toBe('rgba(var(--primary-rgb), 0)');
123
163
  });
124
164
  });
@@ -69,6 +69,51 @@ export const addSafeAreaUtilities = ({
69
69
  }
70
70
  };
71
71
 
72
+ /**
73
+ * What Tailwind passes a color it was given as a function:
74
+ * - an opacity modifier sets only `opacityValue` (`bg-primary/50` → `0.5`);
75
+ * - a plain class whose utility has an opacity variable sets
76
+ * `opacityVariable` too, and `opacityValue` is `var(--tw-bg-opacity, 1)`
77
+ * (`bg-primary`);
78
+ * - other plain classes pass neither (`caret-primary`);
79
+ * - `theme('colors.primary')` in a stylesheet passes an `opacityValue` of 1.
80
+ */
81
+ type ColorOptions = {
82
+ opacityValue?: string | number;
83
+ opacityVariable?: string;
84
+ };
85
+
86
+ /**
87
+ * A theme color that takes Tailwind's opacity modifiers on Lynx.
88
+ *
89
+ * A plain class reads the complete color, as it always has:
90
+ * `bg-primary` → `background-color: var(--primary)`. A modifier reads the
91
+ * color's channels from its `-rgb` twin:
92
+ * `bg-primary/50` → `background-color: rgba(var(--primary-rgb), 0.5)`.
93
+ *
94
+ * Lynx substitutes `var()` as text before it parses the value, so with
95
+ * `--primary-rgb: 24, 24, 27` its color parser sees `rgba(24, 24, 27, 0.5)`
96
+ * (`CSSValue::Substitution` in lynx-family/lynx). The channels are
97
+ * comma-separated because every Lynx color parser takes `rgba()` in that
98
+ * form. Lynx takes `hsl()` only with commas, which is why the shadcn/ui form,
99
+ * `hsl(var(--x) / 0.5)`, draws nothing.
100
+ *
101
+ * Plain classes stay on `var(--x)` so they don't need the channels: with a
102
+ * theme copied before the `-rgb` variables existed, every plain class still
103
+ * draws, and only modifiers don't. Tailwind's `<alpha-value>` placeholder
104
+ * can't do that, as it puts the channels in plain classes too. An alpha of 1
105
+ * is the plain color, so `bg-primary/100` and `theme('colors.primary')` read
106
+ * `var(--primary)` as well.
107
+ */
108
+ export const themeColor =
109
+ (name: string) =>
110
+ ({ opacityValue, opacityVariable }: ColorOptions = {}): string =>
111
+ opacityValue === undefined ||
112
+ opacityVariable !== undefined ||
113
+ String(opacityValue) === '1'
114
+ ? `var(--${name})`
115
+ : `rgba(var(--${name}-rgb), ${opacityValue})`;
116
+
72
117
  /**
73
118
  * Tailwind plugin that maps @blotch/ui CSS variables to semantic color utilities
74
119
  * and registers safe-area-aware padding utilities.
@@ -86,10 +131,9 @@ export const addSafeAreaUtilities = ({
86
131
  * referencing them directly avoids color-function parsing entirely. This matches
87
132
  * the official React Lynx Tailwind example.
88
133
  *
89
- * Tradeoff: Tailwind opacity modifiers on semantic colors (bg-primary/50) don't
90
- * work — the value is opaque with no separable channels. For the rare
91
- * translucent-semantic need, add a dedicated rgba() variable (see
92
- * `--destructive-subtle` → `destructive.subtle`) or use `opacity-*` on the element.
134
+ * Opacity modifiers (`bg-primary/50`, `text-foreground/70`,
135
+ * `border-border/60`) read each color's `--x-rgb` channels instead; see
136
+ * `themeColor`.
93
137
  *
94
138
  * Safe area: see `addSafeAreaUtilities` for the `*-safe` / `*-safe-<n>` padding
95
139
  * utilities that keep content clear of notches and home indicators.
@@ -106,49 +150,69 @@ export const blotchPlugin = plugin(
106
150
  {
107
151
  theme: {
108
152
  extend: {
153
+ // Tailwind's types only allow strings here, but it also takes a
154
+ // function, which it calls with the modifier's alpha: that's how it
155
+ // applies `<alpha-value>` itself. Every color but destructive.subtle
156
+ // and glass.dim, which are translucent already, has an `--x-rgb` twin
157
+ // in the theme, so takes opacity modifiers.
109
158
  colors: {
110
- background: 'var(--background)',
111
- foreground: 'var(--foreground)',
159
+ background: themeColor('background'),
160
+ foreground: themeColor('foreground'),
112
161
  primary: {
113
- DEFAULT: 'var(--primary)',
114
- foreground: 'var(--primary-foreground)',
162
+ DEFAULT: themeColor('primary'),
163
+ foreground: themeColor('primary-foreground'),
115
164
  },
116
165
  secondary: {
117
- DEFAULT: 'var(--secondary)',
118
- foreground: 'var(--secondary-foreground)',
166
+ DEFAULT: themeColor('secondary'),
167
+ foreground: themeColor('secondary-foreground'),
119
168
  },
120
169
  muted: {
121
- DEFAULT: 'var(--muted)',
122
- foreground: 'var(--muted-foreground)',
170
+ DEFAULT: themeColor('muted'),
171
+ foreground: themeColor('muted-foreground'),
123
172
  },
124
173
  accent: {
125
- DEFAULT: 'var(--accent)',
126
- foreground: 'var(--accent-foreground)',
174
+ DEFAULT: themeColor('accent'),
175
+ foreground: themeColor('accent-foreground'),
127
176
  },
128
177
  destructive: {
129
- DEFAULT: 'var(--destructive)',
130
- foreground: 'var(--destructive-foreground)',
131
- // Pre-composed translucent red for subtle destructive backgrounds,
132
- // since bg-destructive/10 (opacity modifier) can't work here.
178
+ DEFAULT: themeColor('destructive'),
179
+ foreground: themeColor('destructive-foreground'),
180
+ // Red at 10%, the destructive alert's background. It's
181
+ // translucent already, so it has no channels and takes no
182
+ // modifier. It predates modifiers and stays for the apps that
183
+ // use it: bg-destructive/10 draws the same color, but only in a
184
+ // theme that has --destructive-rgb.
133
185
  subtle: 'var(--destructive-subtle)',
134
186
  },
135
- border: 'var(--border)',
136
- input: 'var(--input)',
137
- ring: 'var(--ring)',
187
+ border: themeColor('border'),
188
+ input: themeColor('input'),
189
+ ring: themeColor('ring'),
138
190
  card: {
139
- DEFAULT: 'var(--card)',
140
- foreground: 'var(--card-foreground)',
191
+ DEFAULT: themeColor('card'),
192
+ foreground: themeColor('card-foreground'),
141
193
  },
142
194
  // Chart series colors, e.g. bg-chart-1 for a legend swatch that
143
195
  // matches the first series.
144
196
  chart: {
145
- 1: 'var(--chart-1)',
146
- 2: 'var(--chart-2)',
147
- 3: 'var(--chart-3)',
148
- 4: 'var(--chart-4)',
149
- 5: 'var(--chart-5)',
197
+ 1: themeColor('chart-1'),
198
+ 2: themeColor('chart-2'),
199
+ 3: themeColor('chart-3'),
200
+ 4: themeColor('chart-4'),
201
+ 5: themeColor('chart-5'),
150
202
  },
151
- },
203
+ // What sits on Liquid Glass, and its tint: text-glass-foreground,
204
+ // bg-glass-tint, bg-glass-dim (see the glass component).
205
+ glass: {
206
+ tint: {
207
+ DEFAULT: themeColor('glass-tint'),
208
+ foreground: themeColor('glass-tint-foreground'),
209
+ },
210
+ foreground: themeColor('glass-foreground'),
211
+ 'muted-foreground': themeColor('glass-muted-foreground'),
212
+ 'clear-foreground': themeColor('glass-clear-foreground'),
213
+ dim: 'var(--glass-dim)',
214
+ },
215
+ } as unknown as Record<string, string>,
152
216
  borderRadius: {
153
217
  lg: 'var(--radius)',
154
218
  md: 'calc(var(--radius) - 2px)',