@blotch/dolan 22.2.3 → 22.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) 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 +36 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +187 -7
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +280 -7
  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 +107 -1
  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 +5 -0
  17. package/dist/commands/update.js +126 -45
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +314 -2
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +7 -0
  22. package/dist/lockfile.js +36 -0
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +25 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +48 -2
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +45 -3
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +5 -0
  33. package/dist/test-utils.js +6 -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/components/accordion/accordion.test.ts +158 -144
  51. package/dist/ui/components/accordion/accordion.ts +19 -14
  52. package/dist/ui/components/action-sheet/action-sheet.test.ts +272 -187
  53. package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
  54. package/dist/ui/components/alert/alert.test.ts +67 -19
  55. package/dist/ui/components/alert/alert.ts +14 -16
  56. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +206 -139
  57. package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
  58. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  59. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  60. package/dist/ui/components/badge/badge.test.ts +57 -23
  61. package/dist/ui/components/badge/badge.ts +12 -14
  62. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  63. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
  64. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
  65. package/dist/ui/components/button/button.test.ts +611 -2
  66. package/dist/ui/components/button/button.ts +431 -62
  67. package/dist/ui/components/button/index.ts +2 -0
  68. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  69. package/dist/ui/components/button-group/button-group.ts +380 -68
  70. package/dist/ui/components/button-group/index.ts +3 -0
  71. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  72. package/dist/ui/components/card/card.test.ts +82 -62
  73. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  74. package/dist/ui/components/chart/chart-presets.ts +8 -1
  75. package/dist/ui/components/chart/chart-theme.ts +12 -0
  76. package/dist/ui/components/chart/chart.test.ts +1017 -0
  77. package/dist/ui/components/chart/chart.ts +56 -43
  78. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  79. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  80. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  81. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  82. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  83. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  84. package/dist/ui/components/context-menu/index.ts +1 -0
  85. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  86. package/dist/ui/components/dialog/dialog.ts +29 -23
  87. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  88. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  89. package/dist/ui/components/dock/dock.test.ts +749 -0
  90. package/dist/ui/components/dock/dock.ts +960 -0
  91. package/dist/ui/components/dock/index.ts +2 -0
  92. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  93. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  94. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  95. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  96. package/dist/ui/components/glass/glass-design.ts +119 -0
  97. package/dist/ui/components/glass/glass.test.ts +338 -0
  98. package/dist/ui/components/glass/glass.ts +360 -0
  99. package/dist/ui/components/glass/index.ts +18 -0
  100. package/dist/ui/components/icon/icon-theme.test.ts +36 -0
  101. package/dist/ui/components/icon/icon-theme.ts +36 -13
  102. package/dist/ui/components/icon/icon.test.ts +52 -41
  103. package/dist/ui/components/input/input.test.ts +178 -101
  104. package/dist/ui/components/input/input.ts +15 -12
  105. package/dist/ui/components/label/label.test.ts +11 -24
  106. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  107. package/dist/ui/components/list/list.test.ts +80 -49
  108. package/dist/ui/components/menu/index.ts +25 -0
  109. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  110. package/dist/ui/components/menu/menu-placement.ts +159 -0
  111. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  112. package/dist/ui/components/menu/menu-popup.ts +544 -0
  113. package/dist/ui/components/menu/menu.test.ts +429 -0
  114. package/dist/ui/components/menu/menu.ts +415 -0
  115. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  116. package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
  117. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  118. package/dist/ui/components/progress/progress.test.ts +102 -46
  119. package/dist/ui/components/progress/progress.ts +52 -54
  120. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  121. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  122. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  123. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  124. package/dist/ui/components/select/select.test.ts +138 -78
  125. package/dist/ui/components/select/select.ts +17 -11
  126. package/dist/ui/components/separator/separator.test.ts +18 -17
  127. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  128. package/dist/ui/components/sheet/sheet.ts +9 -1
  129. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  130. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  131. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  132. package/dist/ui/components/spinner/spinner.ts +16 -19
  133. package/dist/ui/components/switch/switch.test.ts +116 -46
  134. package/dist/ui/components/switch/switch.ts +8 -13
  135. package/dist/ui/components/tab-bar/index.ts +1 -0
  136. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  137. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  138. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  139. package/dist/ui/components/tabs/tabs.ts +110 -118
  140. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  141. package/dist/ui/components/textarea/textarea.ts +14 -12
  142. package/dist/ui/components/toast/toast.test.ts +434 -350
  143. package/dist/ui/components/toast/toast.ts +42 -51
  144. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  145. package/dist/ui/theme/chart-colors.test.ts +8 -3
  146. package/dist/ui/theme/dark.css +38 -2
  147. package/dist/ui/theme/default.css +55 -7
  148. package/dist/ui/theme/icon-colors.test.ts +8 -3
  149. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  150. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  151. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  152. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  153. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  154. package/dist/ui/theme/tailwind.css +113 -0
  155. package/dist/ui/utils/animate.test.ts +13 -1
  156. package/dist/utils/analyze.d.ts +14 -0
  157. package/dist/utils/analyze.js +15 -0
  158. package/dist/utils/analyze.js.map +1 -1
  159. package/dist/utils/analyze.test.js +14 -1
  160. package/dist/utils/analyze.test.js.map +1 -1
  161. package/dist/utils/installed.d.ts +12 -0
  162. package/dist/utils/installed.js +16 -0
  163. package/dist/utils/installed.js.map +1 -1
  164. package/dist/utils/installed.test.js +17 -1
  165. package/dist/utils/installed.test.js.map +1 -1
  166. package/dist/utils/resolve-paths.d.ts +4 -4
  167. package/dist/utils/resolve-paths.js +5 -7
  168. package/dist/utils/resolve-paths.js.map +1 -1
  169. package/dist/utils/resolve-paths.test.js +9 -5
  170. package/dist/utils/resolve-paths.test.js.map +1 -1
  171. package/dist/utils/tailwind.d.ts +25 -0
  172. package/dist/utils/tailwind.js +51 -0
  173. package/dist/utils/tailwind.js.map +1 -0
  174. package/dist/utils/tailwind.test.d.ts +1 -0
  175. package/dist/utils/tailwind.test.js +71 -0
  176. package/dist/utils/tailwind.test.js.map +1 -0
  177. package/package.json +9 -1
  178. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  179. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  180. package/dist/ui/components/button/button-render.test.ts +0 -132
  181. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  182. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  183. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  184. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  185. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -1,5 +1,481 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { buttonGroupItemVariants } from './button-group';
1
+ import { Component, type Provider, signal } from '@angular/core';
2
+ import { LynxAccessibilitySettings } from '@blotch/angular-lynx';
3
+ import {
4
+ fireEvent,
5
+ render,
6
+ waitForUpdate,
7
+ } from '@blotch/angular-lynx-testing-library';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
10
+ import { UiIcon } from '../icon/icon';
11
+ import { ICON_LIGHT_COLORS } from '../icon/icon-theme';
12
+ import {
13
+ type ButtonGroupOrientation,
14
+ type ButtonGroupSize,
15
+ type ButtonGroupVariant,
16
+ UiButtonGroup,
17
+ UiButtonGroupItem,
18
+ buttonGroupItemVariants,
19
+ } from './button-group';
20
+
21
+ @Component({
22
+ imports: [UiButtonGroup, UiButtonGroupItem, UiIcon],
23
+ template: `
24
+ <ui-button-group
25
+ [orientation]="orientation()"
26
+ [variant]="variant()"
27
+ [size]="size()"
28
+ [disabled]="groupDisabled()"
29
+ class="w-full"
30
+ >
31
+ <ui-button-group-item
32
+ class="flex-1"
33
+ [accessibility-label]="copyLabel()"
34
+ (pressed)="presses.push('Copy')"
35
+ >
36
+ @if (withIcon()) {
37
+ <ui-icon name="copy" />
38
+ }
39
+ Copy
40
+ </ui-button-group-item>
41
+ <ui-button-group-item
42
+ [disabled]="pasteDisabled()"
43
+ (pressed)="presses.push('Paste')"
44
+ >Paste</ui-button-group-item
45
+ >
46
+ </ui-button-group>
47
+ `,
48
+ })
49
+ class ButtonGroupHost {
50
+ readonly orientation = signal<ButtonGroupOrientation>('horizontal');
51
+ readonly variant = signal<ButtonGroupVariant>('default');
52
+ readonly size = signal<ButtonGroupSize>('default');
53
+ readonly groupDisabled = signal(false);
54
+ readonly pasteDisabled = signal(false);
55
+ readonly withIcon = signal(false);
56
+ readonly copyLabel = signal<string | undefined>(undefined);
57
+ readonly presses: string[] = [];
58
+ }
59
+
60
+ /** Accessibility settings a test can change, as the host app would. */
61
+ const settings = {
62
+ reduceMotion: signal(false),
63
+ reduceTransparency: signal(false),
64
+ increaseContrast: signal(false),
65
+ };
66
+
67
+ /**
68
+ * Renders the host on a platform, as `SystemInfo` reports it, or with no
69
+ * SystemInfo at all, which gives the glass variants Material's design.
70
+ */
71
+ const renderGroup = async (
72
+ platform?: { platform: string; osVersion: string },
73
+ providers: Provider[] = [],
74
+ ) => {
75
+ vi.stubGlobal('SystemInfo', platform);
76
+ const result = await render(ButtonGroupHost, {
77
+ providers: [
78
+ { provide: LynxAccessibilitySettings, useValue: settings },
79
+ ...providers,
80
+ ],
81
+ });
82
+ return {
83
+ ...result,
84
+ host: result.componentRef.instance as ButtonGroupHost,
85
+ /**
86
+ * The group's own box, around the items' hosts: the bar of the solid
87
+ * variants, Material's connected group, or the glass's frame.
88
+ */
89
+ group: (): HTMLElement => {
90
+ const parent =
91
+ result.getByText('Paste').parentElement!.parentElement!.parentElement!;
92
+ return parent.localName === 'blur-view' ? parent.parentElement! : parent;
93
+ },
94
+ glass: () => result.container.querySelector<HTMLElement>('blur-view')!,
95
+ // An item's view (the one that takes presses) holds its label.
96
+ item: (label: string) => result.getByText(label).parentElement!,
97
+ icon: () => result.container.querySelector('svg')!,
98
+ update: async (change: () => void) => {
99
+ change();
100
+ await waitForUpdate();
101
+ },
102
+ };
103
+ };
104
+
105
+ const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
106
+ const ANDROID = { platform: 'Android', osVersion: '15' };
107
+ const WEB = { platform: 'web', osVersion: '' };
108
+
109
+ /**
110
+ * The scale an item's inline style holds, if any.
111
+ */
112
+ const scaleOf = (view: Element) =>
113
+ (view as HTMLElement).style.getPropertyValue('transform');
114
+
115
+ // The icon's selector isn't a Lynx element, so the renderer falls back to a
116
+ // <view> for it and warns.
117
+ beforeEach(() => {
118
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
119
+ });
120
+
121
+ afterEach(() => {
122
+ vi.restoreAllMocks();
123
+ vi.unstubAllGlobals();
124
+ settings.reduceMotion.set(false);
125
+ settings.reduceTransparency.set(false);
126
+ settings.increaseContrast.set(false);
127
+ });
128
+
129
+ describe('UiButtonGroup', () => {
130
+ it('lays its items out in a row, or in a column when vertical', async () => {
131
+ const { host, group } = await renderGroup();
132
+ expect(group().classList).toContain('flex-row');
133
+ expect(group().classList).toContain('w-full');
134
+
135
+ host.orientation.set('vertical');
136
+ await waitForUpdate();
137
+ expect(group().classList).toContain('flex-col');
138
+ expect(group().classList).not.toContain('flex-row');
139
+ });
140
+
141
+ it("styles every item and label with the group's variant and size", async () => {
142
+ const { host, item, getByText } = await renderGroup();
143
+ expect(item('Copy').classList).toContain('bg-secondary');
144
+ expect(item('Copy').classList).toContain('h-10');
145
+ expect(item('Copy').classList).toContain('flex-1');
146
+ expect(getByText('Copy').classList).toContain('text-secondary-foreground');
147
+ expect(getByText('Copy').classList).toContain('text-sm');
148
+
149
+ host.variant.set('outline');
150
+ host.size.set('sm');
151
+ await waitForUpdate();
152
+ for (const label of ['Copy', 'Paste']) {
153
+ expect(item(label).classList).toContain('bg-background');
154
+ expect(item(label).classList).toContain('h-9');
155
+ expect(getByText(label).classList).toContain('text-foreground');
156
+ expect(getByText(label).classList).toContain('text-xs');
157
+ }
158
+
159
+ host.size.set('lg');
160
+ await waitForUpdate();
161
+ expect(item('Paste').classList).toContain('h-11');
162
+ expect(getByText('Paste').classList).toContain('text-base');
163
+ });
164
+ });
165
+
166
+ describe('UiButtonGroupItem', () => {
167
+ it('emits `pressed` when tapped', async () => {
168
+ const { host, item } = await renderGroup();
169
+ fireEvent.tap(item('Copy'));
170
+ fireEvent.tap(item('Paste'));
171
+ expect(host.presses).toEqual(['Copy', 'Paste']);
172
+ expect(item('Copy').classList).not.toContain('opacity-50');
173
+ });
174
+
175
+ it('dims and ignores taps when disabled itself, leaving the others working', async () => {
176
+ const { host, item } = await renderGroup();
177
+ host.pasteDisabled.set(true);
178
+ await waitForUpdate();
179
+ expect(item('Paste').classList).toContain('opacity-50');
180
+ expect(item('Copy').classList).not.toContain('opacity-50');
181
+
182
+ fireEvent.tap(item('Paste'));
183
+ fireEvent.tap(item('Copy'));
184
+ expect(host.presses).toEqual(['Copy']);
185
+ });
186
+
187
+ it('dims and ignores taps on every item when the group is disabled', async () => {
188
+ const { host, item } = await renderGroup();
189
+ host.groupDisabled.set(true);
190
+ await waitForUpdate();
191
+ for (const label of ['Copy', 'Paste']) {
192
+ expect(item(label).classList).toContain('opacity-50');
193
+ fireEvent.tap(item(label));
194
+ }
195
+ expect(host.presses).toEqual([]);
196
+ });
197
+
198
+ it('scales down while pressed by touch, and back on release', async () => {
199
+ const { item } = await renderGroup();
200
+ fireEvent.touchstart(item('Copy'));
201
+ expect(scaleOf(item('Copy'))).toBe('scale(0.97)');
202
+ fireEvent.touchend(item('Copy'));
203
+ expect(scaleOf(item('Copy'))).toBe('scale(1)');
204
+
205
+ // A scroll that takes the touch over cancels it: the item scales back
206
+ // rather than staying pressed.
207
+ fireEvent.touchstart(item('Copy'));
208
+ fireEvent.touchcancel(item('Copy'));
209
+ expect(scaleOf(item('Copy'))).toBe('scale(1)');
210
+ });
211
+
212
+ it('scales down while pressed by mouse, and back on release or leave', async () => {
213
+ // Lynx on the web fires mouse events for a cursor, never touch ones.
214
+ const { item } = await renderGroup();
215
+ fireEvent.mousedown(item('Copy'));
216
+ expect(scaleOf(item('Copy'))).toBe('scale(0.97)');
217
+ fireEvent.mouseup(item('Copy'));
218
+ expect(scaleOf(item('Copy'))).toBe('scale(1)');
219
+
220
+ fireEvent.mousedown(item('Copy'));
221
+ fireLynxEvent(item('Copy'), 'mouseleave');
222
+ expect(scaleOf(item('Copy'))).toBe('scale(1)');
223
+ });
224
+
225
+ it("doesn't react to presses while it or its group is disabled", async () => {
226
+ const { host, item } = await renderGroup();
227
+ host.pasteDisabled.set(true);
228
+ host.groupDisabled.set(true);
229
+ await waitForUpdate();
230
+ for (const label of ['Copy', 'Paste']) {
231
+ fireEvent.touchstart(item(label));
232
+ fireEvent.mousedown(item(label));
233
+ expect(scaleOf(item(label))).toBe('');
234
+ fireEvent.touchend(item(label));
235
+ fireEvent.mouseup(item(label));
236
+ expect(scaleOf(item(label))).toBe('');
237
+ }
238
+ });
239
+ });
240
+
241
+ describe('UiButtonGroupItem icons and accessibility', () => {
242
+ it('puts a projected icon before the label, in the label color', async () => {
243
+ const { host, item, getByText, icon, update } = await renderGroup();
244
+ await update(() => host.withIcon.set(true));
245
+
246
+ expect(item('Copy').classList).toContain('gap-2');
247
+ expect(icon().compareDocumentPosition(getByText('Copy'))).toBe(
248
+ Node.DOCUMENT_POSITION_FOLLOWING,
249
+ );
250
+ expect(icon().getAttribute('content')).toContain(
251
+ ICON_LIGHT_COLORS['secondary-foreground'],
252
+ );
253
+
254
+ await update(() => host.variant.set('outline'));
255
+ expect(icon().getAttribute('content')).toContain(
256
+ ICON_LIGHT_COLORS['foreground'],
257
+ );
258
+ });
259
+
260
+ it('shows only the icon in square items, named by its accessibility label', async () => {
261
+ const { host, queryByText, getByRole, update } = await renderGroup();
262
+ await update(() => {
263
+ host.size.set('icon');
264
+ host.withIcon.set(true);
265
+ host.copyLabel.set('Copy the text');
266
+ });
267
+
268
+ expect(queryByText('Copy')).toBeNull();
269
+ const copy = getByRole('button', { name: 'Copy the text' });
270
+ expect(copy.classList).toContain('w-10');
271
+ expect(copy.classList).not.toContain('gap-2');
272
+ // An item with a label is read by it, as a button.
273
+ expect(getByRole('button', { name: 'Paste' }).localName).toBe('text');
274
+ });
275
+
276
+ it('dims at once instead of shrinking under Reduce Motion', async () => {
277
+ const { item, update } = await renderGroup();
278
+ await update(() => settings.reduceMotion.set(true));
279
+
280
+ fireEvent.touchstart(item('Copy'));
281
+ await waitForUpdate();
282
+ expect(item('Copy').classList).toContain('opacity-70');
283
+ expect(scaleOf(item('Copy'))).toBe('');
284
+
285
+ fireEvent.touchend(item('Copy'));
286
+ await waitForUpdate();
287
+ expect(item('Copy').classList).not.toContain('opacity-70');
288
+ expect(scaleOf(item('Copy'))).toBe('');
289
+ });
290
+ });
291
+
292
+ describe('UiButtonGroup glass variants', () => {
293
+ it('is one interactive glass shape on iOS 26, with plain items inside', async () => {
294
+ const { host, group, glass, item, getByText, update } =
295
+ await renderGroup(IOS_26);
296
+ await update(() => host.variant.set('glass'));
297
+
298
+ expect(glass().parentElement).toBe(group());
299
+ expect(glass().getAttribute('blur-effect')).toBe('glass');
300
+ expect(glass().getAttribute('glass-style')).toBe('regular');
301
+ expect(glass().getAttribute('glass-interactive')).toBe('true');
302
+ expect(glass().classList).toContain('flex-row');
303
+ expect(glass().classList).toContain('p-1');
304
+ // A row is a capsule.
305
+ expect(glass().classList).toContain('rounded-full');
306
+ expect(glass().style.getPropertyValue('border-radius')).toBe('');
307
+ expect(group().classList).toContain('rounded-full');
308
+ expect(group().classList).toContain('self-start');
309
+ expect(group().classList).toContain('w-full');
310
+ expect(group().classList).not.toContain('bg-glass-dim');
311
+ for (const label of ['Copy', 'Paste']) {
312
+ expect(glass().contains(item(label))).toBe(true);
313
+ expect(item(label).classList).toContain('h-10');
314
+ expect(item(label).classList).not.toContain('bg-secondary');
315
+ expect(getByText(label).classList).toContain('text-glass-foreground');
316
+ }
317
+
318
+ // A column of wider items has corners of half an item's height plus
319
+ // the 4px inset.
320
+ await update(() => host.orientation.set('vertical'));
321
+ expect(glass().classList).toContain('flex-col');
322
+ expect(glass().classList).not.toContain('rounded-full');
323
+ expect(glass().style.getPropertyValue('border-radius')).toBe('24px');
324
+ expect(group().style.getPropertyValue('border-radius')).toBe('24px');
325
+
326
+ // A column of square icons is a capsule again.
327
+ await update(() => host.size.set('icon'));
328
+ expect(glass().classList).toContain('rounded-full');
329
+ });
330
+
331
+ it('highlights a pressed item in corners concentric with the glass', async () => {
332
+ const { host, item, getByText, update } = await renderGroup(IOS_26);
333
+ await update(() => host.variant.set('glass'));
334
+ const highlight = () => item('Copy').querySelector('.absolute');
335
+
336
+ fireEvent.touchstart(item('Copy'));
337
+ await waitForUpdate();
338
+ expect(highlight()!.classList).toContain('bg-glass-foreground');
339
+ // 24px glass corners, 4px out: 20px.
340
+ expect((highlight() as HTMLElement).style.borderRadius).toBe('20px');
341
+ expect(highlight()!.compareDocumentPosition(getByText('Copy'))).toBe(
342
+ Node.DOCUMENT_POSITION_FOLLOWING,
343
+ );
344
+ expect(scaleOf(item('Copy'))).toBe('');
345
+
346
+ fireEvent.touchend(item('Copy'));
347
+ await waitForUpdate();
348
+ expect(highlight()).toBeNull();
349
+
350
+ // Smaller items, smaller corners: 22px and 18px.
351
+ await update(() => host.size.set('sm'));
352
+ fireEvent.mousedown(item('Copy'));
353
+ await waitForUpdate();
354
+ expect((highlight() as HTMLElement).style.borderRadius).toBe('18px');
355
+ fireLynxEvent(item('Copy'), 'mouseleave');
356
+ await waitForUpdate();
357
+ expect(highlight()).toBeNull();
358
+ });
359
+
360
+ it('sits clear glass on the dimming layer, with white icons', async () => {
361
+ const { host, group, glass, icon, update } = await renderGroup(IOS_26);
362
+ await update(() => {
363
+ host.variant.set('glass-clear');
364
+ host.withIcon.set(true);
365
+ });
366
+
367
+ expect(glass().getAttribute('glass-style')).toBe('clear');
368
+ expect(group().classList).toContain('bg-glass-dim');
369
+ expect(group().classList).toContain('rounded-full');
370
+ expect(icon().getAttribute('content')).toContain(
371
+ ICON_LIGHT_COLORS['glass-clear-foreground'],
372
+ );
373
+ });
374
+
375
+ it("stops the glass reacting and dims the items while it's disabled", async () => {
376
+ const { host, glass, item, update } = await renderGroup(IOS_26);
377
+ await update(() => {
378
+ host.variant.set('glass');
379
+ host.groupDisabled.set(true);
380
+ });
381
+
382
+ expect(glass().hasAttribute('glass-interactive')).toBe(false);
383
+ expect(item('Copy').classList).toContain('opacity-50');
384
+ fireEvent.touchstart(item('Copy'));
385
+ fireEvent.tap(item('Copy'));
386
+ expect(item('Copy').querySelector('.absolute')).toBeNull();
387
+ expect(host.presses).toEqual([]);
388
+ });
389
+
390
+ it('stops the glass swelling under Reduce Motion in the web preview, still highlighting', async () => {
391
+ const { host, glass, item, update } = await renderGroup(WEB);
392
+ await update(() => {
393
+ host.variant.set('glass');
394
+ settings.reduceMotion.set(true);
395
+ });
396
+
397
+ expect(glass().hasAttribute('glass-interactive')).toBe(false);
398
+ fireEvent.mousedown(item('Copy'));
399
+ await waitForUpdate();
400
+ expect(item('Copy').querySelector('.absolute')).not.toBeNull();
401
+ expect(scaleOf(item('Copy'))).toBe('');
402
+ });
403
+
404
+ it('drops the dimming layer when Reduce Transparency makes the glass solid', async () => {
405
+ const { host, group, icon, update } = await renderGroup(WEB);
406
+ await update(() => {
407
+ host.variant.set('glass-clear');
408
+ host.withIcon.set(true);
409
+ settings.reduceTransparency.set(true);
410
+ });
411
+
412
+ expect(group().classList).not.toContain('bg-glass-dim');
413
+ expect(icon().getAttribute('content')).toContain(
414
+ ICON_LIGHT_COLORS['glass-foreground'],
415
+ );
416
+ });
417
+ });
418
+
419
+ describe('UiButtonGroup glass variants in Material (Android)', () => {
420
+ it("is Material's connected button group", async () => {
421
+ const { host, group, glass, item, getByText, icon, update } =
422
+ await renderGroup(ANDROID);
423
+ await update(() => {
424
+ host.variant.set('glass');
425
+ host.withIcon.set(true);
426
+ });
427
+
428
+ expect(glass()).toBeNull();
429
+ for (const name of ['gap-0.5', 'rounded-full', 'overflow-hidden']) {
430
+ expect(group().classList).toContain(name);
431
+ }
432
+ expect(group().classList).not.toContain('bg-border');
433
+ expect(item('Copy').classList).toContain('bg-secondary');
434
+ expect(item('Copy').classList).toContain('rounded-[8px]');
435
+ expect(getByText('Copy').classList).toContain('text-secondary-foreground');
436
+ expect(icon().getAttribute('content')).toContain(
437
+ ICON_LIGHT_COLORS['secondary-foreground'],
438
+ );
439
+
440
+ // Clear glass is the same tonal group here.
441
+ await update(() => host.variant.set('glass-clear'));
442
+ expect(item('Copy').classList).toContain('bg-secondary');
443
+
444
+ // A column's ends are half an item round, or a capsule of square icons.
445
+ await update(() => host.orientation.set('vertical'));
446
+ expect(group().classList).not.toContain('rounded-full');
447
+ expect(group().style.borderRadius).toBe('20px');
448
+ await update(() => host.size.set('icon'));
449
+ expect(group().classList).toContain('rounded-full');
450
+ expect(group().style.borderRadius).toBe('');
451
+ });
452
+
453
+ it('shows the state layer on a pressed item', async () => {
454
+ const { host, item, update } = await renderGroup(ANDROID);
455
+ await update(() => host.variant.set('glass'));
456
+ const layer = () => item('Copy').querySelector('.absolute');
457
+
458
+ fireEvent.touchstart(item('Copy'));
459
+ await waitForUpdate();
460
+ expect(layer()!.classList).toContain('bg-secondary-foreground');
461
+ expect(layer()!.classList).toContain('opacity-10');
462
+ expect(scaleOf(item('Copy'))).toBe('');
463
+
464
+ fireEvent.touchcancel(item('Copy'));
465
+ await waitForUpdate();
466
+ expect(layer()).toBeNull();
467
+ });
468
+
469
+ it('outlines the items under Increase Contrast', async () => {
470
+ const { host, item, update } = await renderGroup(ANDROID);
471
+ await update(() => {
472
+ host.variant.set('glass');
473
+ settings.increaseContrast.set(true);
474
+ });
475
+
476
+ expect(item('Copy').classList).toContain('border-foreground');
477
+ });
478
+ });
3
479
 
4
480
  describe('buttonGroupItemVariants', () => {
5
481
  it('returns default variant and size classes', () => {
@@ -26,6 +502,12 @@ describe('buttonGroupItemVariants', () => {
26
502
  expect(result).toContain('px-6');
27
503
  });
28
504
 
505
+ it('returns square icon size classes', () => {
506
+ const result = buttonGroupItemVariants({ size: 'icon' });
507
+ expect(result).toContain('h-10');
508
+ expect(result).toContain('w-10');
509
+ });
510
+
29
511
  it('always includes base layout classes', () => {
30
512
  const result = buttonGroupItemVariants({ variant: 'outline', size: 'lg' });
31
513
  expect(result).toContain('flex');