@blotch/dolan 22.2.1 → 22.2.3
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 +32 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +129 -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 +41 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +93 -9
- 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 +149 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +285 -26
- 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/update.d.ts +6 -0
- package/dist/commands/update.js +59 -24
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +273 -31
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +32 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +76 -37
- package/dist/registry.test.js.map +1 -1
- package/dist/testing/set-input-signal.d.ts +13 -0
- package/dist/testing/set-input-signal.js +20 -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/action-sheet/action-sheet.test.ts +48 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
- package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
- package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
- package/dist/ui/components/area-chart/area-chart.test.ts +59 -146
- 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/bar-chart/bar-chart-render.test.ts +47 -163
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -58
- 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 +191 -9
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
- package/dist/ui/components/button/button-render.test.ts +19 -0
- package/dist/ui/components/button/button.test.ts +2 -15
- package/dist/ui/components/button/button.ts +18 -2
- package/dist/ui/components/button-group/button-group.test.ts +2 -21
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +107 -85
- 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/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 +139 -0
- package/dist/ui/components/chart/chart-presets.ts +271 -0
- package/dist/ui/components/chart/chart-render.test.ts +730 -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 +198 -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.ts +921 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/dialog/dialog.test.ts +49 -2
- package/dist/ui/components/dialog/dialog.ts +43 -12
- package/dist/ui/components/icon/icon-theme.test.ts +133 -0
- package/dist/ui/components/icon/icon-theme.ts +204 -0
- package/dist/ui/components/icon/icon.test.ts +51 -2
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
- package/dist/ui/components/line-chart/line-chart.test.ts +63 -67
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +69 -2
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
- package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
- package/dist/ui/components/pagination/pagination.test.ts +62 -0
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -47
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/spinner/spinner.test.ts +27 -0
- package/dist/ui/components/spinner/spinner.ts +33 -23
- package/dist/ui/components/toast/toast.test.ts +37 -0
- package/dist/ui/components/toast/toast.ts +20 -4
- package/dist/ui/theme/chart-colors.test.ts +30 -0
- package/dist/ui/theme/dark.css +8 -0
- package/dist/ui/theme/default.css +8 -0
- package/dist/ui/theme/icon-colors.test.ts +30 -0
- package/dist/ui/theme/tailwind-plugin.ts +10 -1
- package/dist/utils/installed.d.ts +17 -0
- package/dist/utils/installed.js +27 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +41 -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 +23 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +34 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/package.json +5 -1
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -549
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
UiChart,
|
|
3
|
+
UiChartTooltip,
|
|
4
|
+
type UiChartTooltipContext,
|
|
5
|
+
tooltip,
|
|
6
|
+
} from './chart';
|
|
7
|
+
export {
|
|
8
|
+
type ChartColorScheme,
|
|
9
|
+
type ChartColorSchemeOption,
|
|
10
|
+
type ChartColorTokens,
|
|
11
|
+
CHART_DARK_COLORS,
|
|
12
|
+
CHART_LIGHT_COLORS,
|
|
13
|
+
DEFAULT_CHART_THEME,
|
|
14
|
+
UI_CHART_THEME,
|
|
15
|
+
type UiChartTheme,
|
|
16
|
+
provideChartTheme,
|
|
17
|
+
resolveChartPaint,
|
|
18
|
+
} from './chart-theme';
|
|
19
|
+
export {
|
|
20
|
+
type CartesianPresetOptions,
|
|
21
|
+
type ChartPadding,
|
|
22
|
+
type XYDatum,
|
|
23
|
+
} from './chart-presets';
|
|
24
|
+
export { type ChartViewport, type ChartZoomAxes } from './chart-zoom';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
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
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { DURATION } from '../../utils/animate';
|
|
5
6
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
7
|
import {
|
|
7
8
|
UiDialog,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
} from './dialog';
|
|
13
14
|
|
|
14
15
|
@Component({
|
|
15
|
-
standalone: true,
|
|
16
16
|
imports: [
|
|
17
17
|
UiDialog,
|
|
18
18
|
UiDialogHeader,
|
|
@@ -36,6 +36,47 @@ class DialogHost {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
describe('UiDialog', () => {
|
|
39
|
+
it('stops its close timers when it is destroyed while closing', async () => {
|
|
40
|
+
const warn = vi.spyOn(console, 'warn');
|
|
41
|
+
const { componentRef, unmount } = await render(UiDialog);
|
|
42
|
+
const inst = componentRef.instance as UiDialog;
|
|
43
|
+
inst.open.set(true);
|
|
44
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
45
|
+
inst.open.set(false);
|
|
46
|
+
await waitForUpdate();
|
|
47
|
+
// Destroyed during the close animation, as an `@if` around the dialog
|
|
48
|
+
// would do. Its close timer (DURATION.normal + 20) used to fire anyway
|
|
49
|
+
// and emit `closed` from the destroyed output.
|
|
50
|
+
unmount();
|
|
51
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
52
|
+
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
53
|
+
warn.mockRestore();
|
|
54
|
+
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
55
|
+
[],
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
60
|
+
const { componentRef, container } = await render(UiDialog);
|
|
61
|
+
const inst = componentRef.instance as UiDialog;
|
|
62
|
+
const panel = container.querySelector(
|
|
63
|
+
'[accessibility-exclusive-focus]',
|
|
64
|
+
) as Element & { invoke?: unknown };
|
|
65
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
66
|
+
const invoke = vi.fn();
|
|
67
|
+
panel.invoke = invoke;
|
|
68
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
69
|
+
nativeElement: panel,
|
|
70
|
+
});
|
|
71
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
72
|
+
nativeElement: panel,
|
|
73
|
+
});
|
|
74
|
+
inst.open.set(true);
|
|
75
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
76
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
77
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
78
|
+
});
|
|
79
|
+
|
|
39
80
|
it('renders header, title, description, and footer', async () => {
|
|
40
81
|
const { getByText } = await render(DialogHost);
|
|
41
82
|
expect(getByText('Title')).toBeTruthy();
|
|
@@ -91,12 +132,18 @@ describe('UiDialog', () => {
|
|
|
91
132
|
// Covers the early-return branches in #animateIn / #animateOut.
|
|
92
133
|
const { componentRef } = await render(UiDialog);
|
|
93
134
|
const inst = componentRef.instance as UiDialog;
|
|
135
|
+
const closed = vi.fn();
|
|
136
|
+
inst.closed.subscribe(closed);
|
|
94
137
|
inst.open.set(true);
|
|
95
138
|
await waitForUpdate();
|
|
96
139
|
await waitForUpdate();
|
|
97
140
|
inst.open.set(false);
|
|
98
141
|
await waitForUpdate();
|
|
142
|
+
// Wait out the close sequence, so #animateOut runs (and bails) while the
|
|
143
|
+
// component is still alive. Destroying it clears the sequence's timers.
|
|
144
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
99
145
|
expect(inst.open()).toBe(false);
|
|
146
|
+
expect(closed).toHaveBeenCalledOnce();
|
|
100
147
|
});
|
|
101
148
|
|
|
102
149
|
it('overlayStyle switches between visible and hidden variants', async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ElementRef,
|
|
3
|
+
type OnDestroy,
|
|
3
4
|
Component,
|
|
4
5
|
ViewEncapsulation,
|
|
5
6
|
computed,
|
|
@@ -26,20 +27,24 @@ import { cn } from '../../utils/cn';
|
|
|
26
27
|
|
|
27
28
|
@Component({
|
|
28
29
|
selector: 'ui-dialog',
|
|
29
|
-
standalone: true,
|
|
30
30
|
imports: [LYNX_ELEMENTS],
|
|
31
31
|
encapsulation: ViewEncapsulation.None,
|
|
32
32
|
template: `
|
|
33
33
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
34
34
|
<view #backdrop [class]="backdropClass()" (bindtap)="onBackdropTap()">
|
|
35
|
-
<view
|
|
35
|
+
<view
|
|
36
|
+
#panel
|
|
37
|
+
[class]="panelClass()"
|
|
38
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
39
|
+
(catchtap)="onPanelTap()"
|
|
40
|
+
>
|
|
36
41
|
<ng-content />
|
|
37
42
|
</view>
|
|
38
43
|
</view>
|
|
39
44
|
</overlay>
|
|
40
45
|
`,
|
|
41
46
|
})
|
|
42
|
-
export class UiDialog {
|
|
47
|
+
export class UiDialog implements OnDestroy {
|
|
43
48
|
readonly open = model(false);
|
|
44
49
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
50
|
|
|
@@ -52,6 +57,9 @@ export class UiDialog {
|
|
|
52
57
|
#backdropAnim?: AnimationHandle;
|
|
53
58
|
#panelAnim?: AnimationHandle;
|
|
54
59
|
#hasBeenOpen = false;
|
|
60
|
+
// The open and close sequences' pending timers, so ngOnDestroy can stop
|
|
61
|
+
// them.
|
|
62
|
+
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
55
63
|
|
|
56
64
|
constructor() {
|
|
57
65
|
// `#hasBeenOpen` prevents the close animation from running on the initial
|
|
@@ -110,15 +118,37 @@ export class UiDialog {
|
|
|
110
118
|
*/
|
|
111
119
|
protected onPanelTap(): void {}
|
|
112
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The dialog can be destroyed while its open or close sequence is still
|
|
123
|
+
* running, for example by an `@if` around it or a route change right after
|
|
124
|
+
* closing it. The rest of the sequence then ran on the destroyed component:
|
|
125
|
+
* it animated detached elements and emitted `closed` from a destroyed
|
|
126
|
+
* output, which Angular warns about in dev builds (NG0953).
|
|
127
|
+
*/
|
|
128
|
+
ngOnDestroy(): void {
|
|
129
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `setTimeout`, tracked so that ngOnDestroy can cancel it.
|
|
134
|
+
*/
|
|
135
|
+
#later(callback: () => void, ms: number): void {
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
this.#timers.delete(timer);
|
|
138
|
+
callback();
|
|
139
|
+
}, ms);
|
|
140
|
+
this.#timers.add(timer);
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
/**
|
|
114
144
|
* Two-phase open: make overlay visible first (so native elements exist in
|
|
115
145
|
* the tree), then animate on the next frame. See nav-drawer for the same
|
|
116
146
|
* pattern — Lynx animate() requires elements to be mounted before use.
|
|
117
147
|
*/
|
|
118
148
|
#doOpen(): void {
|
|
119
|
-
|
|
149
|
+
this.#later(() => {
|
|
120
150
|
this.overlayVisible.set(true);
|
|
121
|
-
|
|
151
|
+
this.#later(() => this.#animateIn(), 0);
|
|
122
152
|
}, 0);
|
|
123
153
|
}
|
|
124
154
|
|
|
@@ -127,9 +157,9 @@ export class UiDialog {
|
|
|
127
157
|
* causing elements to disappear before the animation finishes.
|
|
128
158
|
*/
|
|
129
159
|
#doClose(): void {
|
|
130
|
-
|
|
160
|
+
this.#later(() => {
|
|
131
161
|
this.#animateOut();
|
|
132
|
-
|
|
162
|
+
this.#later(() => {
|
|
133
163
|
this.overlayVisible.set(false);
|
|
134
164
|
this.closed.emit();
|
|
135
165
|
}, DURATION.normal + 20);
|
|
@@ -146,9 +176,14 @@ export class UiDialog {
|
|
|
146
176
|
|
|
147
177
|
// Backdrop fades in first
|
|
148
178
|
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
179
|
+
// Move a screen reader into the panel. It would otherwise stay on whatever
|
|
180
|
+
// opened it, which is now behind the overlay. With the panel's
|
|
181
|
+
// accessibility-exclusive-focus (see the template), it then can't leave
|
|
182
|
+
// the panel until it closes.
|
|
183
|
+
panel.invoke?.('requestAccessibilityFocus');
|
|
149
184
|
|
|
150
185
|
// Panel scales in with gentle spring (staggered 30ms after backdrop)
|
|
151
|
-
|
|
186
|
+
this.#later(() => {
|
|
152
187
|
this.#panelAnim = scaleIn(panel, {
|
|
153
188
|
duration: DURATION.slow,
|
|
154
189
|
easing: EASING.springSubtle,
|
|
@@ -181,7 +216,6 @@ export class UiDialog {
|
|
|
181
216
|
|
|
182
217
|
@Component({
|
|
183
218
|
selector: 'ui-dialog-header',
|
|
184
|
-
standalone: true,
|
|
185
219
|
imports: [LYNX_ELEMENTS],
|
|
186
220
|
encapsulation: ViewEncapsulation.None,
|
|
187
221
|
template: `
|
|
@@ -200,7 +234,6 @@ export class UiDialogHeader {
|
|
|
200
234
|
|
|
201
235
|
@Component({
|
|
202
236
|
selector: 'ui-dialog-title',
|
|
203
|
-
standalone: true,
|
|
204
237
|
imports: [LYNX_ELEMENTS],
|
|
205
238
|
encapsulation: ViewEncapsulation.None,
|
|
206
239
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -215,7 +248,6 @@ export class UiDialogTitle {
|
|
|
215
248
|
|
|
216
249
|
@Component({
|
|
217
250
|
selector: 'ui-dialog-description',
|
|
218
|
-
standalone: true,
|
|
219
251
|
imports: [LYNX_ELEMENTS],
|
|
220
252
|
encapsulation: ViewEncapsulation.None,
|
|
221
253
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -230,7 +262,6 @@ export class UiDialogDescription {
|
|
|
230
262
|
|
|
231
263
|
@Component({
|
|
232
264
|
selector: 'ui-dialog-footer',
|
|
233
|
-
standalone: true,
|
|
234
265
|
imports: [LYNX_ELEMENTS],
|
|
235
266
|
encapsulation: ViewEncapsulation.None,
|
|
236
267
|
template: `
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injector,
|
|
3
|
+
type Provider,
|
|
4
|
+
runInInjectionContext,
|
|
5
|
+
signal,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { LynxTheme } from '@blotch/angular-lynx';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
ICON_DARK_COLORS,
|
|
11
|
+
ICON_LIGHT_COLORS,
|
|
12
|
+
UI_ICON_COLOR,
|
|
13
|
+
UI_ICON_THEME,
|
|
14
|
+
injectIconColor,
|
|
15
|
+
provideIconTheme,
|
|
16
|
+
resolveIconColor,
|
|
17
|
+
} from './icon-theme';
|
|
18
|
+
|
|
19
|
+
const LIGHT_FOREGROUND = ICON_LIGHT_COLORS['foreground'];
|
|
20
|
+
const DARK_FOREGROUND = ICON_DARK_COLORS['foreground'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runs injectIconColor() against a device appearance and extra providers. A
|
|
24
|
+
* plain Injector has no root scope, so it gets the default theme explicitly;
|
|
25
|
+
* the token's own factory is covered by the component tests.
|
|
26
|
+
*/
|
|
27
|
+
const setup = (providers: Provider[] = [], dark = signal(false)) => {
|
|
28
|
+
const injector = Injector.create({
|
|
29
|
+
providers: [
|
|
30
|
+
{ provide: LynxTheme, useValue: { isDarkMode: dark } },
|
|
31
|
+
{
|
|
32
|
+
provide: UI_ICON_THEME,
|
|
33
|
+
useValue: { light: ICON_LIGHT_COLORS, dark: ICON_DARK_COLORS },
|
|
34
|
+
},
|
|
35
|
+
...providers,
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
return runInInjectionContext(injector, () => injectIconColor());
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe('resolveIconColor', () => {
|
|
42
|
+
it('passes a literal color through', () => {
|
|
43
|
+
expect(resolveIconColor('#2563eb', ICON_LIGHT_COLORS)).toBe('#2563eb');
|
|
44
|
+
expect(resolveIconColor(' rgba(1, 2, 3, 1) ', ICON_LIGHT_COLORS)).toBe(
|
|
45
|
+
'rgba(1, 2, 3, 1)',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('swaps a theme token for its value', () => {
|
|
50
|
+
expect(resolveIconColor('var(--primary)', ICON_LIGHT_COLORS)).toBe(
|
|
51
|
+
ICON_LIGHT_COLORS['primary'],
|
|
52
|
+
);
|
|
53
|
+
expect(resolveIconColor('var( --destructive )', ICON_DARK_COLORS)).toBe(
|
|
54
|
+
ICON_DARK_COLORS['destructive'],
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("falls back to the var()'s fallback for an unknown token", () => {
|
|
59
|
+
expect(resolveIconColor('var(--brand, #ff0000)', ICON_LIGHT_COLORS)).toBe(
|
|
60
|
+
'#ff0000',
|
|
61
|
+
);
|
|
62
|
+
// The fallback may itself be a token.
|
|
63
|
+
expect(
|
|
64
|
+
resolveIconColor('var(--brand, var(--primary))', ICON_LIGHT_COLORS),
|
|
65
|
+
).toBe(ICON_LIGHT_COLORS['primary']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('falls back to the foreground for an unknown token with no fallback', () => {
|
|
69
|
+
expect(resolveIconColor('var(--brand)', ICON_LIGHT_COLORS)).toBe(
|
|
70
|
+
LIGHT_FOREGROUND,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('leaves an unknown token as it is when the table has no foreground', () => {
|
|
75
|
+
expect(resolveIconColor('var(--brand)', {})).toBe('var(--brand)');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('provideIconTheme', () => {
|
|
80
|
+
it('merges the overrides over the default tables', () => {
|
|
81
|
+
const provider = provideIconTheme({
|
|
82
|
+
light: { primary: '#123456' },
|
|
83
|
+
colorScheme: 'dark',
|
|
84
|
+
}) as { provide: unknown; useValue: Record<string, unknown> };
|
|
85
|
+
expect(provider.provide).toBe(UI_ICON_THEME);
|
|
86
|
+
expect(provider.useValue['light']).toEqual({
|
|
87
|
+
...ICON_LIGHT_COLORS,
|
|
88
|
+
primary: '#123456',
|
|
89
|
+
});
|
|
90
|
+
expect(provider.useValue['dark']).toEqual(ICON_DARK_COLORS);
|
|
91
|
+
expect(provider.useValue['colorScheme']).toBe('dark');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('injectIconColor', () => {
|
|
96
|
+
it('draws with the light foreground by default, even on a dark device', () => {
|
|
97
|
+
const resolve = setup([], signal(true));
|
|
98
|
+
expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('resolves an explicit color against the active table', () => {
|
|
102
|
+
const resolve = setup([provideIconTheme({ colorScheme: 'dark' })]);
|
|
103
|
+
expect(resolve('var(--primary)')).toBe(ICON_DARK_COLORS['primary']);
|
|
104
|
+
expect(resolve('#00ff00')).toBe('#00ff00');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("follows the device appearance with 'system'", () => {
|
|
108
|
+
const dark = signal(false);
|
|
109
|
+
const resolve = setup([provideIconTheme({ colorScheme: 'system' })], dark);
|
|
110
|
+
expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
|
|
111
|
+
dark.set(true);
|
|
112
|
+
expect(resolve(undefined)).toBe(DARK_FOREGROUND);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("reads the app's own theme state from a function", () => {
|
|
116
|
+
const scheme = signal<'light' | 'dark'>('dark');
|
|
117
|
+
const resolve = setup([provideIconTheme({ colorScheme: () => scheme() })]);
|
|
118
|
+
expect(resolve(undefined)).toBe(DARK_FOREGROUND);
|
|
119
|
+
scheme.set('light');
|
|
120
|
+
expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses the container's color when the icon sets none", () => {
|
|
124
|
+
const inherited = signal<string | undefined>('var(--destructive)');
|
|
125
|
+
const resolve = setup([{ provide: UI_ICON_COLOR, useValue: inherited }]);
|
|
126
|
+
expect(resolve(undefined)).toBe(ICON_LIGHT_COLORS['destructive']);
|
|
127
|
+
// The icon's own color still wins.
|
|
128
|
+
expect(resolve('var(--primary)')).toBe(ICON_LIGHT_COLORS['primary']);
|
|
129
|
+
// A container that sets no color leaves the foreground.
|
|
130
|
+
inherited.set(undefined);
|
|
131
|
+
expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
InjectionToken,
|
|
5
|
+
type Provider,
|
|
6
|
+
type Signal,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { LynxTheme } from '@blotch/angular-lynx';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Colors for the components that draw with Lynx's `<svg>`: `<ui-icon>`,
|
|
12
|
+
* `<ui-spinner>`, and the parts built on them (pagination's chevrons, a loading
|
|
13
|
+
* button's spinner).
|
|
14
|
+
*
|
|
15
|
+
* ## Why colors live in TypeScript as well as CSS
|
|
16
|
+
* `<svg>` parses its markup by itself. It never sees the page's CSS: on the web
|
|
17
|
+
* preview the markup becomes an `<img>`, and the native elements in Lynx 4.1
|
|
18
|
+
* read only a literal `current-color`. So `var(--foreground)` and text color
|
|
19
|
+
* classes can't reach a stroke, and Lynx has no computed-style API to read a
|
|
20
|
+
* variable's value from JS. Instead these components take `var(--token)` colors
|
|
21
|
+
* and swap each for its value in the active table below before they write the
|
|
22
|
+
* markup. This mirrors how `<ui-chart>` colors its marks (`chart-theme.ts`).
|
|
23
|
+
*
|
|
24
|
+
* The tables mirror `default.css` and `dark.css` (a test in the theme folder
|
|
25
|
+
* keeps them in step). If you change a color there, change it here too, or pass
|
|
26
|
+
* your own with {@link provideIconTheme}.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Theme token name (without `--`) to a complete CSS color. */
|
|
30
|
+
export type IconColorTokens = Readonly<Record<string, string>>;
|
|
31
|
+
|
|
32
|
+
export type IconColorScheme = 'light' | 'dark';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which table the colors come from: a fixed scheme, `'system'` to follow the
|
|
36
|
+
* device appearance (`LynxTheme`), or a function reading the app's own theme
|
|
37
|
+
* state.
|
|
38
|
+
*
|
|
39
|
+
* It has to match how the app switches its CSS theme, since the text next to an
|
|
40
|
+
* icon reads the CSS variables. The ui theme's dark mode is opt-in (the app
|
|
41
|
+
* toggles the `.dark` class), so the default is `'light'`: an icon in an app
|
|
42
|
+
* without dark mode stays visible when the device is dark.
|
|
43
|
+
*/
|
|
44
|
+
export type IconColorSchemeOption =
|
|
45
|
+
| IconColorScheme
|
|
46
|
+
| 'system'
|
|
47
|
+
| (() => IconColorScheme);
|
|
48
|
+
|
|
49
|
+
export type UiIconTheme = {
|
|
50
|
+
/** Token values for light mode; mirrors `default.css`. */
|
|
51
|
+
readonly light: IconColorTokens;
|
|
52
|
+
/** Token values for dark mode; mirrors `dark.css`. */
|
|
53
|
+
readonly dark: IconColorTokens;
|
|
54
|
+
/** See {@link IconColorSchemeOption}; `'light'` when unset. */
|
|
55
|
+
readonly colorScheme?: IconColorSchemeOption;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const ICON_LIGHT_COLORS: IconColorTokens = {
|
|
59
|
+
background: 'rgba(255, 255, 255, 1)',
|
|
60
|
+
foreground: 'rgba(9, 9, 11, 1)',
|
|
61
|
+
card: 'rgba(255, 255, 255, 1)',
|
|
62
|
+
'card-foreground': 'rgba(9, 9, 11, 1)',
|
|
63
|
+
primary: 'rgba(24, 24, 27, 1)',
|
|
64
|
+
'primary-foreground': 'rgba(250, 250, 250, 1)',
|
|
65
|
+
secondary: 'rgba(244, 244, 245, 1)',
|
|
66
|
+
'secondary-foreground': 'rgba(24, 24, 27, 1)',
|
|
67
|
+
muted: 'rgba(244, 244, 245, 1)',
|
|
68
|
+
'muted-foreground': 'rgba(113, 113, 122, 1)',
|
|
69
|
+
accent: 'rgba(244, 244, 245, 1)',
|
|
70
|
+
'accent-foreground': 'rgba(24, 24, 27, 1)',
|
|
71
|
+
destructive: 'rgba(239, 68, 68, 1)',
|
|
72
|
+
'destructive-foreground': 'rgba(250, 250, 250, 1)',
|
|
73
|
+
'destructive-subtle': 'rgba(239, 68, 68, 0.1)',
|
|
74
|
+
border: 'rgba(228, 228, 231, 1)',
|
|
75
|
+
input: 'rgba(228, 228, 231, 1)',
|
|
76
|
+
ring: 'rgba(24, 24, 27, 1)',
|
|
77
|
+
'ring-subtle': 'rgba(24, 24, 27, 0.4)',
|
|
78
|
+
'chart-1': 'rgba(231, 110, 80, 1)',
|
|
79
|
+
'chart-2': 'rgba(42, 157, 144, 1)',
|
|
80
|
+
'chart-3': 'rgba(39, 71, 84, 1)',
|
|
81
|
+
'chart-4': 'rgba(232, 196, 104, 1)',
|
|
82
|
+
'chart-5': 'rgba(244, 164, 98, 1)',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const ICON_DARK_COLORS: IconColorTokens = {
|
|
86
|
+
background: 'rgba(9, 9, 11, 1)',
|
|
87
|
+
foreground: 'rgba(250, 250, 250, 1)',
|
|
88
|
+
card: 'rgba(9, 9, 11, 1)',
|
|
89
|
+
'card-foreground': 'rgba(250, 250, 250, 1)',
|
|
90
|
+
primary: 'rgba(250, 250, 250, 1)',
|
|
91
|
+
'primary-foreground': 'rgba(24, 24, 27, 1)',
|
|
92
|
+
secondary: 'rgba(39, 39, 42, 1)',
|
|
93
|
+
'secondary-foreground': 'rgba(250, 250, 250, 1)',
|
|
94
|
+
muted: 'rgba(39, 39, 42, 1)',
|
|
95
|
+
'muted-foreground': 'rgba(161, 161, 170, 1)',
|
|
96
|
+
accent: 'rgba(39, 39, 42, 1)',
|
|
97
|
+
'accent-foreground': 'rgba(250, 250, 250, 1)',
|
|
98
|
+
destructive: 'rgba(127, 29, 29, 1)',
|
|
99
|
+
'destructive-foreground': 'rgba(250, 250, 250, 1)',
|
|
100
|
+
'destructive-subtle': 'rgba(127, 29, 29, 0.1)',
|
|
101
|
+
border: 'rgba(39, 39, 42, 1)',
|
|
102
|
+
input: 'rgba(39, 39, 42, 1)',
|
|
103
|
+
ring: 'rgba(212, 212, 216, 1)',
|
|
104
|
+
'ring-subtle': 'rgba(212, 212, 216, 0.4)',
|
|
105
|
+
'chart-1': 'rgba(38, 98, 217, 1)',
|
|
106
|
+
'chart-2': 'rgba(46, 184, 138, 1)',
|
|
107
|
+
'chart-3': 'rgba(232, 140, 48, 1)',
|
|
108
|
+
'chart-4': 'rgba(175, 87, 219, 1)',
|
|
109
|
+
'chart-5': 'rgba(226, 54, 112, 1)',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const UI_ICON_THEME = new InjectionToken<UiIconTheme>('UI_ICON_THEME', {
|
|
113
|
+
providedIn: 'root',
|
|
114
|
+
factory: () => ({ light: ICON_LIGHT_COLORS, dark: ICON_DARK_COLORS }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Overrides icon colors, merged over the defaults: pass only the tokens you
|
|
119
|
+
* changed in your CSS, or just a color scheme.
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* // An app that switches .dark with the device appearance:
|
|
123
|
+
* providers: [provideIconTheme({ colorScheme: 'system' })]
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export const provideIconTheme = (theme: {
|
|
127
|
+
light?: IconColorTokens;
|
|
128
|
+
dark?: IconColorTokens;
|
|
129
|
+
colorScheme?: IconColorSchemeOption;
|
|
130
|
+
}): Provider => ({
|
|
131
|
+
provide: UI_ICON_THEME,
|
|
132
|
+
useValue: {
|
|
133
|
+
light: { ...ICON_LIGHT_COLORS, ...theme.light },
|
|
134
|
+
dark: { ...ICON_DARK_COLORS, ...theme.dark },
|
|
135
|
+
colorScheme: theme.colorScheme,
|
|
136
|
+
} satisfies UiIconTheme,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The color icons inside an element use when they're given none. A container
|
|
141
|
+
* whose variant recolors its content (a destructive nav drawer item) provides
|
|
142
|
+
* it, and every `<ui-icon>` projected into it picks it up through DI, the way
|
|
143
|
+
* text would inherit `color` on the web.
|
|
144
|
+
*/
|
|
145
|
+
export const UI_ICON_COLOR = new InjectionToken<Signal<string | undefined>>(
|
|
146
|
+
'UI_ICON_COLOR',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
/** What an icon draws with when nothing sets its color. */
|
|
150
|
+
export const DEFAULT_ICON_COLOR = 'var(--foreground)';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Turns a color into one an SVG parser can draw: `var(--token)` (with or
|
|
154
|
+
* without a fallback) becomes the token's value. Anything else (`#2563eb`,
|
|
155
|
+
* `rgba(…)`) is already concrete and passes through.
|
|
156
|
+
*/
|
|
157
|
+
export const resolveIconColor = (
|
|
158
|
+
color: string,
|
|
159
|
+
tokens: IconColorTokens,
|
|
160
|
+
): string => {
|
|
161
|
+
const value = color.trim();
|
|
162
|
+
if (!value.startsWith('var(') || !value.endsWith(')')) return value;
|
|
163
|
+
|
|
164
|
+
const inner = value.slice(4, -1);
|
|
165
|
+
// A custom property name can't contain a comma, so the first comma always
|
|
166
|
+
// separates the name from the fallback.
|
|
167
|
+
const comma = inner.indexOf(',');
|
|
168
|
+
const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
|
|
169
|
+
const token = tokens[name.replace(/^--/, '')];
|
|
170
|
+
if (token !== undefined) return token;
|
|
171
|
+
// An unknown token falls back the way CSS does: to the var()'s fallback when
|
|
172
|
+
// it has one, and otherwise to the foreground so the icon stays visible.
|
|
173
|
+
return comma < 0
|
|
174
|
+
? (tokens['foreground'] ?? value)
|
|
175
|
+
: resolveIconColor(inner.slice(comma + 1), tokens);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* For an SVG component's constructor: a function that resolves its `color`
|
|
180
|
+
* input (or, without one, the surrounding {@link UI_ICON_COLOR}, then the
|
|
181
|
+
* foreground) against the active color table. It reads signals, so a
|
|
182
|
+
* `computed()` calling it follows scheme and theme changes.
|
|
183
|
+
*/
|
|
184
|
+
export const injectIconColor = (): ((color: string | undefined) => string) => {
|
|
185
|
+
const theme = inject(UI_ICON_THEME);
|
|
186
|
+
const appearance = inject(LynxTheme);
|
|
187
|
+
const inherited = inject(UI_ICON_COLOR, { optional: true });
|
|
188
|
+
|
|
189
|
+
const tokens = computed(() => {
|
|
190
|
+
const option = theme.colorScheme ?? 'light';
|
|
191
|
+
const scheme =
|
|
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
|
+
});
|
|
201
|
+
|
|
202
|
+
return (color) =>
|
|
203
|
+
resolveIconColor(color ?? inherited?.() ?? DEFAULT_ICON_COLOR, tokens());
|
|
204
|
+
};
|
|
@@ -1,13 +1,18 @@
|
|
|
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
5
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
6
|
import { UiIcon } from './icon';
|
|
7
|
+
import {
|
|
8
|
+
ICON_DARK_COLORS,
|
|
9
|
+
ICON_LIGHT_COLORS,
|
|
10
|
+
UI_ICON_COLOR,
|
|
11
|
+
provideIconTheme,
|
|
12
|
+
} from './icon-theme';
|
|
7
13
|
import { ICONS, type IconName } from './icons';
|
|
8
14
|
|
|
9
15
|
@Component({
|
|
10
|
-
standalone: true,
|
|
11
16
|
imports: [UiIcon, LYNX_ELEMENTS],
|
|
12
17
|
template: `<ui-icon [name]="name" [size]="size" [color]="color" />`,
|
|
13
18
|
})
|
|
@@ -43,6 +48,50 @@ describe('UiIcon', () => {
|
|
|
43
48
|
expect(svg?.getAttribute('content')).toContain('rgb(255, 0, 0)');
|
|
44
49
|
expect(svg?.getAttribute('content')).not.toContain('currentColor');
|
|
45
50
|
});
|
|
51
|
+
|
|
52
|
+
it("draws in the light theme's foreground when no color is set", async () => {
|
|
53
|
+
const { componentRef, container } = await render(UiIcon);
|
|
54
|
+
setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
|
|
55
|
+
await waitForUpdate();
|
|
56
|
+
const content = container.querySelector('svg')?.getAttribute('content');
|
|
57
|
+
expect(content).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
58
|
+
expect(content).not.toContain('currentColor');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('resolves a theme token against the active color scheme', async () => {
|
|
62
|
+
const { componentRef, container } = await render(UiIcon, {
|
|
63
|
+
providers: [provideIconTheme({ colorScheme: 'dark' })],
|
|
64
|
+
});
|
|
65
|
+
const icon = componentRef.instance as UiIcon;
|
|
66
|
+
setInputSignal(icon.name, 'check' as IconName);
|
|
67
|
+
setInputSignal(icon.color, 'var(--primary)');
|
|
68
|
+
await waitForUpdate();
|
|
69
|
+
expect(container.querySelector('svg')?.getAttribute('content')).toContain(
|
|
70
|
+
ICON_DARK_COLORS['primary'],
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("takes its container's color when it sets none", async () => {
|
|
75
|
+
const { componentRef, container } = await render(UiIcon, {
|
|
76
|
+
providers: [
|
|
77
|
+
{ provide: UI_ICON_COLOR, useValue: signal('var(--destructive)') },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
setInputSignal((componentRef.instance as UiIcon).name, 'x' as IconName);
|
|
81
|
+
await waitForUpdate();
|
|
82
|
+
expect(container.querySelector('svg')?.getAttribute('content')).toContain(
|
|
83
|
+
ICON_LIGHT_COLORS['destructive'],
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('applies the class input to the svg', async () => {
|
|
88
|
+
const { componentRef, container } = await render(UiIcon);
|
|
89
|
+
const icon = componentRef.instance as UiIcon;
|
|
90
|
+
setInputSignal(icon.name, 'check' as IconName);
|
|
91
|
+
setInputSignal(icon.userClass, 'mr-2');
|
|
92
|
+
await waitForUpdate();
|
|
93
|
+
expect(container.querySelector('svg')?.getAttribute('class')).toBe('mr-2');
|
|
94
|
+
});
|
|
46
95
|
});
|
|
47
96
|
|
|
48
97
|
describe('ICONS registry', () => {
|