@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.
- package/dist/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +36 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +187 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +280 -7
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +107 -1
- package/dist/commands/init.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 +5 -0
- package/dist/commands/update.js +126 -45
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +314 -2
- package/dist/commands/update.test.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 +6 -0
- package/dist/registry.js +48 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +45 -3
- 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 +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- 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 +272 -187
- package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
- 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 +206 -139
- package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- 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 +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +431 -62
- 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 +82 -1
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +8 -1
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +56 -43
- 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 +210 -152
- package/dist/ui/components/dialog/dialog.ts +29 -23
- 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 +36 -0
- package/dist/ui/components/icon/icon-theme.ts +36 -13
- package/dist/ui/components/icon/icon.test.ts +52 -41
- 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/line-chart.test.ts +76 -1
- package/dist/ui/components/list/list.test.ts +80 -49
- 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 +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- 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/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- 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 +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- 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 +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- 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 +93 -29
- 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 +12 -0
- package/dist/utils/installed.js +16 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +17 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- 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 +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- 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 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
booleanAttribute,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
effect,
|
|
6
|
+
ElementRef,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
numberAttribute,
|
|
10
|
+
ViewEncapsulation,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
13
|
+
|
|
14
|
+
import { cn } from '../../utils/cn';
|
|
15
|
+
import {
|
|
16
|
+
injectColorScheme,
|
|
17
|
+
resolveIconColor,
|
|
18
|
+
UI_ICON_COLOR,
|
|
19
|
+
UI_ICON_THEME,
|
|
20
|
+
} from '../icon/icon-theme';
|
|
21
|
+
import { injectGlassDesign, injectSystemDrawsGlass } from './glass-design';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `'regular'` glass, the default, frosts what's behind it and suits most
|
|
25
|
+
* controls. `'clear'` glass barely frosts it, for controls over photos and
|
|
26
|
+
* video; put a dimming layer under it when what's behind is bright (see
|
|
27
|
+
* `bg-glass-dim`). Don't mix the two on one screen.
|
|
28
|
+
*/
|
|
29
|
+
export type GlassVariant = 'regular' | 'clear';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A capsule (fully rounded ends, the default for controls), a circle (give
|
|
33
|
+
* it equal width and height) or a rectangle with `radius` corners.
|
|
34
|
+
*/
|
|
35
|
+
export type GlassShape = 'capsule' | 'circle' | 'rounded';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a glass element draws:
|
|
39
|
+
*
|
|
40
|
+
* - `'glass'`: Liquid Glass.
|
|
41
|
+
* - `'frosted'`: a blur material, on iOS 15 to 25.
|
|
42
|
+
* - `'solid'`: an opaque surface. That's Material on Android, unless the app
|
|
43
|
+
* opted into glass there, and glass under Reduce Transparency on Android
|
|
44
|
+
* and the web, which can't adapt the glass they draw. iOS adapts its own.
|
|
45
|
+
*/
|
|
46
|
+
export type GlassSurface = 'glass' | 'frosted' | 'solid';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How much the glass blurs where the blur is drawn by the runtime: Android's
|
|
50
|
+
* approximation, and the web before WebGL takes over. iOS's glass ignores it.
|
|
51
|
+
* Clear glass barely blurs.
|
|
52
|
+
*/
|
|
53
|
+
const GLASS_BLUR: Readonly<Record<GlassVariant, string>> = {
|
|
54
|
+
regular: '16px',
|
|
55
|
+
clear: '6px',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** The blur of the frosted materials before iOS 26. */
|
|
59
|
+
const FROSTED_BLUR = '20px';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A solid surface floats over the content as glass does, so it gets
|
|
63
|
+
* Material's elevation shadow (level 2). An inline style, because Tailwind's
|
|
64
|
+
* shadow classes draw nothing on Lynx.
|
|
65
|
+
*/
|
|
66
|
+
const SOLID_ELEVATION =
|
|
67
|
+
'0 1px 2px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.15)';
|
|
68
|
+
|
|
69
|
+
/** AngularLynx's wrapper of the native `blur-view` the component sits on. */
|
|
70
|
+
type BlurViewElement = { setAttribute(name: string, value: unknown): void };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The color an icon projected into the glass draws with when it's given
|
|
74
|
+
* none: the glass's foreground (see UI_ICON_COLOR), the way text there reads
|
|
75
|
+
* `text-glass-foreground`. A named function, not an inline factory: the
|
|
76
|
+
* component's metadata keeps its own copy of an inline one, which nothing
|
|
77
|
+
* calls, so an app's coverage report would show it as uncovered.
|
|
78
|
+
*/
|
|
79
|
+
const glassIconColor = () => {
|
|
80
|
+
const glass = inject(UiGlass);
|
|
81
|
+
return computed(() => glass.foreground());
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Liquid Glass, for the controls and bars that float above an app's content:
|
|
86
|
+
* a `<blur-view>` that draws the right material on each platform.
|
|
87
|
+
*
|
|
88
|
+
* ```html
|
|
89
|
+
* <blur-view ui-glass class="flex-row items-center gap-2 px-4 py-2">
|
|
90
|
+
* <text class="text-glass-foreground">Now playing</text>
|
|
91
|
+
* </blur-view>
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* It sits on the `<blur-view>` itself rather than wrapping one, so the
|
|
95
|
+
* element you lay out and style is the glass, and what you put in it is
|
|
96
|
+
* inside the glass. That matters on iOS: interactive glass reacts only to
|
|
97
|
+
* touches on itself and what's in it, not on views stacked over it.
|
|
98
|
+
*
|
|
99
|
+
* What it draws follows the platform (see GlassDesign): UIKit's Liquid Glass
|
|
100
|
+
* on iOS 26 and later, a frosted blur material on iOS 15 to 25, an opaque
|
|
101
|
+
* Material surface on Android unless the app opts into glass with
|
|
102
|
+
* `provideGlass({ android: 'glass' })`, and the runtime's WebGL glass in the
|
|
103
|
+
* web preview. It also follows the user's accessibility settings where the
|
|
104
|
+
* platform doesn't: see {@link surface}.
|
|
105
|
+
*
|
|
106
|
+
* Text and icons inside read `text-glass-foreground` (and
|
|
107
|
+
* `text-glass-muted-foreground`), which the glass points at the right color
|
|
108
|
+
* for its variant and the user's contrast setting. Glass belongs to the
|
|
109
|
+
* layer above content, and shouldn't be layered on glass.
|
|
110
|
+
*/
|
|
111
|
+
@Component({
|
|
112
|
+
selector: 'blur-view[ui-glass]',
|
|
113
|
+
imports: [LYNX_ELEMENTS],
|
|
114
|
+
encapsulation: ViewEncapsulation.None,
|
|
115
|
+
// Icons projected into the glass inject this through its element injector,
|
|
116
|
+
// which content projection keeps them under.
|
|
117
|
+
providers: [{ provide: UI_ICON_COLOR, useFactory: glassIconColor }],
|
|
118
|
+
host: {
|
|
119
|
+
'[class]': 'surfaceClass()',
|
|
120
|
+
'[style.border-radius.px]': 'cornerRadius()',
|
|
121
|
+
'[style.background-color]': 'solidTint()',
|
|
122
|
+
'[style.box-shadow]': 'elevation()',
|
|
123
|
+
},
|
|
124
|
+
// iOS draws no background on a blur view, so a frosted material takes its
|
|
125
|
+
// tint from a fill under the content, the shape of the glass: the filled
|
|
126
|
+
// button of the design before Liquid Glass.
|
|
127
|
+
template: `
|
|
128
|
+
@if (frostedTint(); as color) {
|
|
129
|
+
<view
|
|
130
|
+
[class]="fillClass()"
|
|
131
|
+
[style.background-color]="color"
|
|
132
|
+
[style.border-radius.px]="cornerRadius()"
|
|
133
|
+
></view>
|
|
134
|
+
}
|
|
135
|
+
<ng-content />
|
|
136
|
+
`,
|
|
137
|
+
})
|
|
138
|
+
export class UiGlass {
|
|
139
|
+
/** `'regular'` or `'clear'`; see GlassVariant. */
|
|
140
|
+
readonly variant = input<GlassVariant>('regular');
|
|
141
|
+
/** `'capsule'`, `'circle'` or `'rounded'`; see GlassShape. */
|
|
142
|
+
readonly shape = input<GlassShape>('capsule');
|
|
143
|
+
/**
|
|
144
|
+
* The corner radius of a `'rounded'` shape, in px. For a shape nested in
|
|
145
|
+
* it, see `concentricRadius()`.
|
|
146
|
+
*/
|
|
147
|
+
readonly radius = input(20, { transform: numberAttribute });
|
|
148
|
+
/**
|
|
149
|
+
* A color the glass takes on, such as `var(--glass-tint)` for the theme's
|
|
150
|
+
* tint or `#2563eb`: a theme token or any CSS color. Use it sparingly, for
|
|
151
|
+
* the screen's main action. It fills a solid surface or a frosted
|
|
152
|
+
* material, as Material, and iOS before Liquid Glass, fill a button.
|
|
153
|
+
*/
|
|
154
|
+
readonly tint = input<string | undefined>(undefined);
|
|
155
|
+
/**
|
|
156
|
+
* The glass reacts to touch, swelling and brightening under the finger as
|
|
157
|
+
* a Liquid Glass control does: on iOS 26 and later, and in the web preview.
|
|
158
|
+
*/
|
|
159
|
+
readonly interactive = input(false, { transform: booleanAttribute });
|
|
160
|
+
|
|
161
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
162
|
+
readonly #systemDrawsGlass = injectSystemDrawsGlass();
|
|
163
|
+
readonly #scheme = injectColorScheme();
|
|
164
|
+
readonly #theme = inject(UI_ICON_THEME);
|
|
165
|
+
readonly #host = inject<ElementRef<BlurViewElement>>(ElementRef);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The design on this platform, which components built on the glass follow
|
|
169
|
+
* for their own form (see GlassDesign).
|
|
170
|
+
*/
|
|
171
|
+
readonly design = injectGlassDesign();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* What the glass draws (see GlassSurface): the design's material, or an
|
|
175
|
+
* opaque surface. Where the runtime draws the glass (Android's opt-in and
|
|
176
|
+
* the web), Reduce Transparency turns it opaque, as UIKit turns its own.
|
|
177
|
+
*/
|
|
178
|
+
readonly surface = computed<GlassSurface>(() => {
|
|
179
|
+
const design = this.design();
|
|
180
|
+
if (design === 'material') return 'solid';
|
|
181
|
+
if (!this.#systemDrawsGlass && this.#settings.reduceTransparency()) {
|
|
182
|
+
return 'solid';
|
|
183
|
+
}
|
|
184
|
+
return design;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/** Whether what's behind shows through clear glass. */
|
|
188
|
+
readonly #seeThrough = computed(
|
|
189
|
+
() => this.variant() === 'clear' && this.surface() !== 'solid',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The color of text and icons on this glass, as a `var()`: the clear
|
|
194
|
+
* foreground on see-through clear glass, which sits over photos and video,
|
|
195
|
+
* and the glass foreground otherwise. Projected `<ui-icon>`s get it, and an
|
|
196
|
+
* SVG of your own can resolve it the way icons do (see icon-theme.ts).
|
|
197
|
+
*/
|
|
198
|
+
readonly foreground = computed(() =>
|
|
199
|
+
this.#seeThrough()
|
|
200
|
+
? 'var(--glass-clear-foreground)'
|
|
201
|
+
: 'var(--glass-foreground)',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The tint as a color the native element can parse: an attribute can't
|
|
206
|
+
* read CSS variables, so a theme token is swapped for its value in the
|
|
207
|
+
* active scheme, as icons do.
|
|
208
|
+
*/
|
|
209
|
+
readonly #tintColor = computed(() => {
|
|
210
|
+
const tint = this.tint();
|
|
211
|
+
return tint ? resolveIconColor(tint, this.#theme[this.#scheme()]) : null;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A tinted solid surface is filled with the tint, as Material's filled
|
|
216
|
+
* buttons are, and as iOS draws tinted glass under Reduce Transparency.
|
|
217
|
+
*/
|
|
218
|
+
protected readonly solidTint = computed(() =>
|
|
219
|
+
this.surface() === 'solid' ? this.#tintColor() : null,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
protected readonly frostedTint = computed(() =>
|
|
223
|
+
this.surface() === 'frosted' ? this.#tintColor() : null,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
protected readonly fillClass = computed(() =>
|
|
227
|
+
cn(
|
|
228
|
+
'h-full w-full absolute left-0 top-0',
|
|
229
|
+
this.shape() !== 'rounded' && 'rounded-full',
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
protected readonly elevation = computed(() =>
|
|
234
|
+
this.surface() === 'solid' ? SOLID_ELEVATION : null,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
protected readonly cornerRadius = computed(() =>
|
|
238
|
+
this.shape() === 'rounded' ? this.radius() : null,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
protected readonly surfaceClass = computed(() => {
|
|
242
|
+
const increaseContrast = this.#settings.increaseContrast();
|
|
243
|
+
return cn(
|
|
244
|
+
this.shape() !== 'rounded' && 'rounded-full',
|
|
245
|
+
// Material's tonal surface, and the opaque glass of Reduce
|
|
246
|
+
// Transparency, unless a tint fills it (see solidTint). iOS ignores a
|
|
247
|
+
// blur-view's background, but never draws a solid surface.
|
|
248
|
+
this.surface() === 'solid' && !this.tint() && 'bg-secondary',
|
|
249
|
+
// Increase Contrast gives the glass a visible edge, as UIKit gives its
|
|
250
|
+
// own glass one.
|
|
251
|
+
increaseContrast &&
|
|
252
|
+
!this.#systemDrawsGlass &&
|
|
253
|
+
'border border-glass-foreground',
|
|
254
|
+
// The glass points the foreground tokens its content reads at the
|
|
255
|
+
// right color (a variable redefined here reaches the projected text,
|
|
256
|
+
// since variables inherit and `color` doesn't): white on see-through
|
|
257
|
+
// clear glass, and full strength for muted text under Increase
|
|
258
|
+
// Contrast.
|
|
259
|
+
this.#seeThrough()
|
|
260
|
+
? '[--glass-foreground:var(--glass-clear-foreground)] [--glass-muted-foreground:var(--glass-clear-foreground)]'
|
|
261
|
+
: increaseContrast &&
|
|
262
|
+
'[--glass-muted-foreground:var(--glass-foreground)]',
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The blur-view's attributes for the surface. Glass is forced to the app's
|
|
268
|
+
* color scheme (`ios-user-interface-style`), which iOS would otherwise take
|
|
269
|
+
* from the device: an app in its light theme on a phone in dark mode would
|
|
270
|
+
* get dark glass under dark text. The frosted materials before iOS 26 don't
|
|
271
|
+
* adapt to dark mode, so the scheme picks one. A solid surface turns the
|
|
272
|
+
* blur off, and on Android stops the blur-view redrawing the page behind it
|
|
273
|
+
* on every frame (`enable-auto-blur`), which it does even with no radius.
|
|
274
|
+
*/
|
|
275
|
+
readonly #attributes = computed(() => {
|
|
276
|
+
const surface = this.surface();
|
|
277
|
+
const variant = this.variant();
|
|
278
|
+
const dark = this.#scheme() === 'dark';
|
|
279
|
+
let effect = 'light';
|
|
280
|
+
let radius = '0px';
|
|
281
|
+
if (surface === 'glass') {
|
|
282
|
+
effect = 'glass';
|
|
283
|
+
radius = GLASS_BLUR[variant];
|
|
284
|
+
} else if (surface === 'frosted') {
|
|
285
|
+
// Apple's controls over media used the dark material in either theme.
|
|
286
|
+
effect = dark || variant === 'clear' ? 'dark' : 'extra-light';
|
|
287
|
+
radius = FROSTED_BLUR;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
'blur-effect': effect,
|
|
291
|
+
'blur-radius': radius,
|
|
292
|
+
'enable-auto-blur': surface !== 'solid',
|
|
293
|
+
'glass-style': variant,
|
|
294
|
+
'glass-tint-color': this.#tintColor(),
|
|
295
|
+
'glass-interactive': this.interactive() || null,
|
|
296
|
+
'ios-user-interface-style': dark ? 'dark' : 'light',
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Set on the element as they are, rather than through host attribute
|
|
301
|
+
// bindings, which turn every value into a string: Lynx reads
|
|
302
|
+
// `glass-interactive` and `enable-auto-blur` as booleans. `null` removes an
|
|
303
|
+
// attribute, which restores Lynx's default.
|
|
304
|
+
protected readonly material = effect(() => {
|
|
305
|
+
const element = this.#host.nativeElement;
|
|
306
|
+
for (const [name, value] of Object.entries(this.#attributes())) {
|
|
307
|
+
element.setAttribute(name, value);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Merges the glass elements inside it, as iOS 26 draws a group of glass
|
|
314
|
+
* controls: shapes closer than `spacing` flow into each other like droplets,
|
|
315
|
+
* and the group draws them in one pass. Put a `<blur-view ui-glass>` directly
|
|
316
|
+
* inside it for each shape: iOS merges the glass in the group's own content,
|
|
317
|
+
* and a wrapper that draws anything of its own comes between them.
|
|
318
|
+
*
|
|
319
|
+
* ```html
|
|
320
|
+
* <blur-view ui-glass-group [spacing]="16" class="flex-row gap-2">
|
|
321
|
+
* <blur-view ui-glass shape="circle" class="h-12 w-12">…</blur-view>
|
|
322
|
+
* <blur-view ui-glass shape="circle" class="h-12 w-12">…</blur-view>
|
|
323
|
+
* </blur-view>
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* The group draws nothing itself. Shapes merge on iOS 26 and later and in
|
|
327
|
+
* the web preview. Android's glass doesn't merge, and the frosted and
|
|
328
|
+
* Material designs have no glass to merge.
|
|
329
|
+
*
|
|
330
|
+
* Moving or resizing a shape, with a CSS transition for example, morphs the
|
|
331
|
+
* merged glass with it. A shape that's added or removed appears or
|
|
332
|
+
* disappears at once: iOS's glass doesn't animate that in Lynx.
|
|
333
|
+
*/
|
|
334
|
+
@Component({
|
|
335
|
+
selector: 'blur-view[ui-glass-group]',
|
|
336
|
+
encapsulation: ViewEncapsulation.None,
|
|
337
|
+
template: `<ng-content />`,
|
|
338
|
+
})
|
|
339
|
+
export class UiGlassGroup {
|
|
340
|
+
/** How close two glass shapes come before they merge, in px. */
|
|
341
|
+
readonly spacing = input(0, { transform: numberAttribute });
|
|
342
|
+
|
|
343
|
+
readonly #design = injectGlassDesign();
|
|
344
|
+
readonly #host = inject<ElementRef<BlurViewElement>>(ElementRef);
|
|
345
|
+
|
|
346
|
+
// As UiGlass sets its own (see there). A container that isn't merging
|
|
347
|
+
// glass is a plain transparent view: `light` with no radius draws nothing,
|
|
348
|
+
// where `glass-container` would log an error before iOS 26. It never blurs,
|
|
349
|
+
// so on Android it never redraws the page behind it either.
|
|
350
|
+
protected readonly material = effect(() => {
|
|
351
|
+
const element = this.#host.nativeElement;
|
|
352
|
+
element.setAttribute(
|
|
353
|
+
'blur-effect',
|
|
354
|
+
this.#design() === 'glass' ? 'glass-container' : 'light',
|
|
355
|
+
);
|
|
356
|
+
element.setAttribute('blur-radius', '0px');
|
|
357
|
+
element.setAttribute('enable-auto-blur', false);
|
|
358
|
+
element.setAttribute('spacing', this.spacing());
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
UiGlass,
|
|
3
|
+
UiGlassGroup,
|
|
4
|
+
type GlassShape,
|
|
5
|
+
type GlassSurface,
|
|
6
|
+
type GlassVariant,
|
|
7
|
+
} from './glass';
|
|
8
|
+
export {
|
|
9
|
+
FIRST_GLASS_IOS,
|
|
10
|
+
UI_GLASS_OPTIONS,
|
|
11
|
+
glassDesignFor,
|
|
12
|
+
injectGlassDesign,
|
|
13
|
+
injectSystemDrawsGlass,
|
|
14
|
+
provideGlass,
|
|
15
|
+
type GlassDesign,
|
|
16
|
+
type GlassOptions,
|
|
17
|
+
} from './glass-design';
|
|
18
|
+
export { concentricRadius } from './concentric-radius';
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ICON_LIGHT_COLORS,
|
|
12
12
|
UI_ICON_COLOR,
|
|
13
13
|
UI_ICON_THEME,
|
|
14
|
+
injectColorScheme,
|
|
14
15
|
injectIconColor,
|
|
15
16
|
provideIconTheme,
|
|
16
17
|
resolveIconColor,
|
|
@@ -92,6 +93,41 @@ describe('provideIconTheme', () => {
|
|
|
92
93
|
});
|
|
93
94
|
});
|
|
94
95
|
|
|
96
|
+
describe('injectColorScheme', () => {
|
|
97
|
+
const scheme = (providers: Provider[], dark = signal(false)) =>
|
|
98
|
+
runInInjectionContext(
|
|
99
|
+
Injector.create({
|
|
100
|
+
providers: [
|
|
101
|
+
{ provide: LynxTheme, useValue: { isDarkMode: dark } },
|
|
102
|
+
{
|
|
103
|
+
provide: UI_ICON_THEME,
|
|
104
|
+
useValue: { light: ICON_LIGHT_COLORS, dark: ICON_DARK_COLORS },
|
|
105
|
+
},
|
|
106
|
+
...providers,
|
|
107
|
+
],
|
|
108
|
+
}),
|
|
109
|
+
() => injectColorScheme(),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
it("is light unless the theme says otherwise, whatever the device's appearance", () => {
|
|
113
|
+
expect(scheme([], signal(true))()).toBe('light');
|
|
114
|
+
expect(scheme([provideIconTheme({ colorScheme: 'dark' })])()).toBe('dark');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("follows the device with 'system', or the app's own state", () => {
|
|
118
|
+
const dark = signal(false);
|
|
119
|
+
const system = scheme([provideIconTheme({ colorScheme: 'system' })], dark);
|
|
120
|
+
expect(system()).toBe('light');
|
|
121
|
+
dark.set(true);
|
|
122
|
+
expect(system()).toBe('dark');
|
|
123
|
+
|
|
124
|
+
const own = signal<'light' | 'dark'>('dark');
|
|
125
|
+
expect(scheme([provideIconTheme({ colorScheme: () => own() })])()).toBe(
|
|
126
|
+
'dark',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
95
131
|
describe('injectIconColor', () => {
|
|
96
132
|
it('draws with the light foreground by default, even on a dark device', () => {
|
|
97
133
|
const resolve = setup([], signal(true));
|
|
@@ -80,6 +80,12 @@ export const ICON_LIGHT_COLORS: IconColorTokens = {
|
|
|
80
80
|
'chart-3': 'rgba(39, 71, 84, 1)',
|
|
81
81
|
'chart-4': 'rgba(232, 196, 104, 1)',
|
|
82
82
|
'chart-5': 'rgba(244, 164, 98, 1)',
|
|
83
|
+
'glass-tint': 'rgba(24, 24, 27, 1)',
|
|
84
|
+
'glass-tint-foreground': 'rgba(250, 250, 250, 1)',
|
|
85
|
+
'glass-foreground': 'rgba(9, 9, 11, 1)',
|
|
86
|
+
'glass-muted-foreground': 'rgba(63, 63, 70, 1)',
|
|
87
|
+
'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
|
|
88
|
+
'glass-dim': 'rgba(0, 0, 0, 0.35)',
|
|
83
89
|
};
|
|
84
90
|
|
|
85
91
|
export const ICON_DARK_COLORS: IconColorTokens = {
|
|
@@ -107,6 +113,12 @@ export const ICON_DARK_COLORS: IconColorTokens = {
|
|
|
107
113
|
'chart-3': 'rgba(232, 140, 48, 1)',
|
|
108
114
|
'chart-4': 'rgba(175, 87, 219, 1)',
|
|
109
115
|
'chart-5': 'rgba(226, 54, 112, 1)',
|
|
116
|
+
'glass-tint': 'rgba(250, 250, 250, 1)',
|
|
117
|
+
'glass-tint-foreground': 'rgba(24, 24, 27, 1)',
|
|
118
|
+
'glass-foreground': 'rgba(250, 250, 250, 1)',
|
|
119
|
+
'glass-muted-foreground': 'rgba(212, 212, 216, 1)',
|
|
120
|
+
'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
|
|
121
|
+
'glass-dim': 'rgba(0, 0, 0, 0.35)',
|
|
110
122
|
};
|
|
111
123
|
|
|
112
124
|
export const UI_ICON_THEME = new InjectionToken<UiIconTheme>('UI_ICON_THEME', {
|
|
@@ -175,6 +187,26 @@ export const resolveIconColor = (
|
|
|
175
187
|
: resolveIconColor(inner.slice(comma + 1), tokens);
|
|
176
188
|
};
|
|
177
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The color scheme the app's theme is in, as {@link provideIconTheme} sets it
|
|
192
|
+
* (`'light'` unless it says otherwise). For what has to match the app's CSS
|
|
193
|
+
* theme but can't read it: icons pick their color table with it, and glass
|
|
194
|
+
* picks a light or dark material.
|
|
195
|
+
*/
|
|
196
|
+
export const injectColorScheme = (): Signal<IconColorScheme> => {
|
|
197
|
+
const option = inject(UI_ICON_THEME).colorScheme ?? 'light';
|
|
198
|
+
const appearance = inject(LynxTheme);
|
|
199
|
+
return computed(() =>
|
|
200
|
+
option === 'system'
|
|
201
|
+
? appearance.isDarkMode()
|
|
202
|
+
? 'dark'
|
|
203
|
+
: 'light'
|
|
204
|
+
: typeof option === 'function'
|
|
205
|
+
? option()
|
|
206
|
+
: option,
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
178
210
|
/**
|
|
179
211
|
* For an SVG component's constructor: a function that resolves its `color`
|
|
180
212
|
* input (or, without one, the surrounding {@link UI_ICON_COLOR}, then the
|
|
@@ -183,21 +215,12 @@ export const resolveIconColor = (
|
|
|
183
215
|
*/
|
|
184
216
|
export const injectIconColor = (): ((color: string | undefined) => string) => {
|
|
185
217
|
const theme = inject(UI_ICON_THEME);
|
|
186
|
-
const
|
|
218
|
+
const scheme = injectColorScheme();
|
|
187
219
|
const inherited = inject(UI_ICON_COLOR, { optional: true });
|
|
188
220
|
|
|
189
|
-
const tokens = computed(() =>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
option === 'system'
|
|
193
|
-
? appearance.isDarkMode()
|
|
194
|
-
? 'dark'
|
|
195
|
-
: 'light'
|
|
196
|
-
: typeof option === 'function'
|
|
197
|
-
? option()
|
|
198
|
-
: option;
|
|
199
|
-
return scheme === 'dark' ? theme.dark : theme.light;
|
|
200
|
-
});
|
|
221
|
+
const tokens = computed(() =>
|
|
222
|
+
scheme() === 'dark' ? theme.dark : theme.light,
|
|
223
|
+
);
|
|
201
224
|
|
|
202
225
|
return (color) =>
|
|
203
226
|
resolveIconColor(color ?? inherited?.() ?? DEFAULT_ICON_COLOR, tokens());
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
3
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
4
|
import { UiIcon } from './icon';
|
|
7
5
|
import {
|
|
8
6
|
ICON_DARK_COLORS,
|
|
@@ -13,84 +11,97 @@ import {
|
|
|
13
11
|
import { ICONS, type IconName } from './icons';
|
|
14
12
|
|
|
15
13
|
@Component({
|
|
16
|
-
imports: [UiIcon
|
|
17
|
-
template:
|
|
14
|
+
imports: [UiIcon],
|
|
15
|
+
template: `
|
|
16
|
+
<ui-icon
|
|
17
|
+
[name]="name()"
|
|
18
|
+
[size]="size()"
|
|
19
|
+
[color]="color()"
|
|
20
|
+
[class]="className()"
|
|
21
|
+
/>
|
|
22
|
+
`,
|
|
18
23
|
})
|
|
19
24
|
class IconHost {
|
|
20
|
-
name
|
|
21
|
-
size
|
|
22
|
-
color
|
|
25
|
+
readonly name = signal<IconName>('check');
|
|
26
|
+
readonly size = signal<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
27
|
+
readonly color = signal<string | undefined>(undefined);
|
|
28
|
+
readonly className = signal('');
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
const renderIcon = async (providers: Parameters<typeof render>[1] = {}) => {
|
|
32
|
+
const result = await render(IconHost, providers);
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
host: result.componentRef.instance as IconHost,
|
|
36
|
+
svg: () => result.container.querySelector('svg')!,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
25
40
|
describe('UiIcon', () => {
|
|
26
|
-
it('
|
|
27
|
-
const {
|
|
28
|
-
expect(
|
|
41
|
+
it('draws the named icon at the default size', async () => {
|
|
42
|
+
const { svg } = await renderIcon();
|
|
43
|
+
expect(svg().getAttribute('style')).toContain('24px');
|
|
44
|
+
expect(svg().getAttribute('content')).toContain('<svg');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('draws another icon when the name changes', async () => {
|
|
48
|
+
const { host, svg } = await renderIcon();
|
|
49
|
+
const check = svg().getAttribute('content');
|
|
50
|
+
host.name.set('x');
|
|
51
|
+
await waitForUpdate();
|
|
52
|
+
expect(svg().getAttribute('content')).not.toBe(check);
|
|
29
53
|
});
|
|
30
54
|
|
|
31
55
|
it('applies the size input to the width/height style', async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const { componentRef, container } = await render(UiIcon);
|
|
35
|
-
setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
|
|
36
|
-
setInputSignal((componentRef.instance as UiIcon).size, 'xs');
|
|
56
|
+
const { host, svg } = await renderIcon();
|
|
57
|
+
host.size.set('xs');
|
|
37
58
|
await waitForUpdate();
|
|
38
|
-
|
|
39
|
-
expect(svg?.getAttribute('style')).toContain('16px');
|
|
59
|
+
expect(svg().getAttribute('style')).toContain('16px');
|
|
40
60
|
});
|
|
41
61
|
|
|
42
62
|
it('substitutes currentColor with the color input value', async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
// Lynx's SVG parser reads neither `currentColor` nor CSS variables.
|
|
64
|
+
const { host, svg } = await renderIcon();
|
|
65
|
+
host.color.set('rgb(255, 0, 0)');
|
|
46
66
|
await waitForUpdate();
|
|
47
|
-
|
|
48
|
-
expect(svg
|
|
49
|
-
expect(svg?.getAttribute('content')).not.toContain('currentColor');
|
|
67
|
+
expect(svg().getAttribute('content')).toContain('rgb(255, 0, 0)');
|
|
68
|
+
expect(svg().getAttribute('content')).not.toContain('currentColor');
|
|
50
69
|
});
|
|
51
70
|
|
|
52
71
|
it("draws in the light theme's foreground when no color is set", async () => {
|
|
53
|
-
const {
|
|
54
|
-
|
|
55
|
-
await waitForUpdate();
|
|
56
|
-
const content = container.querySelector('svg')?.getAttribute('content');
|
|
72
|
+
const { svg } = await renderIcon();
|
|
73
|
+
const content = svg().getAttribute('content');
|
|
57
74
|
expect(content).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
58
75
|
expect(content).not.toContain('currentColor');
|
|
59
76
|
});
|
|
60
77
|
|
|
61
78
|
it('resolves a theme token against the active color scheme', async () => {
|
|
62
|
-
const {
|
|
79
|
+
const { host, svg } = await renderIcon({
|
|
63
80
|
providers: [provideIconTheme({ colorScheme: 'dark' })],
|
|
64
81
|
});
|
|
65
|
-
|
|
66
|
-
setInputSignal(icon.name, 'check' as IconName);
|
|
67
|
-
setInputSignal(icon.color, 'var(--primary)');
|
|
82
|
+
host.color.set('var(--primary)');
|
|
68
83
|
await waitForUpdate();
|
|
69
|
-
expect(
|
|
84
|
+
expect(svg().getAttribute('content')).toContain(
|
|
70
85
|
ICON_DARK_COLORS['primary'],
|
|
71
86
|
);
|
|
72
87
|
});
|
|
73
88
|
|
|
74
89
|
it("takes its container's color when it sets none", async () => {
|
|
75
|
-
const {
|
|
90
|
+
const { svg } = await renderIcon({
|
|
76
91
|
providers: [
|
|
77
92
|
{ provide: UI_ICON_COLOR, useValue: signal('var(--destructive)') },
|
|
78
93
|
],
|
|
79
94
|
});
|
|
80
|
-
|
|
81
|
-
await waitForUpdate();
|
|
82
|
-
expect(container.querySelector('svg')?.getAttribute('content')).toContain(
|
|
95
|
+
expect(svg().getAttribute('content')).toContain(
|
|
83
96
|
ICON_LIGHT_COLORS['destructive'],
|
|
84
97
|
);
|
|
85
98
|
});
|
|
86
99
|
|
|
87
100
|
it('applies the class input to the svg', async () => {
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
-
setInputSignal(icon.name, 'check' as IconName);
|
|
91
|
-
setInputSignal(icon.userClass, 'mr-2');
|
|
101
|
+
const { host, svg } = await renderIcon();
|
|
102
|
+
host.className.set('mr-2');
|
|
92
103
|
await waitForUpdate();
|
|
93
|
-
expect(
|
|
104
|
+
expect(svg().getAttribute('class')).toBe('mr-2');
|
|
94
105
|
});
|
|
95
106
|
});
|
|
96
107
|
|