@vespera-ui/angular 0.1.0 → 0.2.0
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/LICENSE +201 -0
- package/README.md +8 -3
- package/dist/README.md +58 -0
- package/dist/fesm2022/vespera-ui-angular.mjs +1825 -0
- package/dist/fesm2022/vespera-ui-angular.mjs.map +1 -0
- package/dist/index.d.ts +341 -0
- package/ng-package.json +7 -0
- package/package.json +13 -15
- package/src/alert.component.ts +24 -0
- package/src/badge.component.ts +21 -0
- package/src/button.component.ts +25 -0
- package/src/card.component.ts +30 -0
- package/src/data.component.ts +181 -0
- package/src/display.component.ts +29 -0
- package/src/feedback.component.ts +55 -0
- package/src/field.component.ts +61 -0
- package/src/forms.component.ts +113 -0
- package/src/media.component.ts +88 -0
- package/src/nav.component.ts +199 -0
- package/src/public-api.ts +13 -0
- package/src/structure.component.ts +162 -0
- package/src/toggle.component.ts +62 -0
- package/tsconfig.lib.json +21 -0
- package/fesm2022/vespera-ui-angular.mjs +0 -404
- package/fesm2022/vespera-ui-angular.mjs.map +0 -1
- package/index.d.ts +0 -101
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type TabItem = string | { value: string; label: string; count?: number };
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'vsp-tabs',
|
|
7
|
+
template: `<div class="ui-tabs" style="align-items: center">
|
|
8
|
+
@for (t of tabs; track id(t)) {
|
|
9
|
+
<button type="button" [class]="cls(t)" (click)="pick(id(t))">
|
|
10
|
+
{{ lbl(t) }}
|
|
11
|
+
@if (cnt(t) != null) {
|
|
12
|
+
<span class="badge badge-muted" style="margin-left: 7px">{{ cnt(t) }}</span>
|
|
13
|
+
}
|
|
14
|
+
</button>
|
|
15
|
+
}
|
|
16
|
+
<div style="flex: 1"></div>
|
|
17
|
+
<ng-content select="[slot=right]" />
|
|
18
|
+
</div>`,
|
|
19
|
+
})
|
|
20
|
+
export class VspTabs {
|
|
21
|
+
@Input() tabs: TabItem[] = [];
|
|
22
|
+
@Input() value?: string;
|
|
23
|
+
@Output() valueChange = new EventEmitter<string>();
|
|
24
|
+
id(t: TabItem): string {
|
|
25
|
+
return typeof t === 'string' ? t : t.value;
|
|
26
|
+
}
|
|
27
|
+
lbl(t: TabItem): string {
|
|
28
|
+
return typeof t === 'string' ? t : t.label;
|
|
29
|
+
}
|
|
30
|
+
cnt(t: TabItem): number | undefined {
|
|
31
|
+
return typeof t === 'object' ? t.count : undefined;
|
|
32
|
+
}
|
|
33
|
+
cls(t: TabItem): string {
|
|
34
|
+
return 'ui-tab' + (this.value === this.id(t) ? ' on' : '');
|
|
35
|
+
}
|
|
36
|
+
pick(v: string): void {
|
|
37
|
+
this.value = v;
|
|
38
|
+
this.valueChange.emit(v);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Component({
|
|
43
|
+
selector: 'vsp-breadcrumb',
|
|
44
|
+
template: `<nav style="display: flex; align-items: center; gap: 7px; font-size: 12.5px">
|
|
45
|
+
@for (it of items; track $index; let i = $index, last = $last) {
|
|
46
|
+
@if (i > 0) {
|
|
47
|
+
<svg
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
width="13"
|
|
50
|
+
height="13"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="2"
|
|
54
|
+
stroke-linecap="round"
|
|
55
|
+
stroke-linejoin="round"
|
|
56
|
+
style="color: var(--text-faint)"
|
|
57
|
+
>
|
|
58
|
+
<path d="M9 18l6-6-6-6" />
|
|
59
|
+
</svg>
|
|
60
|
+
}
|
|
61
|
+
<span
|
|
62
|
+
[style.color]="last ? 'var(--text)' : 'var(--text-dim)'"
|
|
63
|
+
[style.fontWeight]="last ? 600 : 500"
|
|
64
|
+
>{{ it }}</span
|
|
65
|
+
>
|
|
66
|
+
}
|
|
67
|
+
</nav>`,
|
|
68
|
+
})
|
|
69
|
+
export class VspBreadcrumb {
|
|
70
|
+
@Input() items: string[] = [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface PageItem {
|
|
74
|
+
gap: boolean;
|
|
75
|
+
n: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Component({
|
|
79
|
+
selector: 'vsp-pagination',
|
|
80
|
+
template: `<div style="display: flex; gap: 4px; align-items: center">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="btn btn-ghost btn-sm"
|
|
84
|
+
[disabled]="page === 0"
|
|
85
|
+
aria-label="Previous page"
|
|
86
|
+
(click)="go(page - 1)"
|
|
87
|
+
>
|
|
88
|
+
<svg
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
width="14"
|
|
91
|
+
height="14"
|
|
92
|
+
fill="none"
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
stroke-width="2"
|
|
95
|
+
stroke-linecap="round"
|
|
96
|
+
stroke-linejoin="round"
|
|
97
|
+
>
|
|
98
|
+
<path d="M15 18l-6-6 6-6" />
|
|
99
|
+
</svg>
|
|
100
|
+
</button>
|
|
101
|
+
@for (item of nums; track $index) {
|
|
102
|
+
@if (item.gap) {
|
|
103
|
+
<span class="mono" style="padding: 0 6px; color: var(--text-faint)">…</span>
|
|
104
|
+
} @else {
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
[class]="numCls(item.n)"
|
|
108
|
+
style="min-width: 32px; padding: 0"
|
|
109
|
+
(click)="go(item.n)"
|
|
110
|
+
>
|
|
111
|
+
{{ item.n + 1 }}
|
|
112
|
+
</button>
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="btn btn-ghost btn-sm"
|
|
118
|
+
[disabled]="page >= pages - 1"
|
|
119
|
+
aria-label="Next page"
|
|
120
|
+
(click)="go(page + 1)"
|
|
121
|
+
>
|
|
122
|
+
<svg
|
|
123
|
+
viewBox="0 0 24 24"
|
|
124
|
+
width="14"
|
|
125
|
+
height="14"
|
|
126
|
+
fill="none"
|
|
127
|
+
stroke="currentColor"
|
|
128
|
+
stroke-width="2"
|
|
129
|
+
stroke-linecap="round"
|
|
130
|
+
stroke-linejoin="round"
|
|
131
|
+
>
|
|
132
|
+
<path d="M9 18l6-6-6-6" />
|
|
133
|
+
</svg>
|
|
134
|
+
</button>
|
|
135
|
+
</div>`,
|
|
136
|
+
})
|
|
137
|
+
export class VspPagination {
|
|
138
|
+
@Input() page = 0;
|
|
139
|
+
@Output() pageChange = new EventEmitter<number>();
|
|
140
|
+
@Input() pages = 1;
|
|
141
|
+
get nums(): PageItem[] {
|
|
142
|
+
const r: PageItem[] = [];
|
|
143
|
+
for (let i = 0; i < this.pages; i++) {
|
|
144
|
+
if (i === 0 || i === this.pages - 1 || Math.abs(i - this.page) <= 1)
|
|
145
|
+
r.push({ gap: false, n: i });
|
|
146
|
+
else if (!r[r.length - 1]?.gap) r.push({ gap: true, n: -1 });
|
|
147
|
+
}
|
|
148
|
+
return r;
|
|
149
|
+
}
|
|
150
|
+
numCls(n: number): string {
|
|
151
|
+
return 'btn btn-sm ' + (n === this.page ? 'btn-primary' : 'btn-subtle');
|
|
152
|
+
}
|
|
153
|
+
go(p: number): void {
|
|
154
|
+
this.page = p;
|
|
155
|
+
this.pageChange.emit(p);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Component({
|
|
160
|
+
selector: 'vsp-stepper',
|
|
161
|
+
template: `<div class="ui-steps">
|
|
162
|
+
@for (s of steps; track $index; let i = $index) {
|
|
163
|
+
@if (i > 0) {
|
|
164
|
+
<div [class]="barCls(i)"></div>
|
|
165
|
+
}
|
|
166
|
+
<div [class]="stepCls(i)">
|
|
167
|
+
<span class="ui-step-dot">
|
|
168
|
+
@if (i < current) {
|
|
169
|
+
<svg
|
|
170
|
+
viewBox="0 0 24 24"
|
|
171
|
+
width="14"
|
|
172
|
+
height="14"
|
|
173
|
+
fill="none"
|
|
174
|
+
stroke="currentColor"
|
|
175
|
+
stroke-width="2"
|
|
176
|
+
stroke-linecap="round"
|
|
177
|
+
stroke-linejoin="round"
|
|
178
|
+
>
|
|
179
|
+
<path d="M20 6L9 17l-5-5" />
|
|
180
|
+
</svg>
|
|
181
|
+
} @else {
|
|
182
|
+
{{ i + 1 }}
|
|
183
|
+
}
|
|
184
|
+
</span>
|
|
185
|
+
<span class="ui-step-label">{{ s }}</span>
|
|
186
|
+
</div>
|
|
187
|
+
}
|
|
188
|
+
</div>`,
|
|
189
|
+
})
|
|
190
|
+
export class VspStepper {
|
|
191
|
+
@Input() steps: string[] = [];
|
|
192
|
+
@Input() current = 0;
|
|
193
|
+
barCls(i: number): string {
|
|
194
|
+
return 'ui-step-bar' + (i <= this.current ? ' done' : '');
|
|
195
|
+
}
|
|
196
|
+
stepCls(i: number): string {
|
|
197
|
+
return 'ui-step ' + (i < this.current ? 'done' : i === this.current ? 'active' : 'pending');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './button.component';
|
|
2
|
+
export * from './badge.component';
|
|
3
|
+
export * from './display.component';
|
|
4
|
+
export * from './media.component';
|
|
5
|
+
export * from './feedback.component';
|
|
6
|
+
export * from './card.component';
|
|
7
|
+
export * from './alert.component';
|
|
8
|
+
export * from './field.component';
|
|
9
|
+
export * from './toggle.component';
|
|
10
|
+
export * from './forms.component';
|
|
11
|
+
export * from './nav.component';
|
|
12
|
+
export * from './data.component';
|
|
13
|
+
export * from './structure.component';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type BannerTone = 'info' | 'warn' | 'accent';
|
|
4
|
+
const BANNER_ICON: Record<BannerTone, string> = {
|
|
5
|
+
info: 'M12 3l1.6 5L19 9.6l-5 1.6L12 16l-1.6-4.8L5 9.6l5.4-1.6z',
|
|
6
|
+
warn: 'M18 8a6 6 0 00-12 0c0 7-3 9-3 9h18s-3-2-3-9',
|
|
7
|
+
accent: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'vsp-banner',
|
|
12
|
+
template: `<div [class]="cls">
|
|
13
|
+
<span #ic style="display: contents"><ng-content select="[slot=icon]" /></span>
|
|
14
|
+
@if (!ic.childElementCount) {
|
|
15
|
+
<svg
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
width="18"
|
|
18
|
+
height="18"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
stroke-width="2"
|
|
22
|
+
stroke-linecap="round"
|
|
23
|
+
stroke-linejoin="round"
|
|
24
|
+
>
|
|
25
|
+
<path [attr.d]="iconPath" />
|
|
26
|
+
</svg>
|
|
27
|
+
}
|
|
28
|
+
<div style="flex: 1; font-size: 13px; font-weight: 500"><ng-content /></div>
|
|
29
|
+
<ng-content select="[slot=action]" />
|
|
30
|
+
@if (dismissible) {
|
|
31
|
+
<button type="button" class="ui-banner-x" aria-label="Dismiss" (click)="dismiss.emit()">
|
|
32
|
+
<svg
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
width="15"
|
|
35
|
+
height="15"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="2"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
>
|
|
42
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
43
|
+
</svg>
|
|
44
|
+
</button>
|
|
45
|
+
}
|
|
46
|
+
</div>`,
|
|
47
|
+
})
|
|
48
|
+
export class VspBanner {
|
|
49
|
+
@Input() tone: BannerTone = 'info';
|
|
50
|
+
@Input() dismissible = false;
|
|
51
|
+
@Output() dismiss = new EventEmitter<void>();
|
|
52
|
+
get cls(): string {
|
|
53
|
+
return 'ui-banner ' + this.tone;
|
|
54
|
+
}
|
|
55
|
+
get iconPath(): string {
|
|
56
|
+
return BANNER_ICON[this.tone];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Component({
|
|
61
|
+
selector: 'vsp-empty-state',
|
|
62
|
+
template: `<div
|
|
63
|
+
style="display: grid; place-items: center; text-align: center"
|
|
64
|
+
[style.padding]="compact ? '32px 20px' : '56px 24px'"
|
|
65
|
+
>
|
|
66
|
+
<div style="max-width: 340px">
|
|
67
|
+
<span
|
|
68
|
+
style="width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; margin: 0 auto 18px; background: color-mix(in oklab, var(--accent) 12%, transparent); color: var(--accent); border: 1px solid color-mix(in oklab, var(--accent) 22%, transparent)"
|
|
69
|
+
>
|
|
70
|
+
<span #ic style="display: contents"><ng-content select="[slot=icon]" /></span>
|
|
71
|
+
@if (!ic.childElementCount) {
|
|
72
|
+
<svg
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
width="26"
|
|
75
|
+
height="26"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
stroke-width="2"
|
|
79
|
+
stroke-linecap="round"
|
|
80
|
+
stroke-linejoin="round"
|
|
81
|
+
>
|
|
82
|
+
<path
|
|
83
|
+
d="M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"
|
|
84
|
+
/>
|
|
85
|
+
</svg>
|
|
86
|
+
}
|
|
87
|
+
</span>
|
|
88
|
+
<div style="font-size: 17px; font-weight: 700">{{ title }}</div>
|
|
89
|
+
@if (desc) {
|
|
90
|
+
<p style="margin: 7px 0 0; color: var(--text-dim); font-size: 13.5px; line-height: 1.6">
|
|
91
|
+
{{ desc }}
|
|
92
|
+
</p>
|
|
93
|
+
}
|
|
94
|
+
<div
|
|
95
|
+
#ac
|
|
96
|
+
[style.display]="ac.childElementCount ? 'flex' : 'none'"
|
|
97
|
+
style="margin-top: 20px; gap: 8px; justify-content: center"
|
|
98
|
+
>
|
|
99
|
+
<ng-content select="[slot=action]" />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>`,
|
|
103
|
+
})
|
|
104
|
+
export class VspEmptyState {
|
|
105
|
+
@Input() title?: string;
|
|
106
|
+
@Input() desc?: string;
|
|
107
|
+
@Input() compact = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface AccordionItem {
|
|
111
|
+
title: string;
|
|
112
|
+
body: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Component({
|
|
116
|
+
selector: 'vsp-accordion',
|
|
117
|
+
template: `<div class="ui-acc">
|
|
118
|
+
@for (it of items; track $index; let i = $index) {
|
|
119
|
+
<div [class]="itemCls(i)">
|
|
120
|
+
<button type="button" class="ui-acc-head" (click)="toggle(i)">
|
|
121
|
+
{{ it.title }}
|
|
122
|
+
<svg
|
|
123
|
+
class="chev"
|
|
124
|
+
viewBox="0 0 24 24"
|
|
125
|
+
width="17"
|
|
126
|
+
height="17"
|
|
127
|
+
fill="none"
|
|
128
|
+
stroke="currentColor"
|
|
129
|
+
stroke-width="2"
|
|
130
|
+
stroke-linecap="round"
|
|
131
|
+
stroke-linejoin="round"
|
|
132
|
+
>
|
|
133
|
+
<path d="M9 18l6-6-6-6" />
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
<div class="ui-acc-bodywrap">
|
|
137
|
+
<div>
|
|
138
|
+
<div class="ui-acc-body">{{ it.body }}</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
}
|
|
143
|
+
</div>`,
|
|
144
|
+
})
|
|
145
|
+
export class VspAccordion implements OnInit {
|
|
146
|
+
@Input() items: AccordionItem[] = [];
|
|
147
|
+
@Input() multiple = false;
|
|
148
|
+
@Input() defaultOpen: number[] = [];
|
|
149
|
+
private open = new Set<number>();
|
|
150
|
+
ngOnInit(): void {
|
|
151
|
+
this.open = new Set(this.defaultOpen);
|
|
152
|
+
}
|
|
153
|
+
toggle(i: number): void {
|
|
154
|
+
const n = new Set<number>(this.multiple ? this.open : []);
|
|
155
|
+
if (this.open.has(i)) n.delete(i);
|
|
156
|
+
else n.add(i);
|
|
157
|
+
this.open = n;
|
|
158
|
+
}
|
|
159
|
+
itemCls(i: number): string {
|
|
160
|
+
return 'ui-acc-item' + (this.open.has(i) ? ' open' : '');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'vsp-switch',
|
|
5
|
+
template: `<button
|
|
6
|
+
type="button"
|
|
7
|
+
[disabled]="disabled"
|
|
8
|
+
[class]="cls"
|
|
9
|
+
[attr.aria-pressed]="checked"
|
|
10
|
+
aria-label="Toggle"
|
|
11
|
+
(click)="toggle()"
|
|
12
|
+
></button>`,
|
|
13
|
+
})
|
|
14
|
+
export class VspSwitch {
|
|
15
|
+
@Input() checked = false;
|
|
16
|
+
@Output() checkedChange = new EventEmitter<boolean>();
|
|
17
|
+
@Input() size?: 'sm';
|
|
18
|
+
@Input() disabled = false;
|
|
19
|
+
get cls(): string {
|
|
20
|
+
return ['ui-switch', this.size === 'sm' && 'sm', this.checked && 'on']
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(' ');
|
|
23
|
+
}
|
|
24
|
+
toggle(): void {
|
|
25
|
+
this.checked = !this.checked;
|
|
26
|
+
this.checkedChange.emit(this.checked);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'vsp-checkbox',
|
|
32
|
+
template: `<label class="ui-opt" [style.opacity]="disabled ? 0.5 : 1">
|
|
33
|
+
<input
|
|
34
|
+
type="checkbox"
|
|
35
|
+
[checked]="checked"
|
|
36
|
+
[disabled]="disabled"
|
|
37
|
+
(change)="toggle()"
|
|
38
|
+
style="position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0"
|
|
39
|
+
/>
|
|
40
|
+
<span [class]="checkCls"></span>
|
|
41
|
+
<span>
|
|
42
|
+
<span>{{ label }}</span>
|
|
43
|
+
@if (sub) {
|
|
44
|
+
<span class="ui-opt-sub">{{ sub }}</span>
|
|
45
|
+
}
|
|
46
|
+
</span>
|
|
47
|
+
</label>`,
|
|
48
|
+
})
|
|
49
|
+
export class VspCheckbox {
|
|
50
|
+
@Input() checked = false;
|
|
51
|
+
@Output() checkedChange = new EventEmitter<boolean>();
|
|
52
|
+
@Input() label?: string;
|
|
53
|
+
@Input() sub?: string;
|
|
54
|
+
@Input() disabled = false;
|
|
55
|
+
get checkCls(): string {
|
|
56
|
+
return this.checked ? 'ui-check on' : 'ui-check';
|
|
57
|
+
}
|
|
58
|
+
toggle(): void {
|
|
59
|
+
this.checked = !this.checked;
|
|
60
|
+
this.checkedChange.emit(this.checked);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist/out-tsc",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"declarationMap": true,
|
|
6
|
+
"inlineSources": true,
|
|
7
|
+
"types": [],
|
|
8
|
+
"lib": ["ES2022", "dom"],
|
|
9
|
+
"target": "ES2022",
|
|
10
|
+
"module": "ES2022",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"experimentalDecorators": false,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true
|
|
15
|
+
},
|
|
16
|
+
"angularCompilerOptions": {
|
|
17
|
+
"strictTemplates": true,
|
|
18
|
+
"compilationMode": "partial"
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*.ts"]
|
|
21
|
+
}
|