@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
@@ -1,7 +1,9 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { createFixture, DEFAULT_CONFIG } from '../test-utils';
4
+ import { stripVTControlCharacters } from 'node:util';
5
+ import { hashContent } from '../lockfile';
6
+ import { createFixture, DEFAULT_CONFIG, installPackage, onTailwind, } from '../test-utils';
5
7
  vi.mock('@clack/prompts', () => ({
6
8
  intro: vi.fn(),
7
9
  outro: vi.fn(),
@@ -22,6 +24,8 @@ vi.mock('../utils/resolve-paths.js', () => ({
22
24
  return ['card.ts', 'index.ts'];
23
25
  return [`${name}.ts`];
24
26
  },
27
+ // dolan's own theme, which the fixture's `themeFiles` write.
28
+ getUiSourceDir: () => fixture.uiDir,
25
29
  }));
26
30
  beforeEach(() => {
27
31
  vi.clearAllMocks();
@@ -35,13 +39,22 @@ afterEach(() => {
35
39
  });
36
40
  const writePackageJson = (dir, content) => writeFileSync(join(dir, 'package.json'), JSON.stringify(content));
37
41
  // What doctor requires (no router: no component imports it), plus the
38
- // package button imports.
42
+ // package button imports. Not Tailwind: doctor reads the version installed
43
+ // (see installPackage), not the one package.json lists.
39
44
  const FULL_ANGULAR_DEPS = {
40
45
  '@angular/core': '^20.0.0',
41
46
  '@blotch/angular-lynx': '^0.0.1',
42
47
  '@blotch/dolan': '^0.0.1',
43
48
  'class-variance-authority': '^0.7.1',
44
- tailwindcss: '^4.0.0',
49
+ };
50
+ /**
51
+ * Every message logged at a level, without the colors picocolors adds in CI.
52
+ */
53
+ const logged = async (level) => {
54
+ const p = await import('@clack/prompts');
55
+ return vi
56
+ .mocked(p.log[level])
57
+ .mock.calls.map(([message]) => stripVTControlCharacters(String(message)));
45
58
  };
46
59
  /**
47
60
  * A lockfile that tracks the given components, as `dolan add` writes it.
@@ -84,6 +97,7 @@ describe('doctorCommand', () => {
84
97
  components: { card: { 'card.ts': '', 'index.ts': '' } },
85
98
  });
86
99
  writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
100
+ installPackage(fixture.dir, 'tailwindcss', '3.4.19');
87
101
  writeFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'tailwind-plugin.ts'), '');
88
102
  writeFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'default.css'), '');
89
103
  writeFileSync(join(fixture.dir, 'tailwind.config.ts'), "import { blotchPlugin } from './src/styles/tailwind-plugin';\nexport default { plugins: [blotchPlugin] };\n");
@@ -92,6 +106,8 @@ describe('doctorCommand', () => {
92
106
  await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
93
107
  const p = await import('@clack/prompts');
94
108
  expect(p.log.error).not.toHaveBeenCalled();
109
+ expect(p.log.warn).not.toHaveBeenCalled();
110
+ expect(await logged('success')).toContain('tailwindcss 3.4.19 found, set up for Tailwind 3 (tailwind.config.ts)');
95
111
  expect(p.log.success).toHaveBeenCalledWith(expect.stringContaining('component(s) healthy'));
96
112
  });
97
113
  it('fails when the components or theme directories are missing', async () => {
@@ -115,6 +131,7 @@ describe('doctorCommand', () => {
115
131
  it('warns when tailwind-plugin.ts and theme CSS are both missing', async () => {
116
132
  fixture = createFixture({ config: DEFAULT_CONFIG });
117
133
  writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
134
+ installPackage(fixture.dir, 'tailwindcss', '3.4.19');
118
135
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
119
136
  const { doctorCommand } = await import('./doctor');
120
137
  await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
@@ -132,18 +149,50 @@ describe('doctorCommand', () => {
132
149
  const p = await import('@clack/prompts');
133
150
  expect(p.log.success).toHaveBeenCalledWith('Theme CSS found');
134
151
  });
135
- it('warns when no tailwind config is found', async () => {
152
+ it('warns when no tailwind config is found on Tailwind 3', async () => {
153
+ fixture = createFixture({ config: DEFAULT_CONFIG });
154
+ writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
155
+ installPackage(fixture.dir, 'tailwindcss', '3.4.19');
156
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
157
+ const { doctorCommand } = await import('./doctor');
158
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
159
+ // The build runs Tailwind 3 only from a config.
160
+ expect(await logged('warn')).toContain('tailwindcss 3.4.19 found, but no Tailwind config: the build runs Tailwind 3 only from a tailwind.config.*. Add one with ng generate @blotch/angular-lynx:add-tailwind --tailwind-version=3');
161
+ expect(await logged('success')).not.toContainEqual(expect.stringContaining('set up for'));
162
+ });
163
+ it("passes when a Tailwind 4 stylesheet imports the theme's tailwind.css", async () => {
164
+ fixture = createFixture({ config: DEFAULT_CONFIG });
165
+ writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
166
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
167
+ writeFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'tailwind.css'), '');
168
+ writeFileSync(join(fixture.dir, 'src/styles.css'), "@import 'tailwindcss/utilities.css';\n@import './styles/tailwind.css';\n");
169
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
170
+ const { doctorCommand } = await import('./doctor');
171
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
172
+ const p = await import('@clack/prompts');
173
+ expect(await logged('success')).toContain('tailwindcss 4.3.3 found, set up for Tailwind 4');
174
+ expect(p.log.success).toHaveBeenCalledWith(expect.stringContaining('imported in'));
175
+ expect(p.log.success).toHaveBeenCalledWith(expect.stringContaining('found in theme'));
176
+ });
177
+ it("warns when a Tailwind 4 stylesheet, or its absence, doesn't import tailwind.css", async () => {
136
178
  fixture = createFixture({ config: DEFAULT_CONFIG });
137
179
  writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
180
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
138
181
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
139
182
  const { doctorCommand } = await import('./doctor');
140
183
  await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
184
+ // Tailwind's own imports don't count.
185
+ writeFileSync(join(fixture.dir, 'src/styles.css'), "@import 'tailwindcss/utilities.css';");
186
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
141
187
  const p = await import('@clack/prompts');
142
- expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('No Tailwind config found'));
188
+ expect(vi
189
+ .mocked(p.log.warn)
190
+ .mock.calls.filter(([message]) => message.includes('not imported in'))).toHaveLength(2);
143
191
  });
144
192
  it('warns when a tailwind config exists but does not reference blotchPlugin', async () => {
145
193
  fixture = createFixture({ config: DEFAULT_CONFIG });
146
194
  writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
195
+ installPackage(fixture.dir, 'tailwindcss', '3.4.19');
147
196
  writeFileSync(join(fixture.dir, 'tailwind.config.js'), 'module.exports = { plugins: [] };');
148
197
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
149
198
  const { doctorCommand } = await import('./doctor');
@@ -177,7 +226,7 @@ describe('doctorCommand', () => {
177
226
  await expect(doctorCommand()).rejects.toThrow('process.exit(1)');
178
227
  const p = await import('@clack/prompts');
179
228
  expect(p.log.error).toHaveBeenCalledWith(expect.stringMatching(/Missing packages: .*@angular\/core.*@blotch\/angular-lynx.*@blotch\/dolan/));
180
- expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('tailwindcss'));
229
+ expect(await logged('warn')).toContain('tailwindcss not found — required if using dolan UI components. Install it with npm install tailwindcss, or set it up with ng generate @blotch/angular-lynx:add-tailwind');
181
230
  });
182
231
  it('reads Angular deps from devDependencies too', async () => {
183
232
  fixture = createFixture({ config: DEFAULT_CONFIG });
@@ -267,15 +316,16 @@ describe('doctorCommand', () => {
267
316
  expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('dolan add spinner'));
268
317
  });
269
318
  it('does not warn about a dependency that is also installed', async () => {
270
- // button depends on spinner, and spinner on icon (registry.ts) —
271
- // installing all three means the `!installed.includes(dep)` check in
272
- // checkComponentHealth is false for every dependency, so no "requires ...
273
- // run dolan add" warning fires.
319
+ // button depends on spinner, glass and icon, and spinner and glass on
320
+ // icon (registry.ts) — installing all four means the
321
+ // `!installed.includes(dep)` check in checkComponentHealth is false for
322
+ // every dependency, so no "requires ... run dolan add" warning fires.
274
323
  fixture = createFixture({
275
324
  config: DEFAULT_CONFIG,
276
325
  components: {
277
326
  button: { 'button.ts': '' },
278
327
  spinner: { 'spinner.ts': '' },
328
+ glass: { 'glass.ts': '' },
279
329
  icon: { 'icon.ts': '' },
280
330
  },
281
331
  });
@@ -382,4 +432,368 @@ describe('doctorCommand', () => {
382
432
  expect(p.log.warn).not.toHaveBeenCalledWith(expect.stringContaining('is missing files'));
383
433
  });
384
434
  });
435
+ describe('doctorCommand Tailwind', () => {
436
+ /**
437
+ * A project in `dir` with nothing wrong but its Tailwind, which the test
438
+ * installs. Runs doctor.
439
+ */
440
+ const runDoctor = async (dir, files = {}) => {
441
+ writePackageJson(dir, { dependencies: FULL_ANGULAR_DEPS });
442
+ for (const [file, content] of Object.entries(files)) {
443
+ writeFileSync(join(dir, file), content);
444
+ }
445
+ vi.spyOn(process, 'cwd').mockReturnValue(dir);
446
+ const { doctorCommand } = await import('./doctor');
447
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
448
+ };
449
+ /** The theme's tailwind.css, imported as dolan init shows. */
450
+ const IMPORTED = { 'src/styles.css': "@import './styles/tailwind.css';" };
451
+ it('finds the Tailwind a workspace root installs', async () => {
452
+ // The app's package.json lists no Tailwind: the root's is every package's.
453
+ fixture = createFixture({});
454
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
455
+ const app = join(fixture.dir, 'apps', 'app');
456
+ for (const dir of Object.values(DEFAULT_CONFIG.aliases)) {
457
+ mkdirSync(join(app, dir), { recursive: true });
458
+ }
459
+ writeFileSync(join(app, 'dolan.config.json'), JSON.stringify(DEFAULT_CONFIG));
460
+ await runDoctor(app, IMPORTED);
461
+ expect(await logged('success')).toContain('tailwindcss 4.3.3 found, set up for Tailwind 4');
462
+ expect(await logged('warn')).not.toContainEqual(expect.stringContaining('tailwindcss'));
463
+ });
464
+ it('finds the Tailwind @tailwindcss/postcss depends on, hoisted next to it', async () => {
465
+ // npm's layout, for an app that lists only the plugin.
466
+ fixture = createFixture({ config: DEFAULT_CONFIG });
467
+ installPackage(fixture.dir, '@tailwindcss/postcss', '4.3.3');
468
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
469
+ await runDoctor(fixture.dir, IMPORTED);
470
+ expect(await logged('success')).toContain('tailwindcss 4.3.3 found, set up for Tailwind 4');
471
+ });
472
+ it("finds the Tailwind @tailwindcss/postcss depends on in the plugin's own folder", async () => {
473
+ // pnpm's layout, out of the app's reach. The build still finds it: pnpm
474
+ // runs `ng` with the packages it hoists on NODE_PATH.
475
+ fixture = createFixture({ config: DEFAULT_CONFIG });
476
+ installPackage(installPackage(fixture.dir, '@tailwindcss/postcss', '4.3.3'), 'tailwindcss', '4.3.3');
477
+ await runDoctor(fixture.dir, IMPORTED);
478
+ expect(await logged('success')).toEqual(expect.arrayContaining([
479
+ 'tailwindcss 4.3.3 found, set up for Tailwind 4',
480
+ 'tailwind.css imported in src/styles.css',
481
+ ]));
482
+ });
483
+ it('warns about a Tailwind config with Tailwind 4 installed, as the build does', async () => {
484
+ fixture = createFixture({ config: DEFAULT_CONFIG });
485
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
486
+ await runDoctor(fixture.dir, {
487
+ 'tailwind.config.js': 'module.exports = { plugins: [blotchPlugin] };',
488
+ });
489
+ expect(await logged('warn')).toContain("Found tailwind.config.js, but Tailwind CSS 4.3.3 is installed, which doesn't run from a Tailwind config file. While it's there, dolan sets the theme up for Tailwind 3. Move its settings into your stylesheet and delete it, then run dolan update for the theme's tailwind.css.");
490
+ // Tailwind 4 doesn't run the config, so its plugin isn't a pass.
491
+ expect(await logged('success')).not.toContainEqual(expect.stringMatching(/set up for|blotchPlugin/));
492
+ });
493
+ it("names the package manager's command when Tailwind isn't installed", async () => {
494
+ fixture = createFixture({ config: DEFAULT_CONFIG });
495
+ writeFileSync(join(fixture.dir, 'yarn.lock'), '');
496
+ await runDoctor(fixture.dir);
497
+ expect(await logged('warn')).toContainEqual(expect.stringContaining('tailwindcss not found — required if using dolan UI components. Install it with yarn add tailwindcss'));
498
+ // Without a version, there's no setup to check.
499
+ expect(await logged('warn')).not.toContainEqual(expect.stringMatching(/Tailwind config|tailwind\.css not imported/));
500
+ });
501
+ });
502
+ describe('doctorCommand theme channels', () => {
503
+ /**
504
+ * A plugin that maps primary and border with themeColor(), as dolan's does.
505
+ */
506
+ const PLUGIN = `colors: {
507
+ primary: { DEFAULT: themeColor('primary') },
508
+ border: themeColor("border"),
509
+ destructive: { subtle: 'var(--destructive-subtle)' },
510
+ }`;
511
+ /**
512
+ * Writes the theme's plugin and stylesheets, then runs doctor.
513
+ */
514
+ const runDoctor = async (plugin, css) => {
515
+ fixture = createFixture({ config: DEFAULT_CONFIG });
516
+ writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
517
+ const themeDir = join(fixture.dir, DEFAULT_CONFIG.aliases.theme);
518
+ writeFileSync(join(themeDir, 'tailwind-plugin.ts'), plugin);
519
+ for (const [file, content] of Object.entries(css)) {
520
+ writeFileSync(join(themeDir, file), content);
521
+ }
522
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
523
+ const { doctorCommand } = await import('./doctor');
524
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
525
+ return import('@clack/prompts');
526
+ };
527
+ it('passes when every mapped color has matching channels', async () => {
528
+ const p = await runDoctor(PLUGIN, {
529
+ 'default.css': `/* --primary: red; */
530
+ page {
531
+ --primary: rgba(24, 24, 27, 1);
532
+ --primary-rgb: 24, 24, 27;
533
+ --border: #e4e4e7;
534
+ --border-rgb: 228,228,231;
535
+ }`,
536
+ // A theme that sets only some colors is checked for those alone.
537
+ 'ocean.css': 'page { --primary: rgb(37 99 235); --primary-rgb: 37,99, 235; }',
538
+ });
539
+ expect(p.log.success).toHaveBeenCalledWith('Theme colors have the channels opacity modifiers need');
540
+ expect(p.log.warn).not.toHaveBeenCalledWith(expect.stringContaining('channels'));
541
+ });
542
+ it('warns about colors without channels, with the line to add', async () => {
543
+ // A theme copied before the -rgb variables, kept by dolan update.
544
+ const p = await runDoctor(PLUGIN, {
545
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); --border: hsl(240, 6%, 90%); }',
546
+ });
547
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringMatching(/default\.css.*--primary.*--border.*bg-primary\/50.*--primary-rgb: 24, 24, 27;/));
548
+ });
549
+ it("suggests placeholder channels when it can't read the color", async () => {
550
+ const p = await runDoctor(PLUGIN, {
551
+ 'dark.css': 'page.dark { --border: hsl(240, 4%, 16%); }',
552
+ });
553
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('--border-rgb: r, g, b;'));
554
+ });
555
+ it("warns about channels that don't match their color", async () => {
556
+ // The brand color changed, its channels didn't.
557
+ const p = await runDoctor(PLUGIN, {
558
+ 'default.css': `page {
559
+ --primary: #2563eb;
560
+ --primary-rgb: 24, 24, 27;
561
+ --border: #fff;
562
+ --border-rgb: 0, 0, 0;
563
+ }`,
564
+ });
565
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringMatching(/default\.css.*--primary-rgb.*--border-rgb.*don't/));
566
+ });
567
+ it('warns about space-separated channels, which rgba() reads as one argument', async () => {
568
+ const p = await runDoctor(PLUGIN, {
569
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); --primary-rgb: 24 24 27; }',
570
+ });
571
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringMatching(/--primary-rgb.* doesn't hold .*comma-separated/));
572
+ expect(p.log.success).not.toHaveBeenCalledWith(expect.stringContaining('channels'));
573
+ });
574
+ it('says "doesn\'t" for one differing color, and skips colors it can\'t read', async () => {
575
+ const p = await runDoctor(PLUGIN, {
576
+ 'default.css': `page {
577
+ --primary: rgba(37, 99, 235, 1);
578
+ --primary-rgb: 24, 24, 27;
579
+ --border: hsl(240, 6%, 90%);
580
+ --border-rgb: 1, 2, 3;
581
+ }`,
582
+ });
583
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringMatching(/--primary-rgb.* doesn't hold/));
584
+ expect(p.log.warn).not.toHaveBeenCalledWith(expect.stringContaining('--border-rgb'));
585
+ });
586
+ it("doesn't check a plugin that maps no themeColor()", async () => {
587
+ // An older plugin: its modifiers never worked, so nothing needs channels.
588
+ const p = await runDoctor("colors: { primary: 'var(--primary)' }", {
589
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); }',
590
+ });
591
+ expect(p.log.warn).not.toHaveBeenCalledWith(expect.stringContaining('channels'));
592
+ expect(p.log.success).not.toHaveBeenCalledWith(expect.stringContaining('channels'));
593
+ });
594
+ describe('on Tailwind 4', () => {
595
+ /**
596
+ * Writes the theme's tailwind.css and stylesheets, then runs doctor.
597
+ */
598
+ const runDoctor4 = async (css) => {
599
+ fixture = createFixture({ config: DEFAULT_CONFIG });
600
+ writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
601
+ installPackage(fixture.dir, 'tailwindcss', '4.3.3');
602
+ const themeDir = join(fixture.dir, DEFAULT_CONFIG.aliases.theme);
603
+ for (const [file, content] of Object.entries(css)) {
604
+ writeFileSync(join(themeDir, file), content);
605
+ }
606
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
607
+ const { doctorCommand } = await import('./doctor');
608
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
609
+ return import('@clack/prompts');
610
+ };
611
+ /**
612
+ * The colors tailwind.css maps, as the theme's does.
613
+ */
614
+ const TAILWIND_CSS = `@theme inline {
615
+ --color-primary: var(--primary);
616
+ --color-destructive-subtle: var(--destructive-subtle);
617
+ }`;
618
+ it('checks the colors tailwind.css maps, skipping translucent ones', async () => {
619
+ const p = await runDoctor4({
620
+ 'tailwind.css': TAILWIND_CSS,
621
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); --primary-rgb: 24, 24, 27; --destructive-subtle: rgba(239, 68, 68, 0.1); }',
622
+ });
623
+ expect(p.log.success).toHaveBeenCalledWith('Theme colors have the channels opacity modifiers need');
624
+ });
625
+ it('warns about a color tailwind.css maps without channels', async () => {
626
+ const p = await runDoctor4({
627
+ 'tailwind.css': TAILWIND_CSS,
628
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); }',
629
+ });
630
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringMatching(/default\.css.*--primary-rgb: 24, 24, 27;/));
631
+ });
632
+ it("doesn't check a theme without tailwind.css or the plugin", async () => {
633
+ const p = await runDoctor4({
634
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); }',
635
+ });
636
+ expect(p.log.success).not.toHaveBeenCalledWith(expect.stringContaining('channels'));
637
+ });
638
+ });
639
+ });
640
+ describe('doctorCommand theme colors components read', () => {
641
+ /** Every glass color, as dolan's default.css sets them. */
642
+ const GLASS_CSS = `page {
643
+ --glass-tint: rgba(24, 24, 27, 1);
644
+ --glass-tint-rgb: 24, 24, 27;
645
+ --glass-tint-foreground: rgba(250, 250, 250, 1);
646
+ --glass-tint-foreground-rgb: 250, 250, 250;
647
+ --glass-foreground: rgba(9, 9, 11, 1);
648
+ --glass-foreground-rgb: 9, 9, 11;
649
+ --glass-muted-foreground: rgba(63, 63, 70, 1);
650
+ --glass-muted-foreground-rgb: 63, 63, 70;
651
+ --glass-clear-foreground: rgba(255, 255, 255, 1);
652
+ --glass-clear-foreground-rgb: 255, 255, 255;
653
+ --glass-dim: rgba(0, 0, 0, 0.35);
654
+ }`;
655
+ /** The glass colors mapped as dolan's tailwind.css maps them. */
656
+ const GLASS_TAILWIND_CSS = `@theme inline {
657
+ --color-glass-tint: var(--glass-tint);
658
+ --color-glass-tint-foreground: var(--glass-tint-foreground);
659
+ --color-glass-foreground: var(--glass-foreground);
660
+ --color-glass-muted-foreground: var(--glass-muted-foreground);
661
+ --color-glass-clear-foreground: var(--glass-clear-foreground);
662
+ --color-glass-dim: var(--glass-dim);
663
+ }`;
664
+ /**
665
+ * A Tailwind 4 project with glass installed and the given theme files,
666
+ * and dolan's own theme if `bundled` is given. Runs doctor.
667
+ */
668
+ const runDoctor = async (theme, bundled, root = {}) => {
669
+ fixture = createFixture({
670
+ config: DEFAULT_CONFIG,
671
+ lockfile: tracking('glass', 'icon'),
672
+ components: {
673
+ glass: { 'glass.ts': '' },
674
+ icon: { 'icon.ts': '' },
675
+ },
676
+ themeFiles: bundled,
677
+ });
678
+ writePackageJson(fixture.dir, { dependencies: FULL_ANGULAR_DEPS });
679
+ // A Tailwind config is a Tailwind 3 setup.
680
+ installPackage(fixture.dir, 'tailwindcss', root['tailwind.config.ts'] ? '3.4.19' : '4.3.3');
681
+ const themeDir = join(fixture.dir, DEFAULT_CONFIG.aliases.theme);
682
+ for (const [file, content] of Object.entries(theme)) {
683
+ writeFileSync(join(themeDir, file), content);
684
+ }
685
+ for (const [file, content] of Object.entries(root)) {
686
+ writeFileSync(join(fixture.dir, file), content);
687
+ }
688
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
689
+ const { doctorCommand } = await import('./doctor');
690
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
691
+ return import('@clack/prompts');
692
+ };
693
+ it('passes when the theme has and maps every color the components read', async () => {
694
+ const p = await runDoctor({
695
+ 'default.css': GLASS_CSS,
696
+ 'tailwind.css': GLASS_TAILWIND_CSS,
697
+ });
698
+ expect(p.log.success).toHaveBeenCalledWith('Theme has the colors your components read');
699
+ });
700
+ it("lists the colors a theme lacks, with dolan's lines for them", async () => {
701
+ // A theme copied before glass, and edited, so dolan update kept it.
702
+ const p = await runDoctor({
703
+ 'default.css': 'page { --primary: rgba(24, 24, 27, 1); }',
704
+ 'dark.css': GLASS_CSS.replace('page', 'page.dark'),
705
+ }, { 'default.css': GLASS_CSS });
706
+ expect(await logged('warn')).toContainEqual(expect.stringMatching(/default\.css lacks theme colors that glass read: --glass-tint, .*--glass-dim\..*dolan update.*\n--glass-tint: rgba\(24, 24, 27, 1\);\n--glass-tint-rgb: 24, 24, 27;\n/s));
707
+ expect(await logged('warn')).not.toContainEqual(expect.stringContaining('dark.css lacks'));
708
+ expect(p.log.success).not.toHaveBeenCalledWith('Theme has the colors your components read');
709
+ });
710
+ it("points at dolan update alone without dolan's theme to quote", async () => {
711
+ await runDoctor({ 'default.css': 'page {}' });
712
+ expect(await logged('warn')).toContainEqual(expect.stringMatching(/default\.css lacks .*haven't edited\.$/));
713
+ });
714
+ it("lists the colors tailwind.css doesn't map", async () => {
715
+ await runDoctor({
716
+ 'default.css': GLASS_CSS,
717
+ 'tailwind.css': GLASS_TAILWIND_CSS.replace(' --color-glass-dim: var(--glass-dim);\n', ''),
718
+ });
719
+ expect(await logged('warn')).toContainEqual(expect.stringMatching(/tailwind\.css doesn't map theme colors that glass read to classes: glass-dim\./));
720
+ });
721
+ it("accepts the Tailwind 3 plugin's themeColor() and var() mappings", async () => {
722
+ const p = await runDoctor({
723
+ 'default.css': GLASS_CSS,
724
+ 'tailwind-plugin.ts': `glass: {
725
+ tint: { DEFAULT: themeColor('glass-tint'), foreground: themeColor("glass-tint-foreground") },
726
+ foreground: themeColor('glass-foreground'),
727
+ 'muted-foreground': themeColor('glass-muted-foreground'),
728
+ 'clear-foreground': themeColor('glass-clear-foreground'),
729
+ dim: 'var(--glass-dim)',
730
+ }`,
731
+ }, undefined, { 'tailwind.config.ts': 'export default { plugins: [blotchPlugin] };' });
732
+ expect(p.log.success).toHaveBeenCalledWith('Theme has the colors your components read');
733
+ });
734
+ });
735
+ // A move between Tailwind versions leaves the other version's theme files,
736
+ // which nothing uses: the plugin is dead code the app's coverage counts.
737
+ describe('doctorCommand — theme files for the Tailwind version the app left', () => {
738
+ /**
739
+ * Runs doctor in an app on `tailwind` whose theme folder has `files`, which
740
+ * the lockfile records unless `tracked` says otherwise, and returns its
741
+ * warnings about them, colors stripped.
742
+ */
743
+ const leftoverWarnings = async (tailwind, files, tracked = files) => {
744
+ fixture = createFixture({
745
+ config: DEFAULT_CONFIG,
746
+ lockfile: {
747
+ version: 1,
748
+ components: {},
749
+ theme: Object.fromEntries(tracked.map((file) => [file, { hash: hashContent('') }])),
750
+ },
751
+ });
752
+ const dependencies = FULL_ANGULAR_DEPS;
753
+ if (tailwind === null)
754
+ writePackageJson(fixture.dir, { dependencies });
755
+ else
756
+ onTailwind(fixture.dir, tailwind, { dependencies });
757
+ for (const file of files) {
758
+ writeFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, file), '');
759
+ }
760
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
761
+ const { doctorCommand } = await import('./doctor');
762
+ await expect(doctorCommand()).rejects.toThrow('process.exit(0)');
763
+ const p = await import('@clack/prompts');
764
+ return vi
765
+ .mocked(p.log.warn)
766
+ .mock.calls.map(([message]) => stripVTControlCharacters(message))
767
+ .filter((message) => message.startsWith('Found '));
768
+ };
769
+ it('warns about the Tailwind 3 plugin and its test in a Tailwind 4 app, for dolan update to delete', async () => {
770
+ expect(await leftoverWarnings(4, [
771
+ 'tailwind.css',
772
+ 'tailwind-plugin.ts',
773
+ 'tailwind-plugin.test.ts',
774
+ ])).toEqual([
775
+ "Found tailwind-plugin.ts, tailwind-plugin.test.ts in src/styles, but the app uses Tailwind 4, which takes the theme's classes from tailwind.css. Run dolan update to delete them, or delete them.",
776
+ ]);
777
+ });
778
+ it('warns about tailwind.css in a Tailwind 3 app', async () => {
779
+ expect(await leftoverWarnings(3, ['tailwind.css', 'tailwind-plugin.ts'])).toEqual([
780
+ "Found tailwind.css in src/styles, but the app uses Tailwind 3, which takes the theme's classes from tailwind-plugin.ts. Run dolan update to delete it, or delete it.",
781
+ ]);
782
+ });
783
+ it("doesn't send the app to dolan update for a plugin it doesn't delete", async () => {
784
+ // Kept when `update` asked, so the app's own now.
785
+ expect(await leftoverWarnings(4, ['tailwind.css', 'tailwind-plugin.ts'], [])).toEqual([
786
+ "Found tailwind-plugin.ts in src/styles, but the app uses Tailwind 4, which takes the theme's classes from tailwind.css. Delete it, unless your own code uses it.",
787
+ ]);
788
+ });
789
+ it.each([
790
+ [4, ['tailwind.css']],
791
+ [3, ['tailwind-plugin.ts', 'tailwind-plugin.test.ts']],
792
+ ])('says nothing when a Tailwind %s theme has only the files the app takes', async (tailwind, files) => {
793
+ expect(await leftoverWarnings(tailwind, [...files])).toEqual([]);
794
+ });
795
+ it('says nothing without Tailwind, which has no version to have left', async () => {
796
+ expect(await leftoverWarnings(null, ['tailwind.css'])).toEqual([]);
797
+ });
798
+ });
385
799
  //# sourceMappingURL=doctor.test.js.map