@blotch/dolan 22.2.2 → 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.
- package/dist/commands/add.d.ts +6 -2
- package/dist/commands/add.js +41 -11
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +162 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +228 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +367 -10
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +169 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +392 -27
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +174 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +587 -33
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +78 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +119 -38
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +14 -0
- package/dist/testing/set-input-signal.js +21 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
- package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
- package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
- package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +434 -49
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +335 -0
- package/dist/ui/components/chart/chart-presets.ts +278 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +210 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +934 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +213 -108
- package/dist/ui/components/dialog/dialog.ts +72 -35
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +169 -0
- package/dist/ui/components/icon/icon-theme.ts +227 -0
- package/dist/ui/components/icon/icon.test.ts +85 -25
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +119 -21
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
- package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
- package/dist/ui/components/pagination/pagination.test.ts +177 -65
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +67 -22
- package/dist/ui/components/spinner/spinner.ts +48 -41
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +436 -315
- package/dist/ui/components/toast/toast.ts +62 -55
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +35 -0
- package/dist/ui/theme/dark.css +46 -2
- package/dist/ui/theme/default.css +63 -7
- package/dist/ui/theme/icon-colors.test.ts +35 -0
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +98 -25
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +14 -0
- package/dist/utils/analyze.js +15 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +29 -0
- package/dist/utils/installed.js +43 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +57 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +21 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +38 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +13 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
- package/dist/ui/components/button/button-render.test.ts +0 -113
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
|
@@ -1,60 +1,49 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
3
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import {
|
|
5
|
+
import type { IconName } from '../icon/icons';
|
|
6
6
|
import { UiEmptyState } from './empty-state';
|
|
7
7
|
|
|
8
8
|
@Component({
|
|
9
|
-
standalone: true,
|
|
10
9
|
imports: [UiEmptyState, LYNX_ELEMENTS],
|
|
11
10
|
template: `
|
|
12
11
|
<ui-empty-state
|
|
13
|
-
icon="
|
|
12
|
+
[icon]="icon()"
|
|
14
13
|
title="No results"
|
|
15
|
-
description="
|
|
14
|
+
[description]="description()"
|
|
15
|
+
class="py-6"
|
|
16
16
|
>
|
|
17
|
-
<text>
|
|
17
|
+
<text>Clear the filters</text>
|
|
18
18
|
</ui-empty-state>
|
|
19
19
|
`,
|
|
20
20
|
})
|
|
21
|
-
class EmptyStateHost {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
imports: [UiEmptyState, LYNX_ELEMENTS],
|
|
26
|
-
template: `<ui-empty-state title="Empty"></ui-empty-state>`,
|
|
27
|
-
})
|
|
28
|
-
class MinimalEmptyStateHost {}
|
|
21
|
+
class EmptyStateHost {
|
|
22
|
+
readonly icon = signal<IconName | undefined>(undefined);
|
|
23
|
+
readonly description = signal('');
|
|
24
|
+
}
|
|
29
25
|
|
|
30
26
|
describe('UiEmptyState', () => {
|
|
31
|
-
it('
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(container).toBeTruthy();
|
|
27
|
+
it('shows only the title and projected content by default', async () => {
|
|
28
|
+
const { container, getByText, queryByText } = await render(EmptyStateHost);
|
|
29
|
+
expect(getByText('No results').classList).toContain('font-semibold');
|
|
30
|
+
expect(getByText('Clear the filters')).toBeTruthy();
|
|
31
|
+
expect(queryByText('Try adjusting your search')).toBeNull();
|
|
32
|
+
expect(container.querySelector('svg')).toBeNull();
|
|
33
|
+
expect(container.querySelector('.py-6')).toBeTruthy();
|
|
39
34
|
});
|
|
40
35
|
|
|
41
|
-
it('
|
|
42
|
-
const { container } = await render(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
it('renders the description class when a description is programmatically set', async () => {
|
|
47
|
-
// JIT template bindings don't propagate through `input()`; drive the input
|
|
48
|
-
// via setInputSignal so the @if(description()) branch actually renders and
|
|
49
|
-
// its computed class runs.
|
|
50
|
-
const { componentRef, container } = await render(UiEmptyState);
|
|
51
|
-
const inst = componentRef.instance as UiEmptyState;
|
|
52
|
-
setInputSignal(inst.title, 'Title');
|
|
53
|
-
setInputSignal(inst.description, 'A description');
|
|
36
|
+
it('adds the icon and description when they are set', async () => {
|
|
37
|
+
const { componentRef, container, getByText } = await render(EmptyStateHost);
|
|
38
|
+
const host = componentRef.instance as EmptyStateHost;
|
|
39
|
+
host.icon.set('search');
|
|
40
|
+
host.description.set('Try adjusting your search');
|
|
54
41
|
await waitForUpdate();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
42
|
+
expect(getByText('Try adjusting your search').classList).toContain(
|
|
43
|
+
'text-muted-foreground',
|
|
44
|
+
);
|
|
45
|
+
expect(container.querySelector('svg')?.getAttribute('content')).toContain(
|
|
46
|
+
'<svg',
|
|
47
|
+
);
|
|
59
48
|
});
|
|
60
49
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { concentricRadius } from './concentric-radius';
|
|
3
|
+
|
|
4
|
+
describe('concentricRadius', () => {
|
|
5
|
+
it('takes the inset off the outer radius, so the corners share a center', () => {
|
|
6
|
+
expect(concentricRadius(24, 8)).toBe(16);
|
|
7
|
+
expect(concentricRadius(20, 0)).toBe(20);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('gives square corners rather than a negative radius', () => {
|
|
11
|
+
expect(concentricRadius(8, 12)).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The corner radius for a shape nested `inset` px inside a shape whose corner
|
|
3
|
+
* radius is `outerRadius`, so that the two corners share a center and the gap
|
|
4
|
+
* between them stays the same width all the way round. Apple calls this
|
|
5
|
+
* concentricity, and Liquid Glass relies on it: a button inset 8px in a glass
|
|
6
|
+
* panel with 24px corners gets 16px corners.
|
|
7
|
+
*
|
|
8
|
+
* It never goes below 0: a shape inset further than the outer radius has
|
|
9
|
+
* square corners. Capsules and circles need no helper, because a capsule
|
|
10
|
+
* inset evenly inside a capsule is a capsule (`shape="capsule"`).
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* readonly innerRadius = computed(() => concentricRadius(this.radius(), 8));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const concentricRadius = (outerRadius: number, inset: number): number =>
|
|
17
|
+
Math.max(0, outerRadius - inset);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Injector, runInInjectionContext } from '@angular/core';
|
|
2
|
+
import { LynxSystemInfo } from '@blotch/angular-lynx';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
glassDesignFor,
|
|
6
|
+
injectGlassDesign,
|
|
7
|
+
injectSystemDrawsGlass,
|
|
8
|
+
provideGlass,
|
|
9
|
+
UI_GLASS_OPTIONS,
|
|
10
|
+
} from './glass-design';
|
|
11
|
+
|
|
12
|
+
describe('glassDesignFor', () => {
|
|
13
|
+
it('gives iOS 26 and later Liquid Glass, and earlier iOS the frosted materials', () => {
|
|
14
|
+
expect(glassDesignFor('iOS', '26.0', {})).toBe('glass');
|
|
15
|
+
expect(glassDesignFor('iOS', '27.1', {})).toBe('glass');
|
|
16
|
+
expect(glassDesignFor('iOS', '25.4', {})).toBe('frosted');
|
|
17
|
+
expect(glassDesignFor('iOS', '15.0', {})).toBe('frosted');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('counts an iOS version not known yet as current', () => {
|
|
21
|
+
expect(glassDesignFor('iOS', '', {})).toBe('glass');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('gives Android Material unless the app opts into glass', () => {
|
|
25
|
+
expect(glassDesignFor('Android', '15', {})).toBe('material');
|
|
26
|
+
expect(glassDesignFor('Android', '15', { android: 'glass' })).toBe('glass');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('draws glass in the web preview, and Material where there is none', () => {
|
|
30
|
+
expect(glassDesignFor('web', '', {})).toBe('glass');
|
|
31
|
+
expect(glassDesignFor('Harmony', '5.0', { android: 'glass' })).toBe(
|
|
32
|
+
'material',
|
|
33
|
+
);
|
|
34
|
+
expect(glassDesignFor(undefined, '', {})).toBe('material');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('provideGlass', () => {
|
|
39
|
+
it('provides the options', () => {
|
|
40
|
+
expect(provideGlass({ android: 'glass' })).toEqual({
|
|
41
|
+
provide: UI_GLASS_OPTIONS,
|
|
42
|
+
useValue: { android: 'glass' },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('injectGlassDesign', () => {
|
|
48
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Runs `fn` in an injector. A plain injector has no root scope, so it gets
|
|
52
|
+
* LynxSystemInfo, which the app's root injector would make, explicitly.
|
|
53
|
+
*/
|
|
54
|
+
const inject = <T>(fn: () => T, options = {}) =>
|
|
55
|
+
runInInjectionContext(
|
|
56
|
+
Injector.create({
|
|
57
|
+
providers: [
|
|
58
|
+
LynxSystemInfo,
|
|
59
|
+
{ provide: UI_GLASS_OPTIONS, useValue: options },
|
|
60
|
+
],
|
|
61
|
+
}),
|
|
62
|
+
fn,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it("reads the platform and version from SystemInfo, with the app's options", () => {
|
|
66
|
+
vi.stubGlobal('SystemInfo', { platform: 'Android', osVersion: '14' });
|
|
67
|
+
|
|
68
|
+
expect(inject(injectGlassDesign)()).toBe('material');
|
|
69
|
+
expect(inject(injectGlassDesign, { android: 'glass' })()).toBe('glass');
|
|
70
|
+
expect(inject(injectSystemDrawsGlass)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('knows that iOS draws its own glass', () => {
|
|
74
|
+
vi.stubGlobal('SystemInfo', { platform: 'iOS', osVersion: '26.0' });
|
|
75
|
+
|
|
76
|
+
expect(inject(injectSystemDrawsGlass)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
InjectionToken,
|
|
5
|
+
Injector,
|
|
6
|
+
type Provider,
|
|
7
|
+
type Signal,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { LynxSystemInfo } from '@blotch/angular-lynx';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The look the glass components take on this platform. One API covers all
|
|
13
|
+
* three, and each component builds its own form of each: the dock is a
|
|
14
|
+
* floating glass capsule with `'glass'`, a tab bar on a blur material with
|
|
15
|
+
* `'frosted'`, and a navigation bar with `'material'`.
|
|
16
|
+
*
|
|
17
|
+
* - `'glass'`: Liquid Glass. iOS 26 and later draw it with UIKit's glass,
|
|
18
|
+
* the web preview draws it with WebGL, and Android, when the app opts in
|
|
19
|
+
* with {@link provideGlass}, approximates it with a blur, a tint and a rim.
|
|
20
|
+
* - `'frosted'`: Apple's design before Liquid Glass, for iOS 15 to 25, whose
|
|
21
|
+
* `blur-view` has no glass: frosted blur materials.
|
|
22
|
+
* - `'material'`: Material 3 Expressive, Android's default, which uses
|
|
23
|
+
* opaque tonal surfaces instead of glass.
|
|
24
|
+
*/
|
|
25
|
+
export type GlassDesign = 'glass' | 'frosted' | 'material';
|
|
26
|
+
|
|
27
|
+
export type GlassOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* What Android gets: `'material'` (the default), or `'glass'` for the same
|
|
30
|
+
* look as iOS. Android's glass is a blur with a tint and a rim over it,
|
|
31
|
+
* without the refraction. Blurring costs more below Android 12, where Lynx
|
|
32
|
+
* can't use `RenderEffect`.
|
|
33
|
+
*/
|
|
34
|
+
readonly android?: 'material' | 'glass';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The app's glass options; see {@link provideGlass}. */
|
|
38
|
+
export const UI_GLASS_OPTIONS = new InjectionToken<GlassOptions>(
|
|
39
|
+
'UI_GLASS_OPTIONS',
|
|
40
|
+
{ providedIn: 'root', factory: () => ({}) },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets the glass options for the whole app, in `app.config.ts`:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* // Liquid Glass on Android too, instead of Material.
|
|
48
|
+
* providers: [provideGlass({ android: 'glass' })]
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export const provideGlass = (options: GlassOptions): Provider => ({
|
|
52
|
+
provide: UI_GLASS_OPTIONS,
|
|
53
|
+
useValue: options,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** The first iOS whose `blur-view` draws Liquid Glass. */
|
|
57
|
+
export const FIRST_GLASS_IOS = 26;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The design for a platform, as `SystemInfo` names it, and its OS version.
|
|
61
|
+
*
|
|
62
|
+
* Before iOS 26, Lynx's `blur-view` doesn't know `blur-effect="glass"`: it
|
|
63
|
+
* logs an error and draws a `light` blur that ignores dark mode. So older
|
|
64
|
+
* iOS gets `'frosted'`. An iOS version that isn't known yet (empty) counts as
|
|
65
|
+
* current: on the native main thread it arrives from the background thread
|
|
66
|
+
* after the first frame, unless the host app sends it (see
|
|
67
|
+
* `LynxSystemInfo.osVersion`), and iOS 26 and later is where most phones are.
|
|
68
|
+
* Platforms with no glass, such as HarmonyOS, get `'material'`.
|
|
69
|
+
*/
|
|
70
|
+
export const glassDesignFor = (
|
|
71
|
+
platform: string | undefined,
|
|
72
|
+
osVersion: string,
|
|
73
|
+
options: GlassOptions,
|
|
74
|
+
): GlassDesign => {
|
|
75
|
+
switch (platform) {
|
|
76
|
+
case 'iOS':
|
|
77
|
+
return parseInt(osVersion, 10) < FIRST_GLASS_IOS ? 'frosted' : 'glass';
|
|
78
|
+
case 'Android':
|
|
79
|
+
return options.android ?? 'material';
|
|
80
|
+
case 'web':
|
|
81
|
+
return 'glass';
|
|
82
|
+
default:
|
|
83
|
+
return 'material';
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* LynxSystemInfo, or undefined where there's no SystemInfo to read, such as
|
|
89
|
+
* in unit tests, where LynxSystemInfo throws.
|
|
90
|
+
*/
|
|
91
|
+
const systemInfo = (injector: Injector): LynxSystemInfo | undefined => {
|
|
92
|
+
try {
|
|
93
|
+
return injector.get(LynxSystemInfo);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The glass design on this platform, for a component's constructor or field
|
|
101
|
+
* initializers. A signal: on the native main thread, iOS's version can arrive
|
|
102
|
+
* after the first frame.
|
|
103
|
+
*/
|
|
104
|
+
export const injectGlassDesign = (): Signal<GlassDesign> => {
|
|
105
|
+
const options = inject(UI_GLASS_OPTIONS);
|
|
106
|
+
const info = systemInfo(inject(Injector));
|
|
107
|
+
return computed(() =>
|
|
108
|
+
glassDesignFor(info?.platform, info?.osVersion ?? '', options),
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether the system draws the glass and its materials itself: iOS. UIKit
|
|
114
|
+
* adapts them to Reduce Transparency, Increase Contrast and iOS 27's Liquid
|
|
115
|
+
* Glass setting on its own. Elsewhere, Android's approximation and the web
|
|
116
|
+
* preview's drawing can't, so the components adapt what they draw.
|
|
117
|
+
*/
|
|
118
|
+
export const injectSystemDrawsGlass = (): boolean =>
|
|
119
|
+
systemInfo(inject(Injector))?.platform === 'iOS';
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Component, type Provider, signal } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { UiIcon } from '../icon/icon';
|
|
6
|
+
import {
|
|
7
|
+
ICON_DARK_COLORS,
|
|
8
|
+
ICON_LIGHT_COLORS,
|
|
9
|
+
provideIconTheme,
|
|
10
|
+
} from '../icon/icon-theme';
|
|
11
|
+
import {
|
|
12
|
+
UiGlass,
|
|
13
|
+
UiGlassGroup,
|
|
14
|
+
type GlassShape,
|
|
15
|
+
type GlassVariant,
|
|
16
|
+
} from './glass';
|
|
17
|
+
import { provideGlass } from './glass-design';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
imports: [LYNX_ELEMENTS, UiGlass, UiGlassGroup, UiIcon],
|
|
21
|
+
template: `
|
|
22
|
+
<blur-view ui-glass-group [spacing]="spacing()" class="flex-row gap-2">
|
|
23
|
+
<blur-view
|
|
24
|
+
ui-glass
|
|
25
|
+
id="glass"
|
|
26
|
+
class="px-4 py-2"
|
|
27
|
+
[variant]="variant()"
|
|
28
|
+
[shape]="shape()"
|
|
29
|
+
[radius]="radius()"
|
|
30
|
+
[tint]="tint()"
|
|
31
|
+
[interactive]="interactive()"
|
|
32
|
+
>
|
|
33
|
+
<text class="text-glass-foreground">Now playing</text>
|
|
34
|
+
<ui-icon name="play" />
|
|
35
|
+
</blur-view>
|
|
36
|
+
</blur-view>
|
|
37
|
+
`,
|
|
38
|
+
})
|
|
39
|
+
class GlassHost {
|
|
40
|
+
readonly spacing = signal(12);
|
|
41
|
+
readonly variant = signal<GlassVariant>('regular');
|
|
42
|
+
readonly shape = signal<GlassShape>('capsule');
|
|
43
|
+
readonly radius = signal(20);
|
|
44
|
+
readonly tint = signal<string | undefined>(undefined);
|
|
45
|
+
readonly interactive = signal(false);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
49
|
+
const settings = {
|
|
50
|
+
reduceMotion: signal(false),
|
|
51
|
+
reduceTransparency: signal(false),
|
|
52
|
+
increaseContrast: signal(false),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Renders the host on a platform, as `SystemInfo` reports it, or with no
|
|
57
|
+
* SystemInfo at all.
|
|
58
|
+
*/
|
|
59
|
+
const renderOn = async (
|
|
60
|
+
platform: { platform: string; osVersion: string } | undefined,
|
|
61
|
+
providers: Provider[] = [],
|
|
62
|
+
) => {
|
|
63
|
+
vi.stubGlobal('SystemInfo', platform);
|
|
64
|
+
const result = await render(GlassHost, {
|
|
65
|
+
providers: [
|
|
66
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
67
|
+
...providers,
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const host = result.componentRef.instance as GlassHost;
|
|
71
|
+
const glass = result.container.querySelector('#glass')!;
|
|
72
|
+
const group = glass.parentElement!;
|
|
73
|
+
const update = async (change: () => void) => {
|
|
74
|
+
change();
|
|
75
|
+
await waitForUpdate();
|
|
76
|
+
};
|
|
77
|
+
return { ...result, host, glass, group, update };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
81
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
82
|
+
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
83
|
+
const WEB = { platform: 'web', osVersion: '' };
|
|
84
|
+
|
|
85
|
+
// The icon's selector isn't a Lynx element, so the renderer falls back to a
|
|
86
|
+
// <view> for it and warns.
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('UiGlass', () => {
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.restoreAllMocks();
|
|
94
|
+
vi.unstubAllGlobals();
|
|
95
|
+
settings.reduceTransparency.set(false);
|
|
96
|
+
settings.increaseContrast.set(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('draws Liquid Glass on iOS 26, in the app color scheme', async () => {
|
|
100
|
+
const { glass } = await renderOn(IOS_26);
|
|
101
|
+
|
|
102
|
+
expect(glass.tagName.toLowerCase()).toBe('blur-view');
|
|
103
|
+
expect(glass.getAttribute('blur-effect')).toBe('glass');
|
|
104
|
+
expect(glass.getAttribute('glass-style')).toBe('regular');
|
|
105
|
+
expect(glass.getAttribute('blur-radius')).toBe('16px');
|
|
106
|
+
expect(glass.getAttribute('enable-auto-blur')).toBe('true');
|
|
107
|
+
expect(glass.getAttribute('ios-user-interface-style')).toBe('light');
|
|
108
|
+
expect(glass.hasAttribute('glass-tint-color')).toBe(false);
|
|
109
|
+
expect(glass.hasAttribute('glass-interactive')).toBe(false);
|
|
110
|
+
// The app's own classes stay on the glass, beside its shape.
|
|
111
|
+
expect(glass.classList).toContain('px-4');
|
|
112
|
+
expect(glass.classList).toContain('rounded-full');
|
|
113
|
+
expect(glass.classList).not.toContain('bg-secondary');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('keeps its content inside the glass', async () => {
|
|
117
|
+
const { glass } = await renderOn(IOS_26);
|
|
118
|
+
|
|
119
|
+
expect(glass.querySelector('text')?.textContent).toBe('Now playing');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('takes the variant, tint and interactivity', async () => {
|
|
123
|
+
const { glass, host, update } = await renderOn(IOS_26);
|
|
124
|
+
|
|
125
|
+
await update(() => {
|
|
126
|
+
host.variant.set('clear');
|
|
127
|
+
host.tint.set('#2563eb');
|
|
128
|
+
host.interactive.set(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(glass.getAttribute('glass-style')).toBe('clear');
|
|
132
|
+
expect(glass.getAttribute('blur-radius')).toBe('6px');
|
|
133
|
+
expect(glass.getAttribute('glass-tint-color')).toBe('#2563eb');
|
|
134
|
+
expect(glass.getAttribute('glass-interactive')).toBe('true');
|
|
135
|
+
|
|
136
|
+
await update(() => host.interactive.set(false));
|
|
137
|
+
expect(glass.hasAttribute('glass-interactive')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("resolves a theme token tint to the scheme's value, and forces the scheme", async () => {
|
|
141
|
+
const scheme = signal<'light' | 'dark'>('light');
|
|
142
|
+
const { glass, host, update } = await renderOn(IOS_26, [
|
|
143
|
+
provideIconTheme({ colorScheme: () => scheme() }),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
await update(() => host.tint.set('var(--glass-tint)'));
|
|
147
|
+
expect(glass.getAttribute('glass-tint-color')).toBe(
|
|
148
|
+
ICON_LIGHT_COLORS['glass-tint'],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await update(() => scheme.set('dark'));
|
|
152
|
+
expect(glass.getAttribute('glass-tint-color')).toBe(
|
|
153
|
+
ICON_DARK_COLORS['glass-tint'],
|
|
154
|
+
);
|
|
155
|
+
expect(glass.getAttribute('ios-user-interface-style')).toBe('dark');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('turns the foreground white on clear glass, which sits over media', async () => {
|
|
159
|
+
const { glass, host, update } = await renderOn(IOS_26);
|
|
160
|
+
const icon = () => glass.querySelector('svg')!.getAttribute('content');
|
|
161
|
+
|
|
162
|
+
expect(icon()).toContain(ICON_LIGHT_COLORS['glass-foreground']);
|
|
163
|
+
|
|
164
|
+
await update(() => host.variant.set('clear'));
|
|
165
|
+
|
|
166
|
+
expect(glass.classList).toContain(
|
|
167
|
+
'[--glass-foreground:var(--glass-clear-foreground)]',
|
|
168
|
+
);
|
|
169
|
+
expect(glass.classList).toContain(
|
|
170
|
+
'[--glass-muted-foreground:var(--glass-clear-foreground)]',
|
|
171
|
+
);
|
|
172
|
+
expect(icon()).toContain(ICON_LIGHT_COLORS['glass-clear-foreground']);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('gives a rounded shape its radius, and capsules and circles round ends', async () => {
|
|
176
|
+
const { glass, host, update } = await renderOn(IOS_26);
|
|
177
|
+
const radius = () =>
|
|
178
|
+
(glass as HTMLElement).style.getPropertyValue('border-radius');
|
|
179
|
+
|
|
180
|
+
expect(radius()).toBe('');
|
|
181
|
+
|
|
182
|
+
await update(() => {
|
|
183
|
+
host.shape.set('rounded');
|
|
184
|
+
host.radius.set(24);
|
|
185
|
+
});
|
|
186
|
+
expect(glass.classList).not.toContain('rounded-full');
|
|
187
|
+
expect(radius()).toBe('24px');
|
|
188
|
+
|
|
189
|
+
await update(() => host.shape.set('circle'));
|
|
190
|
+
expect(glass.classList).toContain('rounded-full');
|
|
191
|
+
expect(radius()).toBe('');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('leaves Reduce Transparency and Increase Contrast to iOS, except for muted text', async () => {
|
|
195
|
+
const { glass, update } = await renderOn(IOS_26);
|
|
196
|
+
|
|
197
|
+
await update(() => {
|
|
198
|
+
settings.reduceTransparency.set(true);
|
|
199
|
+
settings.increaseContrast.set(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// UIKit adapts its own glass: no opaque surface or border here.
|
|
203
|
+
expect(glass.getAttribute('blur-effect')).toBe('glass');
|
|
204
|
+
expect(glass.classList).not.toContain('border');
|
|
205
|
+
expect(glass.classList).toContain(
|
|
206
|
+
'[--glass-muted-foreground:var(--glass-foreground)]',
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('draws a frosted material before iOS 26, which has no glass', async () => {
|
|
211
|
+
const scheme = signal<'light' | 'dark'>('light');
|
|
212
|
+
const { glass, host, update } = await renderOn(IOS_18, [
|
|
213
|
+
provideIconTheme({ colorScheme: () => scheme() }),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
expect(glass.getAttribute('blur-effect')).toBe('extra-light');
|
|
217
|
+
expect(glass.getAttribute('blur-radius')).toBe('20px');
|
|
218
|
+
|
|
219
|
+
await update(() => scheme.set('dark'));
|
|
220
|
+
expect(glass.getAttribute('blur-effect')).toBe('dark');
|
|
221
|
+
|
|
222
|
+
await update(() => {
|
|
223
|
+
scheme.set('light');
|
|
224
|
+
host.variant.set('clear');
|
|
225
|
+
});
|
|
226
|
+
expect(glass.getAttribute('blur-effect')).toBe('dark');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('fills a frosted material with its tint, under the content', async () => {
|
|
230
|
+
const { glass, host, update } = await renderOn(IOS_18);
|
|
231
|
+
/**
|
|
232
|
+
* The fill: the view that draws a background, before the content.
|
|
233
|
+
*/
|
|
234
|
+
const fill = () =>
|
|
235
|
+
Array.from(glass.children).find(
|
|
236
|
+
(child) => (child as HTMLElement).style.backgroundColor,
|
|
237
|
+
) as HTMLElement;
|
|
238
|
+
|
|
239
|
+
expect(fill()).toBeUndefined();
|
|
240
|
+
|
|
241
|
+
await update(() => host.tint.set('#2563eb'));
|
|
242
|
+
// Drawn first, so the content sits on it.
|
|
243
|
+
expect(Array.from(glass.children).indexOf(fill())).toBeLessThan(
|
|
244
|
+
Array.from(glass.children).indexOf(glass.querySelector('text')!),
|
|
245
|
+
);
|
|
246
|
+
expect(fill().style.getPropertyValue('background-color')).toBe(
|
|
247
|
+
'rgb(37, 99, 235)',
|
|
248
|
+
);
|
|
249
|
+
expect(fill().classList).toContain('rounded-full');
|
|
250
|
+
|
|
251
|
+
await update(() => host.shape.set('rounded'));
|
|
252
|
+
expect(fill().classList).not.toContain('rounded-full');
|
|
253
|
+
expect(fill().style.getPropertyValue('border-radius')).toBe('20px');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('draws an opaque Material surface on Android by default', async () => {
|
|
257
|
+
const { glass, group, host, update } = await renderOn(ANDROID);
|
|
258
|
+
|
|
259
|
+
expect(glass.getAttribute('blur-effect')).toBe('light');
|
|
260
|
+
expect(glass.getAttribute('blur-radius')).toBe('0px');
|
|
261
|
+
// A blur-view redraws what's behind it on every frame on Android, even
|
|
262
|
+
// without a radius, unless this is off.
|
|
263
|
+
expect(glass.getAttribute('enable-auto-blur')).toBe('false');
|
|
264
|
+
expect(glass.classList).toContain('bg-secondary');
|
|
265
|
+
expect(group.getAttribute('blur-effect')).toBe('light');
|
|
266
|
+
|
|
267
|
+
// Clear glass is opaque here too, so its foreground stays dark.
|
|
268
|
+
await update(() => host.variant.set('clear'));
|
|
269
|
+
expect(glass.classList).not.toContain(
|
|
270
|
+
'[--glass-foreground:var(--glass-clear-foreground)]',
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// A tint fills the surface, as Material fills a button.
|
|
274
|
+
await update(() => host.tint.set('#2563eb'));
|
|
275
|
+
expect(glass.classList).not.toContain('bg-secondary');
|
|
276
|
+
expect(
|
|
277
|
+
(glass as HTMLElement).style.getPropertyValue('background-color'),
|
|
278
|
+
).toBe('rgb(37, 99, 235)');
|
|
279
|
+
expect(
|
|
280
|
+
(glass as HTMLElement).style.getPropertyValue('box-shadow'),
|
|
281
|
+
).not.toBe('');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('draws glass on Android when the app opts in, and follows the settings there', async () => {
|
|
285
|
+
const { glass, update } = await renderOn(ANDROID, [
|
|
286
|
+
provideGlass({ android: 'glass' }),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
expect(glass.getAttribute('blur-effect')).toBe('glass');
|
|
290
|
+
expect(glass.classList).not.toContain('bg-secondary');
|
|
291
|
+
|
|
292
|
+
await update(() => settings.increaseContrast.set(true));
|
|
293
|
+
expect(glass.classList).toContain('border');
|
|
294
|
+
expect(glass.classList).toContain('border-glass-foreground');
|
|
295
|
+
|
|
296
|
+
await update(() => settings.reduceTransparency.set(true));
|
|
297
|
+
expect(glass.getAttribute('blur-effect')).toBe('light');
|
|
298
|
+
expect(glass.classList).toContain('bg-secondary');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('draws glass in the web preview', async () => {
|
|
302
|
+
const { glass } = await renderOn(WEB);
|
|
303
|
+
|
|
304
|
+
expect(glass.getAttribute('blur-effect')).toBe('glass');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('draws a Material surface where there is no SystemInfo to read', async () => {
|
|
308
|
+
const { glass } = await renderOn(undefined);
|
|
309
|
+
|
|
310
|
+
expect(glass.classList).toContain('bg-secondary');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('UiGlassGroup', () => {
|
|
315
|
+
afterEach(() => {
|
|
316
|
+
vi.restoreAllMocks();
|
|
317
|
+
vi.unstubAllGlobals();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('merges the glass in it within spacing', async () => {
|
|
321
|
+
const { group, host, update } = await renderOn(IOS_26);
|
|
322
|
+
|
|
323
|
+
expect(group.getAttribute('blur-effect')).toBe('glass-container');
|
|
324
|
+
expect(group.getAttribute('spacing')).toBe('12');
|
|
325
|
+
expect(group.getAttribute('enable-auto-blur')).toBe('false');
|
|
326
|
+
expect(group.classList).toContain('flex-row');
|
|
327
|
+
|
|
328
|
+
await update(() => host.spacing.set(24));
|
|
329
|
+
expect(group.getAttribute('spacing')).toBe('24');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('is a plain container where there is no glass to merge', async () => {
|
|
333
|
+
const { group } = await renderOn(IOS_18);
|
|
334
|
+
|
|
335
|
+
expect(group.getAttribute('blur-effect')).toBe('light');
|
|
336
|
+
expect(group.getAttribute('blur-radius')).toBe('0px');
|
|
337
|
+
});
|
|
338
|
+
});
|