@blotch/dolan 22.2.4 → 22.2.6
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.
- package/dist/commands/add.test.js +2 -0
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +78 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +167 -26
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.test.js +4 -7
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/update.d.ts +3 -1
- package/dist/commands/update.js +78 -15
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +257 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +10 -0
- package/dist/lockfile.js +20 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.js +43 -3
- package/dist/lockfile.test.js.map +1 -1
- package/dist/registry.js +73 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +16 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +24 -2
- package/dist/test-utils.js +41 -2
- package/dist/test-utils.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
- package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/badge/badge.ts +2 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +36 -1
- package/dist/ui/components/button/button.ts +17 -7
- package/dist/ui/components/button-group/button-group.test.ts +53 -9
- package/dist/ui/components/button-group/button-group.ts +67 -21
- package/dist/ui/components/card/card.test.ts +16 -0
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.ts +6 -6
- package/dist/ui/components/chart/chart.test.ts +14 -0
- package/dist/ui/components/chart/chart.ts +10 -3
- package/dist/ui/components/checkbox/checkbox.ts +2 -1
- package/dist/ui/components/collapsible/collapsible.ts +2 -1
- package/dist/ui/components/dialog/dialog.ts +6 -6
- package/dist/ui/components/dock/dock-scroll.ts +9 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +4 -1
- package/dist/ui/components/dock/dock.ts +529 -51
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/glass/glass.test.ts +39 -1
- package/dist/ui/components/glass/glass.ts +87 -17
- package/dist/ui/components/glass/index.ts +1 -0
- package/dist/ui/components/icon/icon-theme.test.ts +28 -0
- package/dist/ui/components/icon/icon-theme.ts +38 -7
- package/dist/ui/components/input/input.ts +2 -1
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/menu-popup.test.ts +18 -14
- package/dist/ui/components/menu/menu-popup.ts +24 -3
- package/dist/ui/components/menu/menu.test.ts +99 -5
- package/dist/ui/components/menu/menu.ts +74 -34
- package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.ts +2 -1
- package/dist/ui/components/radio-group/radio-group.ts +2 -1
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.ts +2 -1
- package/dist/ui/components/sheet/sheet.test.ts +90 -3
- package/dist/ui/components/sheet/sheet.ts +59 -4
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +464 -48
- package/dist/ui/components/switch/switch.ts +613 -43
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +278 -2
- package/dist/ui/components/tabs/tabs.ts +233 -30
- package/dist/ui/components/textarea/textarea.ts +2 -1
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +310 -4
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/utils/analyze.d.ts +10 -5
- package/dist/utils/analyze.js +3 -2
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/installed.d.ts +13 -1
- package/dist/utils/installed.js +17 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +39 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -5
- package/dist/utils/tailwind.js +53 -14
- package/dist/utils/tailwind.js.map +1 -1
- package/dist/utils/tailwind.test.js +85 -22
- package/dist/utils/tailwind.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
<ui-icon name="search" />
|
|
34
34
|
}
|
|
35
35
|
Save
|
|
36
|
+
@if (withTrailingIcon()) {
|
|
37
|
+
<ui-icon name="arrow-right" trailing />
|
|
38
|
+
}
|
|
36
39
|
</ui-button>
|
|
37
40
|
`,
|
|
38
41
|
})
|
|
@@ -43,6 +46,7 @@ class ButtonHost {
|
|
|
43
46
|
readonly loading = signal(false);
|
|
44
47
|
readonly label = signal<string | undefined>(undefined);
|
|
45
48
|
readonly withIcon = signal(false);
|
|
49
|
+
readonly withTrailingIcon = signal(false);
|
|
46
50
|
readonly presses = signal(0);
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -80,6 +84,7 @@ const renderButton = async (
|
|
|
80
84
|
glass: () => result.container.querySelector('blur-view')!,
|
|
81
85
|
spinner: () => result.container.querySelector('svg'),
|
|
82
86
|
icon: () => result.container.querySelector('svg')!,
|
|
87
|
+
icons: () => [...result.container.querySelectorAll('svg')],
|
|
83
88
|
update: async (change: () => void) => {
|
|
84
89
|
change();
|
|
85
90
|
await waitForUpdate();
|
|
@@ -270,6 +275,35 @@ describe('UiButton', () => {
|
|
|
270
275
|
);
|
|
271
276
|
});
|
|
272
277
|
|
|
278
|
+
it('puts an icon marked trailing after the label, in the label color', async () => {
|
|
279
|
+
const { host, button, getByText, icon, update } = await renderButton();
|
|
280
|
+
await update(() => host.withTrailingIcon.set(true));
|
|
281
|
+
|
|
282
|
+
expect(button().classList).toContain('gap-2');
|
|
283
|
+
expect(icon().compareDocumentPosition(getByText('Save'))).toBe(
|
|
284
|
+
Node.DOCUMENT_POSITION_PRECEDING,
|
|
285
|
+
);
|
|
286
|
+
expect(icon().getAttribute('content')).toContain(
|
|
287
|
+
ICON_LIGHT_COLORS['primary-foreground'],
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('puts one icon on each side of the label', async () => {
|
|
292
|
+
const { host, getByText, icons, update } = await renderButton();
|
|
293
|
+
await update(() => {
|
|
294
|
+
host.withIcon.set(true);
|
|
295
|
+
host.withTrailingIcon.set(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const [leading, trailing] = icons();
|
|
299
|
+
expect(leading.compareDocumentPosition(getByText('Save'))).toBe(
|
|
300
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
301
|
+
);
|
|
302
|
+
expect(trailing.compareDocumentPosition(getByText('Save'))).toBe(
|
|
303
|
+
Node.DOCUMENT_POSITION_PRECEDING,
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
273
307
|
it('shows only the icon in an icon size, named by its accessibility label', async () => {
|
|
274
308
|
const { host, button, queryByText, getByRole, update } =
|
|
275
309
|
await renderButton();
|
|
@@ -432,10 +466,11 @@ describe('UiButton glass variants', () => {
|
|
|
432
466
|
expect(getByRole('button', { name: 'Search' })).toBe(button());
|
|
433
467
|
});
|
|
434
468
|
|
|
435
|
-
it('draws the
|
|
469
|
+
it('draws the thin material before iOS 26, and shrinks when pressed', async () => {
|
|
436
470
|
const { host, button, glass, update } = await renderButton(IOS_18);
|
|
437
471
|
await update(() => host.variant.set('glass'));
|
|
438
472
|
|
|
473
|
+
expect(glass().getAttribute('blur-material')).toBe('thin');
|
|
439
474
|
expect(glass().getAttribute('blur-effect')).toBe('extra-light');
|
|
440
475
|
fireEvent.touchstart(button());
|
|
441
476
|
expect(scaleOf(button())).toBe('scale(0.97)');
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Component,
|
|
5
5
|
Injector,
|
|
6
6
|
ViewEncapsulation,
|
|
7
|
+
booleanAttribute,
|
|
7
8
|
computed,
|
|
8
9
|
contentChild,
|
|
9
10
|
inject,
|
|
@@ -182,8 +183,8 @@ const materialButtonVariants = cva(
|
|
|
182
183
|
|
|
183
184
|
/**
|
|
184
185
|
* How a glass variant looks on this platform: `'glass'` draws it on a
|
|
185
|
-
* `<blur-view ui-glass>` (Liquid Glass, or the
|
|
186
|
-
*
|
|
186
|
+
* `<blur-view ui-glass>` (Liquid Glass, or before iOS 26 the thin material
|
|
187
|
+
* Apple used for buttons over content), `'material'` as a Material button. The solid variants are `'solid'`
|
|
187
188
|
* everywhere.
|
|
188
189
|
*/
|
|
189
190
|
type ButtonLook = 'solid' | 'glass' | 'material';
|
|
@@ -271,6 +272,7 @@ const buttonIconColor = () => inject(UiButton).foreground;
|
|
|
271
272
|
<blur-view
|
|
272
273
|
ui-glass
|
|
273
274
|
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
275
|
+
material="thin"
|
|
274
276
|
[tint]="
|
|
275
277
|
variant() === 'glass-prominent' ? 'var(--glass-tint)' : undefined
|
|
276
278
|
"
|
|
@@ -290,12 +292,16 @@ const buttonIconColor = () => inject(UiButton).foreground;
|
|
|
290
292
|
</view>
|
|
291
293
|
|
|
292
294
|
<!-- One copy of the content, for whichever form the button takes: Angular
|
|
293
|
-
projects content once, so it can't be in both branches above.
|
|
295
|
+
projects content once, so it can't be in both branches above.
|
|
296
|
+
|
|
297
|
+
The label's text has to be inside a <text>, so the content can't keep
|
|
298
|
+
its own order: an icon goes before the label, and one marked
|
|
299
|
+
trailing after it, for an icon that points onward ("Next →"). -->
|
|
294
300
|
<ng-template #content>
|
|
295
301
|
@if (loading()) {
|
|
296
302
|
<ui-spinner [size]="spinnerSize()" [color]="foreground()" />
|
|
297
303
|
} @else {
|
|
298
|
-
<ng-content select="ui-icon" />
|
|
304
|
+
<ng-content select="ui-icon:not([trailing])" />
|
|
299
305
|
@if (showLabel()) {
|
|
300
306
|
<text
|
|
301
307
|
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
@@ -303,6 +309,7 @@ const buttonIconColor = () => inject(UiButton).foreground;
|
|
|
303
309
|
><ng-content
|
|
304
310
|
/></text>
|
|
305
311
|
}
|
|
312
|
+
<ng-content select="ui-icon[trailing]" />
|
|
306
313
|
}
|
|
307
314
|
</ng-template>
|
|
308
315
|
`,
|
|
@@ -320,8 +327,8 @@ export class UiButton {
|
|
|
320
327
|
* either).
|
|
321
328
|
*/
|
|
322
329
|
readonly size = input<ButtonSize>('default');
|
|
323
|
-
readonly disabled = input(false);
|
|
324
|
-
readonly loading = input(false);
|
|
330
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
331
|
+
readonly loading = input(false, { transform: booleanAttribute });
|
|
325
332
|
/**
|
|
326
333
|
* What a screen reader says for the button. An icon-only button needs one;
|
|
327
334
|
* a button with a label reads its label without it.
|
|
@@ -334,7 +341,10 @@ export class UiButton {
|
|
|
334
341
|
readonly pressed = output<void>();
|
|
335
342
|
|
|
336
343
|
readonly containerRef = viewChild<ElementRef>('container');
|
|
337
|
-
/**
|
|
344
|
+
/**
|
|
345
|
+
* A `<ui-icon>` projected into the button, which sits before the label, or
|
|
346
|
+
* after it when it's marked `trailing`.
|
|
347
|
+
*/
|
|
338
348
|
readonly projectedIcon = contentChild(iconType());
|
|
339
349
|
|
|
340
350
|
readonly #settings = inject(LynxAccessibilitySettings);
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
}
|
|
39
39
|
Copy
|
|
40
40
|
</ui-button-group-item>
|
|
41
|
+
@if (withCut()) {
|
|
42
|
+
<ui-button-group-item>Cut</ui-button-group-item>
|
|
43
|
+
}
|
|
41
44
|
<ui-button-group-item
|
|
42
45
|
[disabled]="pasteDisabled()"
|
|
43
46
|
(pressed)="presses.push('Paste')"
|
|
@@ -53,6 +56,7 @@ class ButtonGroupHost {
|
|
|
53
56
|
readonly groupDisabled = signal(false);
|
|
54
57
|
readonly pasteDisabled = signal(false);
|
|
55
58
|
readonly withIcon = signal(false);
|
|
59
|
+
readonly withCut = signal(false);
|
|
56
60
|
readonly copyLabel = signal<string | undefined>(undefined);
|
|
57
61
|
readonly presses: string[] = [];
|
|
58
62
|
}
|
|
@@ -103,9 +107,19 @@ const renderGroup = async (
|
|
|
103
107
|
};
|
|
104
108
|
|
|
105
109
|
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
110
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
106
111
|
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
107
112
|
const WEB = { platform: 'web', osVersion: '' };
|
|
108
113
|
|
|
114
|
+
/**
|
|
115
|
+
* An item's inline corner radii: top left, top right, bottom right and
|
|
116
|
+
* bottom left.
|
|
117
|
+
*/
|
|
118
|
+
const cornersOf = (view: HTMLElement) =>
|
|
119
|
+
['top-left', 'top-right', 'bottom-right', 'bottom-left'].map((corner) =>
|
|
120
|
+
view.style.getPropertyValue(`border-${corner}-radius`),
|
|
121
|
+
);
|
|
122
|
+
|
|
109
123
|
/**
|
|
110
124
|
* The scale an item's inline style holds, if any.
|
|
111
125
|
*/
|
|
@@ -328,6 +342,14 @@ describe('UiButtonGroup glass variants', () => {
|
|
|
328
342
|
expect(glass().classList).toContain('rounded-full');
|
|
329
343
|
});
|
|
330
344
|
|
|
345
|
+
it('draws the thin material before iOS 26, as the buttons do', async () => {
|
|
346
|
+
const { host, glass, update } = await renderGroup(IOS_18);
|
|
347
|
+
await update(() => host.variant.set('glass'));
|
|
348
|
+
|
|
349
|
+
expect(glass().getAttribute('blur-material')).toBe('thin');
|
|
350
|
+
expect(glass().getAttribute('blur-effect')).toBe('extra-light');
|
|
351
|
+
});
|
|
352
|
+
|
|
331
353
|
it('highlights a pressed item in corners concentric with the glass', async () => {
|
|
332
354
|
const { host, item, getByText, update } = await renderGroup(IOS_26);
|
|
333
355
|
await update(() => host.variant.set('glass'));
|
|
@@ -426,12 +448,9 @@ describe('UiButtonGroup glass variants in Material (Android)', () => {
|
|
|
426
448
|
});
|
|
427
449
|
|
|
428
450
|
expect(glass()).toBeNull();
|
|
429
|
-
|
|
430
|
-
expect(group().classList).toContain(name);
|
|
431
|
-
}
|
|
451
|
+
expect(group().classList).toContain('gap-0.5');
|
|
432
452
|
expect(group().classList).not.toContain('bg-border');
|
|
433
453
|
expect(item('Copy').classList).toContain('bg-secondary');
|
|
434
|
-
expect(item('Copy').classList).toContain('rounded-[8px]');
|
|
435
454
|
expect(getByText('Copy').classList).toContain('text-secondary-foreground');
|
|
436
455
|
expect(icon().getAttribute('content')).toContain(
|
|
437
456
|
ICON_LIGHT_COLORS['secondary-foreground'],
|
|
@@ -440,14 +459,39 @@ describe('UiButtonGroup glass variants in Material (Android)', () => {
|
|
|
440
459
|
// Clear glass is the same tonal group here.
|
|
441
460
|
await update(() => host.variant.set('glass-clear'));
|
|
442
461
|
expect(item('Copy').classList).toContain('bg-secondary');
|
|
462
|
+
});
|
|
443
463
|
|
|
444
|
-
|
|
445
|
-
|
|
464
|
+
it("rounds the group's ends on its end items, so Increase Contrast's outline follows them", async () => {
|
|
465
|
+
const { host, group, item, update } = await renderGroup(ANDROID);
|
|
466
|
+
await update(() => {
|
|
467
|
+
host.variant.set('glass');
|
|
468
|
+
settings.increaseContrast.set(true);
|
|
469
|
+
});
|
|
470
|
+
// A row is a capsule: half an item round at its ends, small between.
|
|
471
|
+
expect(cornersOf(item('Copy'))).toEqual(['20px', '8px', '8px', '20px']);
|
|
472
|
+
expect(cornersOf(item('Paste'))).toEqual(['8px', '20px', '20px', '8px']);
|
|
473
|
+
expect(item('Copy').classList).toContain('border-foreground');
|
|
474
|
+
// The group doesn't clip the ends, which would cut their outlines.
|
|
446
475
|
expect(group().classList).not.toContain('rounded-full');
|
|
447
|
-
expect(group().
|
|
448
|
-
await update(() => host.size.set('icon'));
|
|
449
|
-
expect(group().classList).toContain('rounded-full');
|
|
476
|
+
expect(group().classList).not.toContain('overflow-hidden');
|
|
450
477
|
expect(group().style.borderRadius).toBe('');
|
|
478
|
+
|
|
479
|
+
// An item added between them has small corners all round.
|
|
480
|
+
await update(() => host.withCut.set(true));
|
|
481
|
+
expect(cornersOf(item('Cut'))).toEqual(['8px', '8px', '8px', '8px']);
|
|
482
|
+
expect(cornersOf(item('Paste'))).toEqual(['8px', '20px', '20px', '8px']);
|
|
483
|
+
|
|
484
|
+
// A column rounds its top and bottom, as much as its items are tall.
|
|
485
|
+
await update(() => {
|
|
486
|
+
host.orientation.set('vertical');
|
|
487
|
+
host.size.set('sm');
|
|
488
|
+
});
|
|
489
|
+
expect(cornersOf(item('Copy'))).toEqual(['18px', '18px', '8px', '8px']);
|
|
490
|
+
expect(cornersOf(item('Paste'))).toEqual(['8px', '8px', '18px', '18px']);
|
|
491
|
+
|
|
492
|
+
// The other looks round nothing per item.
|
|
493
|
+
await update(() => host.variant.set('default'));
|
|
494
|
+
expect(cornersOf(item('Copy'))).toEqual(['', '', '', '']);
|
|
451
495
|
});
|
|
452
496
|
|
|
453
497
|
it('shows the state layer on a pressed item', async () => {
|
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
type ElementRef,
|
|
4
4
|
Component,
|
|
5
5
|
ViewEncapsulation,
|
|
6
|
+
booleanAttribute,
|
|
6
7
|
computed,
|
|
7
8
|
contentChild,
|
|
9
|
+
contentChildren,
|
|
8
10
|
inject,
|
|
9
11
|
input,
|
|
10
12
|
output,
|
|
@@ -33,10 +35,9 @@ import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
|
33
35
|
* Per-item styling of the solid variants. Corners are intentionally left
|
|
34
36
|
* square here: the group container clips the whole bar with `overflow-hidden`
|
|
35
37
|
* + `rounded-md`, so only the outer edges round off while every internal seam
|
|
36
|
-
* stays crisp
|
|
37
|
-
* `:first-child`/`:last-child`
|
|
38
|
-
*
|
|
39
|
-
* CLAUDE.md).
|
|
38
|
+
* stays crisp, and the items need no positional styling (Lynx's CSS engine
|
|
39
|
+
* has no `:first-child`/`:last-child`). Their outline is the container's
|
|
40
|
+
* border, which the clip doesn't cut.
|
|
40
41
|
*/
|
|
41
42
|
export const buttonGroupItemVariants = cva('items-center flex justify-center', {
|
|
42
43
|
variants: {
|
|
@@ -136,6 +137,11 @@ const ITEM_SIZES: Readonly<Record<ButtonGroupSize, string>> = {
|
|
|
136
137
|
*/
|
|
137
138
|
const iconType = () => UiIcon;
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* `UiButtonGroupItem`, for the group's `contentChildren()` (see iconType).
|
|
142
|
+
*/
|
|
143
|
+
const itemType = () => UiButtonGroupItem;
|
|
144
|
+
|
|
139
145
|
/**
|
|
140
146
|
* The color an icon projected into an item draws with when it's given none:
|
|
141
147
|
* the items' label color (see UI_ICON_COLOR). Named, not inline: the
|
|
@@ -171,13 +177,15 @@ const groupIconColor = () => inject(UiButtonGroup).foreground;
|
|
|
171
177
|
template: `
|
|
172
178
|
@if (look() === 'glass') {
|
|
173
179
|
<!-- The frame is the group's box and, under clear glass, its dimming
|
|
174
|
-
layer, in the glass's shape.
|
|
180
|
+
layer, in the glass's shape. Before iOS 26 the glass is the thin
|
|
181
|
+
material, as the buttons' is. -->
|
|
175
182
|
<view [class]="frameClass()" [style.border-radius.px]="cornerRadius()">
|
|
176
183
|
<blur-view
|
|
177
184
|
ui-glass
|
|
178
185
|
[shape]="capsule() ? 'capsule' : 'rounded'"
|
|
179
186
|
[radius]="radius()"
|
|
180
187
|
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
188
|
+
material="thin"
|
|
181
189
|
[interactive]="interactive()"
|
|
182
190
|
[class]="glassClass()"
|
|
183
191
|
>
|
|
@@ -185,10 +193,7 @@ const groupIconColor = () => inject(UiButtonGroup).foreground;
|
|
|
185
193
|
</blur-view>
|
|
186
194
|
</view>
|
|
187
195
|
} @else {
|
|
188
|
-
<view
|
|
189
|
-
[class]="containerClass()"
|
|
190
|
-
[style.border-radius.px]="materialRadius()"
|
|
191
|
-
>
|
|
196
|
+
<view [class]="containerClass()">
|
|
192
197
|
<ng-container [ngTemplateOutlet]="items" />
|
|
193
198
|
</view>
|
|
194
199
|
}
|
|
@@ -207,13 +212,16 @@ export class UiButtonGroup {
|
|
|
207
212
|
readonly variant = input<ButtonGroupVariant>('default');
|
|
208
213
|
/** The items' height and padding; `'icon'` makes square icon-only items. */
|
|
209
214
|
readonly size = input<ButtonGroupSize>('default');
|
|
210
|
-
readonly disabled = input(false);
|
|
215
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
211
216
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
212
217
|
|
|
213
218
|
readonly #settings = inject(LynxAccessibilitySettings);
|
|
214
219
|
readonly #design = injectGlassDesign();
|
|
215
220
|
readonly #systemDrawsGlass = injectSystemDrawsGlass();
|
|
216
221
|
|
|
222
|
+
/** The items in the group, in order. */
|
|
223
|
+
readonly items = contentChildren(itemType(), { descendants: true });
|
|
224
|
+
|
|
217
225
|
/** The form the group takes on this platform, for its items. */
|
|
218
226
|
readonly look = computed<ButtonGroupLook>(() => {
|
|
219
227
|
const variant = this.variant();
|
|
@@ -234,7 +242,8 @@ export class UiButtonGroup {
|
|
|
234
242
|
* The corner radius of an item's pressed highlight, in px. In glass it's
|
|
235
243
|
* concentric with the group's corners, so the gap between them stays even
|
|
236
244
|
* all the way round; in Material's connected group it's the small inner
|
|
237
|
-
* corner, and the group's
|
|
245
|
+
* corner, and an item at one of the group's ends clips it to its round
|
|
246
|
+
* end (see materialEndRadius).
|
|
238
247
|
*/
|
|
239
248
|
readonly itemRadius = computed(() =>
|
|
240
249
|
this.look() === 'material'
|
|
@@ -257,12 +266,13 @@ export class UiButtonGroup {
|
|
|
257
266
|
this.capsule() ? null : this.radius(),
|
|
258
267
|
);
|
|
259
268
|
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
/**
|
|
270
|
+
* How round Material's group is at its ends, in px: half an item's
|
|
271
|
+
* height, which makes a capsule of a row or of a column of square icons,
|
|
272
|
+
* and round corners on a column of wider items. The items at the ends
|
|
273
|
+
* draw them (see UiButtonGroupItem's cornerStyle).
|
|
274
|
+
*/
|
|
275
|
+
readonly materialEndRadius = computed(() => ITEM_HEIGHTS[this.size()] / 2);
|
|
266
276
|
|
|
267
277
|
/**
|
|
268
278
|
* As UiButton's: the glass reacts to touch, unless Reduce Motion is on
|
|
@@ -322,8 +332,9 @@ export class UiButtonGroup {
|
|
|
322
332
|
cn(
|
|
323
333
|
this.look() === 'material'
|
|
324
334
|
? // Material's connected group: the items, 2px apart, show the page
|
|
325
|
-
// between them
|
|
326
|
-
|
|
335
|
+
// between them. No clip: the end items round the group's ends
|
|
336
|
+
// themselves, and a clip would cut their outlines.
|
|
337
|
+
'gap-0.5 flex'
|
|
327
338
|
: // The dividers between items are the group's own `bg-border`
|
|
328
339
|
// showing through a 1px `gap-px` seam — no per-item borders needed,
|
|
329
340
|
// which keeps items position-agnostic. `overflow-hidden` +
|
|
@@ -349,6 +360,7 @@ export class UiButtonGroup {
|
|
|
349
360
|
<view
|
|
350
361
|
#container
|
|
351
362
|
[class]="containerClass()"
|
|
363
|
+
[style]="cornerStyle()"
|
|
352
364
|
[attr.accessibility-element]="accessibilityLabel() ? 'true' : null"
|
|
353
365
|
[attr.accessibility-traits]="accessibilityLabel() ? 'button' : null"
|
|
354
366
|
[attr.accessibility-label]="accessibilityLabel() || null"
|
|
@@ -378,7 +390,7 @@ export class UiButtonGroupItem {
|
|
|
378
390
|
readonly #group = inject(UiButtonGroup);
|
|
379
391
|
readonly #settings = inject(LynxAccessibilitySettings);
|
|
380
392
|
|
|
381
|
-
readonly disabled = input(false);
|
|
393
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
382
394
|
/**
|
|
383
395
|
* What a screen reader says for the item. An icon-only item needs one; an
|
|
384
396
|
* item with a label reads its label without it.
|
|
@@ -405,6 +417,40 @@ export class UiButtonGroupItem {
|
|
|
405
417
|
|
|
406
418
|
protected readonly radius = computed(() => this.#group.itemRadius());
|
|
407
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Whether the item is the group's first and its last, which round the
|
|
422
|
+
* group's ends in Material. Found from the group's items, as Lynx's CSS
|
|
423
|
+
* has no `:first-child`/`:last-child`.
|
|
424
|
+
*/
|
|
425
|
+
readonly #first = computed(() => this.#group.items()[0] === this);
|
|
426
|
+
readonly #last = computed(() => {
|
|
427
|
+
const items = this.#group.items();
|
|
428
|
+
return items[items.length - 1] === this;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A Material item's corners: small between items, and round at the
|
|
433
|
+
* group's ends, as Material's connected buttons are. The end items draw
|
|
434
|
+
* the ends themselves, rather than the group clipping them, so that the
|
|
435
|
+
* outline Increase Contrast gives them follows the curve. Inline, as how
|
|
436
|
+
* round the ends are depends on the size.
|
|
437
|
+
*/
|
|
438
|
+
protected readonly cornerStyle = computed(() => {
|
|
439
|
+
if (this.#group.look() !== 'material') return null;
|
|
440
|
+
const inner = `${MATERIAL_INNER_RADIUS}px`;
|
|
441
|
+
const end = `${this.#group.materialEndRadius()}px`;
|
|
442
|
+
// The left or top corners, and the right or bottom ones.
|
|
443
|
+
const start = this.#first() ? end : inner;
|
|
444
|
+
const finish = this.#last() ? end : inner;
|
|
445
|
+
const column = this.#group.orientation() === 'vertical';
|
|
446
|
+
return {
|
|
447
|
+
'border-top-left-radius': start,
|
|
448
|
+
'border-top-right-radius': column ? start : finish,
|
|
449
|
+
'border-bottom-right-radius': finish,
|
|
450
|
+
'border-bottom-left-radius': column ? finish : start,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
|
|
408
454
|
/**
|
|
409
455
|
* Glass and Material items show a highlight while pressed, in place: the
|
|
410
456
|
* glass around them is what reacts on iOS, and Material marks a press with
|
|
@@ -439,7 +485,7 @@ export class UiButtonGroupItem {
|
|
|
439
485
|
ITEM_SIZES[size],
|
|
440
486
|
look === 'material' &&
|
|
441
487
|
cn(
|
|
442
|
-
'
|
|
488
|
+
'bg-secondary overflow-hidden',
|
|
443
489
|
this.#settings.increaseContrast() && 'border border-foreground',
|
|
444
490
|
),
|
|
445
491
|
this.#gap(),
|
|
@@ -40,6 +40,12 @@ class CardHost {
|
|
|
40
40
|
readonly pressable = signal(false);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
@Component({
|
|
44
|
+
imports: [UiCard, LYNX_ELEMENTS],
|
|
45
|
+
template: `<ui-card pressable><text>Order</text></ui-card>`,
|
|
46
|
+
})
|
|
47
|
+
class BarePressableHost {}
|
|
48
|
+
|
|
43
49
|
/**
|
|
44
50
|
* The card's own view. Each component's host element renders as a view too,
|
|
45
51
|
* so it's found by the card's class rather than by being the first.
|
|
@@ -112,6 +118,16 @@ describe('UiCard', () => {
|
|
|
112
118
|
expect(scaleOf(view)).toBe('scale(1)');
|
|
113
119
|
});
|
|
114
120
|
|
|
121
|
+
it('is pressable with a bare pressable attribute', async () => {
|
|
122
|
+
const { container } = await render(BarePressableHost);
|
|
123
|
+
const view = cardView(container);
|
|
124
|
+
|
|
125
|
+
fireEvent.touchstart(view);
|
|
126
|
+
expect(scaleOf(view)).toBe('scale(0.985)');
|
|
127
|
+
fireEvent.touchend(view);
|
|
128
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
129
|
+
});
|
|
130
|
+
|
|
115
131
|
it('scales down while pressed by mouse, and back on release or leave', async () => {
|
|
116
132
|
const { container, componentRef } = await render(CardHost);
|
|
117
133
|
(componentRef.instance as CardHost).pressable.set(true);
|
|
@@ -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,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,
|
|
@@ -193,9 +193,9 @@ export abstract class UiCartesianPreset<TDatum> {
|
|
|
193
193
|
/** Target number of ticks per axis; the axis picks nice values near it. */
|
|
194
194
|
readonly tickCount = input(5);
|
|
195
195
|
/** Horizontal gridlines at the y ticks. */
|
|
196
|
-
readonly showGrid = input(true);
|
|
196
|
+
readonly showGrid = input(true, { transform: booleanAttribute });
|
|
197
197
|
/** Vertical gridlines at the x ticks. */
|
|
198
|
-
readonly showXGrid = input(false);
|
|
198
|
+
readonly showXGrid = input(false, { transform: booleanAttribute });
|
|
199
199
|
readonly xAxisLabel = input('');
|
|
200
200
|
readonly yAxisLabel = input('');
|
|
201
201
|
readonly padding = input<ChartPadding>({});
|
|
@@ -206,14 +206,14 @@ export abstract class UiCartesianPreset<TDatum> {
|
|
|
206
206
|
readonly xTickFormat = input<(value: number) => string>();
|
|
207
207
|
readonly yTickFormat = input<(value: number) => string>();
|
|
208
208
|
/** Shows a tooltip for the touched point; a tap pins it. */
|
|
209
|
-
readonly tooltip = input(true);
|
|
209
|
+
readonly tooltip = input(true, { transform: booleanAttribute });
|
|
210
210
|
/** What a screen reader announces for the chart. */
|
|
211
211
|
readonly label = input('Chart');
|
|
212
212
|
/** Light, dark or `'system'` colors; the chart theme's setting when unset. */
|
|
213
213
|
readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
|
|
214
|
-
readonly zoomable = input(false);
|
|
214
|
+
readonly zoomable = input(false, { transform: booleanAttribute });
|
|
215
215
|
readonly zoomAxes = input<ChartZoomAxes>('xy');
|
|
216
|
-
readonly showZoomControls = input(true);
|
|
216
|
+
readonly showZoomControls = input(true, { transform: booleanAttribute });
|
|
217
217
|
readonly minZoom = input(1);
|
|
218
218
|
readonly maxZoom = input(8);
|
|
219
219
|
readonly zoomStep = input(1.4);
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '@tanstack/charts/universal';
|
|
19
19
|
import { describe, expect, it, vi } from 'vitest';
|
|
20
20
|
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
21
|
+
import { UI_THEME_COLORS } from '../icon/icon-theme';
|
|
21
22
|
import { UiChart, UiChartTooltip, tooltip } from './chart';
|
|
22
23
|
import {
|
|
23
24
|
CHART_DARK_COLORS,
|
|
@@ -337,6 +338,19 @@ describe('UiChart', () => {
|
|
|
337
338
|
await primaryFill([provideChartTheme({ colorScheme: () => 'dark' })]),
|
|
338
339
|
).toBe(CHART_DARK_COLORS['primary']);
|
|
339
340
|
});
|
|
341
|
+
|
|
342
|
+
it('paints with colors set at runtime above it, such as the wallpaper’s', async () => {
|
|
343
|
+
const wallpaper = {
|
|
344
|
+
provide: UI_THEME_COLORS,
|
|
345
|
+
useValue: signal({ dark: { primary: 'rgba(208, 188, 255, 1)' } }),
|
|
346
|
+
};
|
|
347
|
+
expect(await primaryFill([wallpaper], 'dark')).toBe(
|
|
348
|
+
'rgba(208, 188, 255, 1)',
|
|
349
|
+
);
|
|
350
|
+
expect(await primaryFill([wallpaper], 'light')).toBe(
|
|
351
|
+
CHART_LIGHT_COLORS['primary'],
|
|
352
|
+
);
|
|
353
|
+
});
|
|
340
354
|
});
|
|
341
355
|
|
|
342
356
|
it('gives labels the foreground token when the theme sets no foreground', async () => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Directive,
|
|
5
5
|
TemplateRef,
|
|
6
6
|
ViewEncapsulation,
|
|
7
|
+
booleanAttribute,
|
|
7
8
|
computed,
|
|
8
9
|
contentChild,
|
|
9
10
|
effect,
|
|
@@ -46,6 +47,7 @@ import type {
|
|
|
46
47
|
} from '@tanstack/charts/types';
|
|
47
48
|
|
|
48
49
|
import { cn } from '../../utils/cn';
|
|
50
|
+
import { UI_THEME_COLORS } from '../icon/icon-theme';
|
|
49
51
|
import { adjacentFocusPoint, createChartFocusModel } from './chart-focus';
|
|
50
52
|
import {
|
|
51
53
|
type ChartLabel,
|
|
@@ -342,9 +344,9 @@ export class UiChart {
|
|
|
342
344
|
* light unless the app says otherwise; see `ChartColorSchemeOption`.
|
|
343
345
|
*/
|
|
344
346
|
readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
|
|
345
|
-
readonly zoomable = input(false);
|
|
347
|
+
readonly zoomable = input(false, { transform: booleanAttribute });
|
|
346
348
|
readonly zoomAxes = input<ChartZoomAxes>('xy');
|
|
347
|
-
readonly showZoomControls = input(true);
|
|
349
|
+
readonly showZoomControls = input(true, { transform: booleanAttribute });
|
|
348
350
|
readonly minZoom = input(1);
|
|
349
351
|
readonly maxZoom = input(8);
|
|
350
352
|
/** Factor each zoom button press zooms by. */
|
|
@@ -363,6 +365,7 @@ export class UiChart {
|
|
|
363
365
|
|
|
364
366
|
readonly #theme = inject(UI_CHART_THEME);
|
|
365
367
|
readonly #appearance = inject(LynxTheme);
|
|
368
|
+
readonly #replacedColors = inject(UI_THEME_COLORS, { optional: true });
|
|
366
369
|
readonly #idPrefix = `chart${nextChartId++}`;
|
|
367
370
|
readonly #runtime = createChartRuntime({ defaultTheme: this.#theme.chart });
|
|
368
371
|
readonly #measureText = lynxTextMeasurer();
|
|
@@ -468,7 +471,11 @@ export class UiChart {
|
|
|
468
471
|
: typeof option === 'function'
|
|
469
472
|
? option()
|
|
470
473
|
: option;
|
|
471
|
-
|
|
474
|
+
// Colors set at runtime above the chart, such as Android's wallpaper
|
|
475
|
+
// colors, take the place of the theme's (see UI_THEME_COLORS).
|
|
476
|
+
const replaced = this.#replacedColors?.()[scheme];
|
|
477
|
+
const colors = scheme === 'dark' ? this.#theme.dark : this.#theme.light;
|
|
478
|
+
return replaced ? { ...colors, ...replaced } : colors;
|
|
472
479
|
});
|
|
473
480
|
|
|
474
481
|
readonly #svgOptions = computed(() => {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ElementRef,
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
5
6
|
computed,
|
|
6
7
|
effect,
|
|
7
8
|
input,
|
|
@@ -38,7 +39,7 @@ import { cn } from '../../utils/cn';
|
|
|
38
39
|
})
|
|
39
40
|
export class UiCheckbox {
|
|
40
41
|
readonly checked = model(false);
|
|
41
|
-
readonly disabled = input(false);
|
|
42
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
42
43
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
43
44
|
|
|
44
45
|
readonly changed = output<boolean>();
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ElementRef,
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
5
6
|
computed,
|
|
6
7
|
effect,
|
|
7
8
|
inject,
|
|
@@ -27,7 +28,7 @@ import { cn } from '../../utils/cn';
|
|
|
27
28
|
})
|
|
28
29
|
export class UiCollapsible {
|
|
29
30
|
readonly open = model(false);
|
|
30
|
-
readonly disabled = input(false);
|
|
31
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
31
32
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
32
33
|
|
|
33
34
|
protected readonly containerClass = computed(() =>
|