@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,3 +1,9 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
1
7
  import { createChartRuntime } from '@tanstack/charts/runtime';
2
8
  import { createChartTooltipContent } from '@tanstack/charts/tooltip/model';
3
9
  import type {
@@ -6,7 +12,11 @@ import type {
6
12
  SceneNode,
7
13
  } from '@tanstack/charts/types';
8
14
  import { describe, expect, it } from 'vitest';
9
- import { candlestickChartDefinition } from './candlestick-chart';
15
+ import {
16
+ type CandlestickDatum,
17
+ UiCandlestickChart,
18
+ candlestickChartDefinition,
19
+ } from './candlestick-chart';
10
20
 
11
21
  const data = [
12
22
  { x: 0, open: 100, high: 104, low: 98, close: 102 },
@@ -116,3 +126,74 @@ describe('candlestickChartDefinition', () => {
116
126
  expect(render2(definition).found.length).toBeGreaterThan(0);
117
127
  });
118
128
  });
129
+
130
+ @Component({
131
+ imports: [UiCandlestickChart],
132
+ template: `
133
+ <ui-candlestick-chart
134
+ [data]="data"
135
+ [width]="300"
136
+ [upColor]="upColor()"
137
+ downColor="red"
138
+ (candleTap)="tapped.push($event)"
139
+ />
140
+ `,
141
+ })
142
+ class CandlestickChartHost {
143
+ readonly data = data;
144
+ readonly upColor = signal('green');
145
+ /** Every `candleTap`, in order. */
146
+ readonly tapped: CandlestickDatum[] = [];
147
+ }
148
+
149
+ /**
150
+ * The SVG the chart's marks layer draws.
151
+ */
152
+ const marksSvg = (container: Element) =>
153
+ container.querySelectorAll('svg')[1]!.getAttribute('content')!;
154
+
155
+ /**
156
+ * The middle of each candle body the chart drew.
157
+ */
158
+ const bodiesDrawn = (container: Element) =>
159
+ [
160
+ ...marksSvg(container).matchAll(
161
+ /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.-]+)" height="([\d.-]+)"[^>]*fill=/g,
162
+ ),
163
+ ].map(([, x, y, width, height]) => ({
164
+ x: Number(x) + Number(width) / 2,
165
+ y: Number(y) + Number(height) / 2,
166
+ }));
167
+
168
+ /**
169
+ * Touches a spot of the chart and lifts the finger right there: a tap.
170
+ */
171
+ const tap = async (container: Element, at: { x: number; y: number }) => {
172
+ const root = container.querySelector('[accessibility-label]')!;
173
+ fireEvent.touchstart(root, { touches: [at] });
174
+ fireEvent.touchend(root, { touches: [at] });
175
+ await waitForUpdate();
176
+ };
177
+
178
+ describe('UiCandlestickChart', () => {
179
+ it('colors each candle by direction with its inputs', async () => {
180
+ const { componentRef, container } = await render(CandlestickChartHost);
181
+ expect(bodiesDrawn(container)).toHaveLength(2);
182
+ expect(marksSvg(container)).toContain('fill="green"');
183
+ expect(marksSvg(container)).toContain('fill="red"');
184
+
185
+ (componentRef.instance as CandlestickChartHost).upColor.set('blue');
186
+ await waitForUpdate();
187
+ expect(marksSvg(container)).toContain('fill="blue"');
188
+ });
189
+
190
+ it("emits a tapped candle's datum, and nothing for a tap off the candles", async () => {
191
+ const { componentRef, container } = await render(CandlestickChartHost);
192
+ const host = componentRef.instance as CandlestickChartHost;
193
+ await tap(container, bodiesDrawn(container)[0]!);
194
+ expect(host.tapped).toEqual([data[0]]);
195
+
196
+ await tap(container, { x: -100, y: -100 });
197
+ expect(host.tapped).toEqual([data[0]]);
198
+ });
199
+ });
@@ -1,8 +1,11 @@
1
- import { Component } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
3
7
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
8
  import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
6
9
  import {
7
10
  UiCard,
8
11
  UiCardContent,
@@ -13,7 +16,6 @@ import {
13
16
  } from './card';
14
17
 
15
18
  @Component({
16
- standalone: true,
17
19
  imports: [
18
20
  UiCard,
19
21
  UiCardHeader,
@@ -24,20 +26,53 @@ import {
24
26
  LYNX_ELEMENTS,
25
27
  ],
26
28
  template: `
27
- <ui-card>
28
- <ui-card-header>
29
- <ui-card-title>Title</ui-card-title>
30
- <ui-card-description>Desc</ui-card-description>
29
+ <ui-card [pressable]="pressable()" class="w-full">
30
+ <ui-card-header class="gap-1">
31
+ <ui-card-title class="text-lg">Title</ui-card-title>
32
+ <ui-card-description class="italic">Desc</ui-card-description>
31
33
  </ui-card-header>
32
- <ui-card-content><text>Body</text></ui-card-content>
33
- <ui-card-footer><text>Actions</text></ui-card-footer>
34
+ <ui-card-content class="pt-2"><text>Body</text></ui-card-content>
35
+ <ui-card-footer class="justify-end"><text>Actions</text></ui-card-footer>
34
36
  </ui-card>
35
37
  `,
36
38
  })
37
- class CardHost {}
39
+ class CardHost {
40
+ readonly pressable = signal(false);
41
+ }
42
+
43
+ @Component({
44
+ imports: [UiCard, LYNX_ELEMENTS],
45
+ template: `<ui-card pressable><text>Order</text></ui-card>`,
46
+ })
47
+ class BarePressableHost {}
48
+
49
+ /**
50
+ * The card's own view. Each component's host element renders as a view too,
51
+ * so it's found by the card's class rather than by being the first.
52
+ */
53
+ const cardView = (container: Element) => container.querySelector('.bg-card')!;
54
+
55
+ /**
56
+ * Fires a Lynx event `fireEvent` has no shortcut for, as Lynx dispatches it:
57
+ * `mouseleave` is the mouse's `touchcancel`.
58
+ */
59
+ const fireLynxEvent = (element: Element, name: string) =>
60
+ fireEvent(
61
+ element,
62
+ Object.assign(new Event(`bindEvent:${name}`), {
63
+ eventType: 'bindEvent',
64
+ eventName: name,
65
+ }),
66
+ );
67
+
68
+ /**
69
+ * The scale the card's inline style holds, if any.
70
+ */
71
+ const scaleOf = (view: Element) =>
72
+ (view as HTMLElement).style.getPropertyValue('transform');
38
73
 
39
74
  describe('UiCard', () => {
40
- it('renders each sub-component with projected content', async () => {
75
+ it('renders each part with its projected content', async () => {
41
76
  const { getByText } = await render(CardHost);
42
77
  expect(getByText('Title')).toBeTruthy();
43
78
  expect(getByText('Desc')).toBeTruthy();
@@ -45,66 +80,67 @@ describe('UiCard', () => {
45
80
  expect(getByText('Actions')).toBeTruthy();
46
81
  });
47
82
 
48
- it('renders without a host wrapper', async () => {
49
- const { container } = await render(UiCard);
50
- expect(container).toBeTruthy();
83
+ it("merges each part's class with its own", async () => {
84
+ const { container, getByText } = await render(CardHost);
85
+ expect(cardView(container).getAttribute('class')).toContain('bg-card');
86
+ expect(cardView(container).getAttribute('class')).toContain('w-full');
87
+ expect(getByText('Title').getAttribute('class')).toContain('text-lg');
88
+ expect(getByText('Desc').getAttribute('class')).toContain('italic');
51
89
  });
52
90
 
53
- it('press handlers are no-ops when pressable is false', async () => {
54
- const { componentRef } = await render(UiCard);
55
- const inst = componentRef.instance as unknown as {
56
- onPressStart: () => void;
57
- onPressEnd: () => void;
58
- onPressCancel: () => void;
59
- };
60
- expect(() => inst.onPressStart()).not.toThrow();
61
- expect(() => inst.onPressEnd()).not.toThrow();
62
- expect(() => inst.onPressCancel()).not.toThrow();
91
+ it("doesn't respond to presses unless it's pressable", async () => {
92
+ const { container } = await render(CardHost);
93
+ const view = cardView(container);
94
+ fireEvent.touchstart(view);
95
+ fireEvent.mousedown(view);
96
+ expect(scaleOf(view)).toBe('');
97
+ fireEvent.touchend(view);
98
+ fireEvent.touchcancel(view);
99
+ fireEvent.mouseup(view);
100
+ fireLynxEvent(view, 'mouseleave');
101
+ expect(scaleOf(view)).toBe('');
63
102
  });
64
103
 
65
- it('press handlers fire animations when pressable is true', async () => {
66
- const { componentRef } = await render(UiCard);
67
- const inst = componentRef.instance as UiCard;
68
- setInputSignal(inst.pressable, true);
104
+ it('scales down while pressed by touch, and back on release', async () => {
105
+ const { container, componentRef } = await render(CardHost);
106
+ (componentRef.instance as CardHost).pressable.set(true);
69
107
  await waitForUpdate();
70
- const cast = inst as unknown as {
71
- onPressStart: () => void;
72
- onPressEnd: () => void;
73
- onPressCancel: () => void;
74
- };
75
- // Run twice so `#pressAnim?.cancel()` gets exercised on the second call.
76
- cast.onPressStart();
77
- cast.onPressStart();
78
- cast.onPressEnd();
79
- cast.onPressCancel();
80
- // No assertion — coverage-only paths. Confirm no throw.
81
- expect(true).toBe(true);
82
- });
83
- });
108
+ const view = cardView(container);
84
109
 
85
- describe('Card sub-components', () => {
86
- it('UiCardHeader renders projected content', async () => {
87
- const { container } = await render(UiCardHeader);
88
- expect(container.querySelector('view')).toBeTruthy();
89
- });
110
+ fireEvent.touchstart(view);
111
+ expect(scaleOf(view)).toBe('scale(0.985)');
112
+ fireEvent.touchend(view);
113
+ expect(scaleOf(view)).toBe('scale(1)');
90
114
 
91
- it('UiCardTitle renders projected content', async () => {
92
- const { container } = await render(UiCardTitle);
93
- expect(container.querySelector('text')).toBeTruthy();
115
+ // A scroll that takes the touch over cancels it: the card scales back.
116
+ fireEvent.touchstart(view);
117
+ fireEvent.touchcancel(view);
118
+ expect(scaleOf(view)).toBe('scale(1)');
94
119
  });
95
120
 
96
- it('UiCardDescription renders projected content', async () => {
97
- const { container } = await render(UiCardDescription);
98
- expect(container.querySelector('text')).toBeTruthy();
99
- });
121
+ it('is pressable with a bare pressable attribute', async () => {
122
+ const { container } = await render(BarePressableHost);
123
+ const view = cardView(container);
100
124
 
101
- it('UiCardContent renders projected content', async () => {
102
- const { container } = await render(UiCardContent);
103
- expect(container.querySelector('view')).toBeTruthy();
125
+ fireEvent.touchstart(view);
126
+ expect(scaleOf(view)).toBe('scale(0.985)');
127
+ fireEvent.touchend(view);
128
+ expect(scaleOf(view)).toBe('scale(1)');
104
129
  });
105
130
 
106
- it('UiCardFooter renders projected content', async () => {
107
- const { container } = await render(UiCardFooter);
108
- expect(container.querySelector('view')).toBeTruthy();
131
+ it('scales down while pressed by mouse, and back on release or leave', async () => {
132
+ const { container, componentRef } = await render(CardHost);
133
+ (componentRef.instance as CardHost).pressable.set(true);
134
+ await waitForUpdate();
135
+ const view = cardView(container);
136
+
137
+ fireEvent.mousedown(view);
138
+ expect(scaleOf(view)).toBe('scale(0.985)');
139
+ fireEvent.mouseup(view);
140
+ expect(scaleOf(view)).toBe('scale(1)');
141
+
142
+ fireEvent.mousedown(view);
143
+ fireLynxEvent(view, 'mouseleave');
144
+ expect(scaleOf(view)).toBe('scale(1)');
109
145
  });
110
146
  });
@@ -2,6 +2,7 @@ import {
2
2
  type ElementRef,
3
3
  Component,
4
4
  ViewEncapsulation,
5
+ booleanAttribute,
5
6
  computed,
6
7
  input,
7
8
  viewChild,
@@ -40,7 +41,7 @@ export class UiCard {
40
41
  /**
41
42
  * When true, the card responds to touch with a subtle scale animation
42
43
  */
43
- readonly pressable = input(false);
44
+ readonly pressable = input(false, { transform: booleanAttribute });
44
45
  readonly userClass = input<string>('', { alias: 'class' });
45
46
 
46
47
  readonly containerRef = viewChild<ElementRef>('container');
@@ -1,5 +1,30 @@
1
+ import {
2
+ Component,
3
+ Injector,
4
+ type Type,
5
+ computed,
6
+ input,
7
+ output,
8
+ signal,
9
+ viewChild,
10
+ } from '@angular/core';
11
+ import {
12
+ fireEvent,
13
+ render,
14
+ waitForUpdate,
15
+ } from '@blotch/angular-lynx-testing-library';
16
+ import { defineChart, dot } from '@tanstack/charts/universal';
1
17
  import { describe, expect, it } from 'vitest';
2
- import { linearAxis, minSpacing, presetAxes } from './chart-presets';
18
+ import { UiChart, tooltip } from './chart';
19
+ import {
20
+ type CartesianPresetOptions,
21
+ PRESET_CHART_BINDINGS,
22
+ UiCartesianPreset,
23
+ type XYDatum,
24
+ linearAxis,
25
+ minSpacing,
26
+ presetAxes,
27
+ } from './chart-presets';
3
28
 
4
29
  /**
5
30
  * The configured domain of an axis config's scale instance.
@@ -137,3 +162,174 @@ describe('presetAxes', () => {
137
162
  expect(domainOf(axes.y)).toEqual([-10, 10]);
138
163
  });
139
164
  });
165
+
166
+ /**
167
+ * The smallest preset: a dot per datum on the shared axes, built the way the
168
+ * library's presets (`<ui-line-chart>`, …) build theirs.
169
+ */
170
+ @Component({
171
+ selector: 'test-dot-chart',
172
+ imports: [UiChart],
173
+ template: `
174
+ <ui-chart
175
+ #chart
176
+ [definition]="definition()"
177
+ ${PRESET_CHART_BINDINGS}
178
+ (pointSelect)="pointTap.emit(datumOf($event))"
179
+ />
180
+ `,
181
+ })
182
+ class DotChart extends UiCartesianPreset<XYDatum> {
183
+ readonly data = input.required<readonly XYDatum[]>();
184
+ /** The tapped datum, or `null` for a tap on empty space. */
185
+ readonly pointTap = output<XYDatum | null>();
186
+
187
+ protected readonly definition = computed(() => {
188
+ const options = this.commonOptions();
189
+ const data = this.data();
190
+ return defineChart({
191
+ marks: [dot(data, { x: 'x', y: 'y' })],
192
+ scales: presetAxes(options, {
193
+ x: data.map((d) => d.x),
194
+ y: data.map((d) => d.y),
195
+ }),
196
+ tooltip: options.tooltip === false ? false : tooltip,
197
+ });
198
+ });
199
+ }
200
+
201
+ @Component({
202
+ imports: [DotChart],
203
+ template: `
204
+ <test-dot-chart
205
+ [data]="data"
206
+ [width]="300"
207
+ [height]="160"
208
+ label="Sales"
209
+ class="mt-2"
210
+ [xAxisLabel]="xAxisLabel()"
211
+ yAxisLabel="Units"
212
+ [zoomable]="true"
213
+ zoomAxes="x"
214
+ (pointTap)="tapped.push($event)"
215
+ />
216
+ `,
217
+ })
218
+ class PresetHost {
219
+ readonly data: readonly XYDatum[] = [
220
+ { x: 0, y: 100 },
221
+ { x: 5, y: 300 },
222
+ { x: 10, y: 500 },
223
+ ];
224
+ readonly xAxisLabel = signal('Day');
225
+ /** Every `pointTap`, in order. */
226
+ readonly tapped: (XYDatum | null)[] = [];
227
+ readonly preset = viewChild.required(DotChart);
228
+ }
229
+
230
+ /**
231
+ * The chart's own view, which takes every touch.
232
+ */
233
+ const chartRoot = (container: Element) =>
234
+ container.querySelector('[accessibility-label]')!;
235
+
236
+ /**
237
+ * Where the chart drew each dot, read from its marks layer.
238
+ */
239
+ const dotsDrawn = (container: Element) =>
240
+ [
241
+ ...container
242
+ .querySelectorAll('svg')[1]!
243
+ .getAttribute('content')!
244
+ .matchAll(/<circle cx="([\d.-]+)" cy="([\d.-]+)"/g),
245
+ ].map(([, x, y]) => ({ x: Number(x), y: Number(y) }));
246
+
247
+ /**
248
+ * Touches a spot and lifts the finger right there: a tap.
249
+ */
250
+ const tap = async (root: Element, at: { x: number; y: number }) => {
251
+ fireEvent.touchstart(root, { touches: [at] });
252
+ fireEvent.touchend(root, { touches: [at] });
253
+ await waitForUpdate();
254
+ };
255
+
256
+ describe('UiCartesianPreset', () => {
257
+ it('starts every preset from the same defaults', () => {
258
+ // Angular builds a preset through the base's factory, with the preset's
259
+ // own type, so no preset runs that factory's default: build the base alone.
260
+ const base = Injector.create({
261
+ providers: [
262
+ {
263
+ provide: UiCartesianPreset,
264
+ useClass: UiCartesianPreset as unknown as Type<
265
+ UiCartesianPreset<XYDatum>
266
+ >,
267
+ },
268
+ ],
269
+ }).get(UiCartesianPreset);
270
+ const options = (
271
+ base as unknown as { commonOptions(): CartesianPresetOptions }
272
+ ).commonOptions();
273
+ expect(options).toEqual({
274
+ tickCount: 5,
275
+ showGrid: true,
276
+ showXGrid: false,
277
+ xAxisLabel: '',
278
+ yAxisLabel: '',
279
+ padding: {},
280
+ xMin: undefined,
281
+ xMax: undefined,
282
+ yMin: undefined,
283
+ yMax: undefined,
284
+ xTickFormat: undefined,
285
+ yTickFormat: undefined,
286
+ tooltip: true,
287
+ });
288
+ expect(base.height()).toBe(200);
289
+ expect(base.label()).toBe('Chart');
290
+ expect(base.zoomable()).toBe(false);
291
+ expect(base.zoomAxes()).toBe('xy');
292
+ });
293
+
294
+ it('passes the shared inputs through to its chart', async () => {
295
+ const { container, componentRef, getByText } = await render(PresetHost);
296
+ const root = chartRoot(container);
297
+ expect(root.getAttribute('accessibility-label')).toBe('Sales');
298
+ expect(root.getAttribute('class')).toContain('mt-2');
299
+ expect(root.getAttribute('style')).toContain('height: 160px');
300
+ expect(getByText('Units')).toBeTruthy();
301
+ expect(getByText('Day')).toBeTruthy();
302
+
303
+ (componentRef.instance as PresetHost).xAxisLabel.set('Week');
304
+ await waitForUpdate();
305
+ expect(getByText('Week')).toBeTruthy();
306
+ });
307
+
308
+ it('hands a tap over as the datum, or null off the points', async () => {
309
+ const { container, componentRef } = await render(PresetHost);
310
+ const host = componentRef.instance as PresetHost;
311
+ const root = chartRoot(container);
312
+ const middle = dotsDrawn(container)[1]!;
313
+ await tap(root, { x: middle.x + 1, y: middle.y + 1 });
314
+ await tap(root, { x: -100, y: -100 });
315
+ expect(host.tapped).toEqual([host.data[1], null]);
316
+ });
317
+
318
+ it('zooms its chart in and out, and resets it', async () => {
319
+ const { componentRef, queryByText } = await render(PresetHost);
320
+ const preset = (componentRef.instance as PresetHost).preset();
321
+ // The x axis spans 0–10; zoomed in around the middle, its last tick goes.
322
+ expect(queryByText('10')).not.toBeNull();
323
+ preset.zoomIn();
324
+ await waitForUpdate();
325
+ expect(queryByText('10')).toBeNull();
326
+ preset.zoomOut();
327
+ await waitForUpdate();
328
+ expect(queryByText('10')).not.toBeNull();
329
+
330
+ preset.zoomIn();
331
+ preset.resetZoom();
332
+ await waitForUpdate();
333
+ expect(queryByText('10')).not.toBeNull();
334
+ });
335
+ });
@@ -1,4 +1,4 @@
1
- import { Directive, input, viewChild } from '@angular/core';
1
+ import { Directive, booleanAttribute, input, viewChild } from '@angular/core';
2
2
  import { scaleLinear } from '@tanstack/charts/scales/linear';
3
3
  import type {
4
4
  ChartPoint,
@@ -173,6 +173,13 @@ export const minSpacing = (values: readonly number[]): number => {
173
173
  return Number.isFinite(spacing) && spacing > 0 ? spacing : 1;
174
174
  };
175
175
 
176
+ /**
177
+ * `UiChart`, for `viewChild()`. A call rather than the class itself: for a
178
+ * class, Angular's test build keeps an arrow function that nothing calls,
179
+ * which an app's coverage report would show as uncovered.
180
+ */
181
+ const chartType = () => UiChart;
182
+
176
183
  /**
177
184
  * The inputs and zoom API every cartesian preset shares. A preset extends this
178
185
  * and adds its own data and mark inputs, and renders `<ui-chart #chart>`
@@ -186,9 +193,9 @@ export abstract class UiCartesianPreset<TDatum> {
186
193
  /** Target number of ticks per axis; the axis picks nice values near it. */
187
194
  readonly tickCount = input(5);
188
195
  /** Horizontal gridlines at the y ticks. */
189
- readonly showGrid = input(true);
196
+ readonly showGrid = input(true, { transform: booleanAttribute });
190
197
  /** Vertical gridlines at the x ticks. */
191
- readonly showXGrid = input(false);
198
+ readonly showXGrid = input(false, { transform: booleanAttribute });
192
199
  readonly xAxisLabel = input('');
193
200
  readonly yAxisLabel = input('');
194
201
  readonly padding = input<ChartPadding>({});
@@ -199,20 +206,20 @@ export abstract class UiCartesianPreset<TDatum> {
199
206
  readonly xTickFormat = input<(value: number) => string>();
200
207
  readonly yTickFormat = input<(value: number) => string>();
201
208
  /** Shows a tooltip for the touched point; a tap pins it. */
202
- readonly tooltip = input(true);
209
+ readonly tooltip = input(true, { transform: booleanAttribute });
203
210
  /** What a screen reader announces for the chart. */
204
211
  readonly label = input('Chart');
205
212
  /** Light, dark or `'system'` colors; the chart theme's setting when unset. */
206
213
  readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
207
- readonly zoomable = input(false);
214
+ readonly zoomable = input(false, { transform: booleanAttribute });
208
215
  readonly zoomAxes = input<ChartZoomAxes>('xy');
209
- readonly showZoomControls = input(true);
216
+ readonly showZoomControls = input(true, { transform: booleanAttribute });
210
217
  readonly minZoom = input(1);
211
218
  readonly maxZoom = input(8);
212
219
  readonly zoomStep = input(1.4);
213
220
  readonly userClass = input<string>('', { alias: 'class' });
214
221
 
215
- protected readonly chart = viewChild.required(UiChart);
222
+ protected readonly chart = viewChild.required(chartType());
216
223
 
217
224
  /**
218
225
  * The shared inputs, as the options every preset's definition takes.
@@ -79,6 +79,12 @@ export const CHART_LIGHT_COLORS: ChartColorTokens = {
79
79
  'chart-3': 'rgba(39, 71, 84, 1)',
80
80
  'chart-4': 'rgba(232, 196, 104, 1)',
81
81
  'chart-5': 'rgba(244, 164, 98, 1)',
82
+ 'glass-tint': 'rgba(24, 24, 27, 1)',
83
+ 'glass-tint-foreground': 'rgba(250, 250, 250, 1)',
84
+ 'glass-foreground': 'rgba(9, 9, 11, 1)',
85
+ 'glass-muted-foreground': 'rgba(63, 63, 70, 1)',
86
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
87
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
82
88
  };
83
89
 
84
90
  export const CHART_DARK_COLORS: ChartColorTokens = {
@@ -106,6 +112,12 @@ export const CHART_DARK_COLORS: ChartColorTokens = {
106
112
  'chart-3': 'rgba(232, 140, 48, 1)',
107
113
  'chart-4': 'rgba(175, 87, 219, 1)',
108
114
  'chart-5': 'rgba(226, 54, 112, 1)',
115
+ 'glass-tint': 'rgba(250, 250, 250, 1)',
116
+ 'glass-tint-foreground': 'rgba(24, 24, 27, 1)',
117
+ 'glass-foreground': 'rgba(250, 250, 250, 1)',
118
+ 'glass-muted-foreground': 'rgba(212, 212, 216, 1)',
119
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
120
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
109
121
  };
110
122
 
111
123
  /**