@shimmer-from-structure/angular 2.3.0 → 2.3.2
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/README.md +1 -1
- package/fesm2022/shimmer-from-structure-angular.mjs +298 -0
- package/fesm2022/shimmer-from-structure-angular.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/package.json +17 -27
- package/{src/public-api.ts → public-api.d.ts} +1 -7
- package/{src/shimmer-config.service.ts → shimmer-config.service.d.ts} +7 -20
- package/shimmer.component.d.ts +48 -0
- package/types.d.ts +28 -0
- package/ng-package.json +0 -8
- package/src/shimmer-config.service.test.ts +0 -88
- package/src/shimmer.component.test.ts +0 -175
- package/src/shimmer.component.ts +0 -277
- package/src/test/setup.ts +0 -52
- package/src/types.ts +0 -34
- package/tsconfig.eslint.json +0 -5
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -12
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Shimmer From Structure
|
|
2
2
|
|
|
3
3
|
A **React, Vue, Svelte, Angular & SolidJS** shimmer/skeleton library that **automatically adapts to your component's runtime structure**. Unlike traditional shimmer libraries that require pre-defined skeleton structures, this library analyzes your actual component's DOM at runtime and generates a shimmer effect that perfectly matches its layout.
|
|
4
4
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, input, viewChild, signal, PLATFORM_ID, computed, effect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, NgIf, NgFor } from '@angular/common';
|
|
4
|
+
import { shimmerDefaults, createResizeObserver, extractElementInfo } from '@shimmer-from-structure/core';
|
|
5
|
+
export { shimmerDefaults } from '@shimmer-from-structure/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Injection token for global shimmer configuration.
|
|
9
|
+
* Use `provideShimmerConfig()` to configure in your app.
|
|
10
|
+
*/
|
|
11
|
+
const SHIMMER_CONFIG = new InjectionToken('SHIMMER_CONFIG');
|
|
12
|
+
/**
|
|
13
|
+
* Provider function for shimmer configuration.
|
|
14
|
+
* Use in your app's providers array.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* bootstrapApplication(AppComponent, {
|
|
19
|
+
* providers: [
|
|
20
|
+
* provideShimmerConfig({
|
|
21
|
+
* shimmerColor: 'rgba(255, 255, 255, 0.3)',
|
|
22
|
+
* duration: 1.5
|
|
23
|
+
* })
|
|
24
|
+
* ]
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function provideShimmerConfig(config) {
|
|
29
|
+
return { provide: SHIMMER_CONFIG, useValue: config };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Inject and resolve shimmer configuration.
|
|
33
|
+
* Merges injected config with defaults.
|
|
34
|
+
* Returns fully resolved ShimmerContextValue with all properties defined.
|
|
35
|
+
*/
|
|
36
|
+
function injectShimmerConfig() {
|
|
37
|
+
const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};
|
|
38
|
+
return {
|
|
39
|
+
shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,
|
|
40
|
+
backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,
|
|
41
|
+
duration: config.duration ?? shimmerDefaults.duration,
|
|
42
|
+
fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Shimmer component that creates loading skeleton overlays based on content structure.
|
|
48
|
+
* Automatically measures projected content and creates matching shimmer blocks.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```html
|
|
52
|
+
* <shimmer [loading]="isLoading">
|
|
53
|
+
* <div class="card">
|
|
54
|
+
* <h2>{{ title }}</h2>
|
|
55
|
+
* <p>{{ description }}</p>
|
|
56
|
+
* </div>
|
|
57
|
+
* </shimmer>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
class ShimmerComponent {
|
|
61
|
+
// Inputs using Angular signals
|
|
62
|
+
loading = input(true);
|
|
63
|
+
shimmerColor = input(undefined);
|
|
64
|
+
backgroundColor = input(undefined);
|
|
65
|
+
duration = input(undefined);
|
|
66
|
+
fallbackBorderRadius = input(undefined);
|
|
67
|
+
// View child reference
|
|
68
|
+
measureContainer = viewChild('measureContainer');
|
|
69
|
+
// Internal state
|
|
70
|
+
elements = signal([]);
|
|
71
|
+
// Inject dependencies
|
|
72
|
+
contextConfig = injectShimmerConfig();
|
|
73
|
+
platformId = inject(PLATFORM_ID);
|
|
74
|
+
isBrowser = isPlatformBrowser(this.platformId);
|
|
75
|
+
// Resolved values (props > context > defaults)
|
|
76
|
+
resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);
|
|
77
|
+
resolvedBackgroundColor = computed(() => this.backgroundColor() ?? this.contextConfig.backgroundColor);
|
|
78
|
+
resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);
|
|
79
|
+
resolvedFallbackBorderRadius = computed(() => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius);
|
|
80
|
+
// Cleanup function for ResizeObserver
|
|
81
|
+
resizeCleanup;
|
|
82
|
+
mutationObserver;
|
|
83
|
+
constructor() {
|
|
84
|
+
// Effect to re-measure when loading state changes
|
|
85
|
+
effect((onCleanup) => {
|
|
86
|
+
// Skip effect on server
|
|
87
|
+
if (!this.isBrowser)
|
|
88
|
+
return;
|
|
89
|
+
const isLoading = this.loading();
|
|
90
|
+
const container = this.measureContainer();
|
|
91
|
+
if (isLoading && container) {
|
|
92
|
+
// Clean up existing observers before setting up new ones
|
|
93
|
+
this.cleanup();
|
|
94
|
+
// Set up observers for this loading session
|
|
95
|
+
this.setupObservers();
|
|
96
|
+
// Defer measurement to next frame to ensure content is rendered
|
|
97
|
+
requestAnimationFrame(() => this.measureElements());
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Cleanup when not loading
|
|
101
|
+
this.cleanup();
|
|
102
|
+
}
|
|
103
|
+
// Cleanup on effect re-run or component destruction
|
|
104
|
+
onCleanup(() => {
|
|
105
|
+
this.cleanup();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
ngAfterViewInit() {
|
|
110
|
+
// Effect will handle setup when container becomes available
|
|
111
|
+
}
|
|
112
|
+
ngOnDestroy() {
|
|
113
|
+
this.cleanup();
|
|
114
|
+
}
|
|
115
|
+
setupObservers() {
|
|
116
|
+
if (!this.isBrowser)
|
|
117
|
+
return;
|
|
118
|
+
const container = this.measureContainer()?.nativeElement;
|
|
119
|
+
if (!container)
|
|
120
|
+
return;
|
|
121
|
+
// Set up ResizeObserver
|
|
122
|
+
this.resizeCleanup = createResizeObserver(container, () => this.measureElements());
|
|
123
|
+
// Set up MutationObserver for content changes
|
|
124
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
125
|
+
if (this.loading()) {
|
|
126
|
+
this.measureElements();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
this.mutationObserver.observe(container, {
|
|
130
|
+
childList: true,
|
|
131
|
+
subtree: true,
|
|
132
|
+
characterData: true,
|
|
133
|
+
attributes: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
measureElements() {
|
|
137
|
+
if (!this.isBrowser)
|
|
138
|
+
return;
|
|
139
|
+
const container = this.measureContainer()?.nativeElement;
|
|
140
|
+
if (!container || !this.loading())
|
|
141
|
+
return;
|
|
142
|
+
// Temporarily disconnect mutation observer to avoid recursion
|
|
143
|
+
this.mutationObserver?.disconnect();
|
|
144
|
+
const containerRect = container.getBoundingClientRect();
|
|
145
|
+
const extractedElements = [];
|
|
146
|
+
Array.from(container.children).forEach((child) => {
|
|
147
|
+
extractedElements.push(...extractElementInfo(child, containerRect));
|
|
148
|
+
});
|
|
149
|
+
this.elements.set(extractedElements);
|
|
150
|
+
// Reconnect mutation observer
|
|
151
|
+
if (this.mutationObserver && container) {
|
|
152
|
+
this.mutationObserver.observe(container, {
|
|
153
|
+
childList: true,
|
|
154
|
+
subtree: true,
|
|
155
|
+
characterData: true,
|
|
156
|
+
attributes: false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
cleanup() {
|
|
161
|
+
if (this.resizeCleanup) {
|
|
162
|
+
this.resizeCleanup();
|
|
163
|
+
this.resizeCleanup = undefined;
|
|
164
|
+
}
|
|
165
|
+
if (this.mutationObserver) {
|
|
166
|
+
this.mutationObserver.disconnect();
|
|
167
|
+
this.mutationObserver = undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Manually trigger re-measurement of elements.
|
|
172
|
+
* Useful when content changes programmatically.
|
|
173
|
+
*/
|
|
174
|
+
remeasure() {
|
|
175
|
+
if (this.isBrowser) {
|
|
176
|
+
this.measureElements();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ShimmerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
180
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: ShimmerComponent, isStandalone: true, selector: "shimmer", inputs: { loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, shimmerColor: { classPropertyName: "shimmerColor", publicName: "shimmerColor", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, fallbackBorderRadius: { classPropertyName: "fallbackBorderRadius", publicName: "fallbackBorderRadius", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "measureContainer", first: true, predicate: ["measureContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
181
|
+
<div style="position: relative;">
|
|
182
|
+
<!-- Always render content -->
|
|
183
|
+
<div
|
|
184
|
+
#measureContainer
|
|
185
|
+
[class.shimmer-measure-container]="loading()"
|
|
186
|
+
[attr.aria-hidden]="loading() ? 'true' : null"
|
|
187
|
+
[style.pointer-events]="loading() ? 'none' : null"
|
|
188
|
+
>
|
|
189
|
+
<ng-content></ng-content>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Shimmer overlay - only when loading -->
|
|
193
|
+
@if (loading()) {
|
|
194
|
+
<div
|
|
195
|
+
style="
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 0;
|
|
198
|
+
left: 0;
|
|
199
|
+
right: 0;
|
|
200
|
+
bottom: 0;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
pointer-events: none;
|
|
203
|
+
"
|
|
204
|
+
>
|
|
205
|
+
@for (element of elements(); track $index) {
|
|
206
|
+
<div
|
|
207
|
+
[style.position]="'absolute'"
|
|
208
|
+
[style.left.px]="element.x"
|
|
209
|
+
[style.top.px]="element.y"
|
|
210
|
+
[style.width.px]="element.width"
|
|
211
|
+
[style.height.px]="element.height"
|
|
212
|
+
[style.backgroundColor]="resolvedBackgroundColor()"
|
|
213
|
+
[style.borderRadius]="
|
|
214
|
+
element.borderRadius === '0px'
|
|
215
|
+
? resolvedFallbackBorderRadius() + 'px'
|
|
216
|
+
: element.borderRadius
|
|
217
|
+
"
|
|
218
|
+
[style.overflow]="'hidden'"
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
class="shimmer-animation-element"
|
|
222
|
+
[style.background]="
|
|
223
|
+
'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
|
|
224
|
+
"
|
|
225
|
+
[style.animationDuration]="resolvedDuration() + 's'"
|
|
226
|
+
></div>
|
|
227
|
+
</div>
|
|
228
|
+
}
|
|
229
|
+
</div>
|
|
230
|
+
}
|
|
231
|
+
</div>
|
|
232
|
+
`, isInline: true, styles: [":host{display:contents}.shimmer-measure-container *{color:transparent!important}.shimmer-measure-container img,.shimmer-measure-container svg,.shimmer-measure-container video{opacity:0}.shimmer-animation-element{position:absolute;top:0;left:0;width:100%;height:100%;animation:shimmer-animation 1.5s infinite}@keyframes shimmer-animation{0%{transform:translate(-100%)}to{transform:translate(100%)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
233
|
+
}
|
|
234
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ShimmerComponent, decorators: [{
|
|
235
|
+
type: Component,
|
|
236
|
+
args: [{ selector: 'shimmer', standalone: true, imports: [NgIf, NgFor], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
|
|
237
|
+
<div style="position: relative;">
|
|
238
|
+
<!-- Always render content -->
|
|
239
|
+
<div
|
|
240
|
+
#measureContainer
|
|
241
|
+
[class.shimmer-measure-container]="loading()"
|
|
242
|
+
[attr.aria-hidden]="loading() ? 'true' : null"
|
|
243
|
+
[style.pointer-events]="loading() ? 'none' : null"
|
|
244
|
+
>
|
|
245
|
+
<ng-content></ng-content>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<!-- Shimmer overlay - only when loading -->
|
|
249
|
+
@if (loading()) {
|
|
250
|
+
<div
|
|
251
|
+
style="
|
|
252
|
+
position: absolute;
|
|
253
|
+
top: 0;
|
|
254
|
+
left: 0;
|
|
255
|
+
right: 0;
|
|
256
|
+
bottom: 0;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
pointer-events: none;
|
|
259
|
+
"
|
|
260
|
+
>
|
|
261
|
+
@for (element of elements(); track $index) {
|
|
262
|
+
<div
|
|
263
|
+
[style.position]="'absolute'"
|
|
264
|
+
[style.left.px]="element.x"
|
|
265
|
+
[style.top.px]="element.y"
|
|
266
|
+
[style.width.px]="element.width"
|
|
267
|
+
[style.height.px]="element.height"
|
|
268
|
+
[style.backgroundColor]="resolvedBackgroundColor()"
|
|
269
|
+
[style.borderRadius]="
|
|
270
|
+
element.borderRadius === '0px'
|
|
271
|
+
? resolvedFallbackBorderRadius() + 'px'
|
|
272
|
+
: element.borderRadius
|
|
273
|
+
"
|
|
274
|
+
[style.overflow]="'hidden'"
|
|
275
|
+
>
|
|
276
|
+
<div
|
|
277
|
+
class="shimmer-animation-element"
|
|
278
|
+
[style.background]="
|
|
279
|
+
'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
|
|
280
|
+
"
|
|
281
|
+
[style.animationDuration]="resolvedDuration() + 's'"
|
|
282
|
+
></div>
|
|
283
|
+
</div>
|
|
284
|
+
}
|
|
285
|
+
</div>
|
|
286
|
+
}
|
|
287
|
+
</div>
|
|
288
|
+
`, styles: [":host{display:contents}.shimmer-measure-container *{color:transparent!important}.shimmer-measure-container img,.shimmer-measure-container svg,.shimmer-measure-container video{opacity:0}.shimmer-animation-element{position:absolute;top:0;left:0;width:100%;height:100%;animation:shimmer-animation 1.5s infinite}@keyframes shimmer-animation{0%{transform:translate(-100%)}to{transform:translate(100%)}}\n"] }]
|
|
289
|
+
}], ctorParameters: () => [] });
|
|
290
|
+
|
|
291
|
+
// Public API Surface
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generated bundle index. Do not edit.
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
export { SHIMMER_CONFIG, ShimmerComponent, injectShimmerConfig, provideShimmerConfig };
|
|
298
|
+
//# sourceMappingURL=shimmer-from-structure-angular.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shimmer-from-structure-angular.mjs","sources":["../../src/shimmer-config.service.ts","../../src/shimmer.component.ts","../../src/public-api.ts","../../src/shimmer-from-structure-angular.ts"],"sourcesContent":["import { InjectionToken, inject } from '@angular/core';\nimport type { ShimmerConfig, ShimmerContextValue } from '@shimmer-from-structure/core';\nimport { shimmerDefaults } from '@shimmer-from-structure/core';\n\n/**\n * Injection token for global shimmer configuration.\n * Use `provideShimmerConfig()` to configure in your app.\n */\nexport const SHIMMER_CONFIG = new InjectionToken<ShimmerConfig>('SHIMMER_CONFIG');\n\n/**\n * Provider function for shimmer configuration.\n * Use in your app's providers array.\n *\n * @example\n * ```typescript\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideShimmerConfig({\n * shimmerColor: 'rgba(255, 255, 255, 0.3)',\n * duration: 1.5\n * })\n * ]\n * });\n * ```\n */\nexport function provideShimmerConfig(config: ShimmerConfig) {\n return { provide: SHIMMER_CONFIG, useValue: config };\n}\n\n/**\n * Inject and resolve shimmer configuration.\n * Merges injected config with defaults.\n * Returns fully resolved ShimmerContextValue with all properties defined.\n */\nexport function injectShimmerConfig(): ShimmerContextValue {\n const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};\n\n return {\n shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,\n backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,\n duration: config.duration ?? shimmerDefaults.duration,\n fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,\n };\n}\n\n// Re-export defaults for testing and reference\nexport { shimmerDefaults };\n","import {\n Component,\n input,\n signal,\n computed,\n effect,\n ElementRef,\n viewChild,\n AfterViewInit,\n OnDestroy,\n ChangeDetectionStrategy,\n ViewEncapsulation,\n inject,\n PLATFORM_ID,\n} from '@angular/core';\nimport { NgIf, NgFor, isPlatformBrowser } from '@angular/common';\nimport {\n extractElementInfo,\n createResizeObserver,\n type ElementInfo,\n} from '@shimmer-from-structure/core';\nimport { injectShimmerConfig } from './shimmer-config.service';\n\n/**\n * Shimmer component that creates loading skeleton overlays based on content structure.\n * Automatically measures projected content and creates matching shimmer blocks.\n *\n * @example\n * ```html\n * <shimmer [loading]=\"isLoading\">\n * <div class=\"card\">\n * <h2>{{ title }}</h2>\n * <p>{{ description }}</p>\n * </div>\n * </shimmer>\n * ```\n */\n@Component({\n selector: 'shimmer',\n standalone: true,\n imports: [NgIf, NgFor],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n template: `\n <div style=\"position: relative;\">\n <!-- Always render content -->\n <div\n #measureContainer\n [class.shimmer-measure-container]=\"loading()\"\n [attr.aria-hidden]=\"loading() ? 'true' : null\"\n [style.pointer-events]=\"loading() ? 'none' : null\"\n >\n <ng-content></ng-content>\n </div>\n\n <!-- Shimmer overlay - only when loading -->\n @if (loading()) {\n <div\n style=\"\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n overflow: hidden;\n pointer-events: none;\n \"\n >\n @for (element of elements(); track $index) {\n <div\n [style.position]=\"'absolute'\"\n [style.left.px]=\"element.x\"\n [style.top.px]=\"element.y\"\n [style.width.px]=\"element.width\"\n [style.height.px]=\"element.height\"\n [style.backgroundColor]=\"resolvedBackgroundColor()\"\n [style.borderRadius]=\"\n element.borderRadius === '0px'\n ? resolvedFallbackBorderRadius() + 'px'\n : element.borderRadius\n \"\n [style.overflow]=\"'hidden'\"\n >\n <div\n class=\"shimmer-animation-element\"\n [style.background]=\"\n 'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'\n \"\n [style.animationDuration]=\"resolvedDuration() + 's'\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: contents;\n }\n\n .shimmer-measure-container * {\n color: transparent !important;\n }\n\n .shimmer-measure-container img,\n .shimmer-measure-container svg,\n .shimmer-measure-container video {\n opacity: 0;\n }\n\n .shimmer-animation-element {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n animation: shimmer-animation 1.5s infinite;\n }\n\n @keyframes shimmer-animation {\n 0% {\n transform: translateX(-100%);\n }\n 100% {\n transform: translateX(100%);\n }\n }\n `,\n ],\n})\nexport class ShimmerComponent implements AfterViewInit, OnDestroy {\n // Inputs using Angular signals\n loading = input<boolean>(true);\n shimmerColor = input<string | undefined>(undefined);\n backgroundColor = input<string | undefined>(undefined);\n duration = input<number | undefined>(undefined);\n fallbackBorderRadius = input<number | undefined>(undefined);\n\n // View child reference\n measureContainer = viewChild<ElementRef<HTMLDivElement>>('measureContainer');\n\n // Internal state\n elements = signal<ElementInfo[]>([]);\n\n // Inject dependencies\n private contextConfig = injectShimmerConfig();\n private platformId = inject(PLATFORM_ID);\n private isBrowser = isPlatformBrowser(this.platformId);\n\n // Resolved values (props > context > defaults)\n resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);\n resolvedBackgroundColor = computed(\n () => this.backgroundColor() ?? this.contextConfig.backgroundColor\n );\n resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);\n resolvedFallbackBorderRadius = computed(\n () => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius\n );\n\n // Cleanup function for ResizeObserver\n private resizeCleanup: (() => void) | undefined;\n private mutationObserver: MutationObserver | undefined;\n\n constructor() {\n // Effect to re-measure when loading state changes\n effect((onCleanup) => {\n // Skip effect on server\n if (!this.isBrowser) return;\n\n const isLoading = this.loading();\n const container = this.measureContainer();\n\n if (isLoading && container) {\n // Clean up existing observers before setting up new ones\n this.cleanup();\n\n // Set up observers for this loading session\n this.setupObservers();\n\n // Defer measurement to next frame to ensure content is rendered\n requestAnimationFrame(() => this.measureElements());\n } else {\n // Cleanup when not loading\n this.cleanup();\n }\n\n // Cleanup on effect re-run or component destruction\n onCleanup(() => {\n this.cleanup();\n });\n });\n }\n\n ngAfterViewInit(): void {\n // Effect will handle setup when container becomes available\n }\n\n ngOnDestroy(): void {\n this.cleanup();\n }\n\n private setupObservers(): void {\n if (!this.isBrowser) return;\n\n const container = this.measureContainer()?.nativeElement;\n if (!container) return;\n\n // Set up ResizeObserver\n this.resizeCleanup = createResizeObserver(container, () => this.measureElements());\n\n // Set up MutationObserver for content changes\n this.mutationObserver = new MutationObserver(() => {\n if (this.loading()) {\n this.measureElements();\n }\n });\n\n this.mutationObserver.observe(container, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: false,\n });\n }\n\n private measureElements(): void {\n if (!this.isBrowser) return;\n\n const container = this.measureContainer()?.nativeElement;\n if (!container || !this.loading()) return;\n\n // Temporarily disconnect mutation observer to avoid recursion\n this.mutationObserver?.disconnect();\n\n const containerRect = container.getBoundingClientRect();\n const extractedElements: ElementInfo[] = [];\n\n Array.from(container.children).forEach((child) => {\n extractedElements.push(...extractElementInfo(child, containerRect));\n });\n\n this.elements.set(extractedElements);\n\n // Reconnect mutation observer\n if (this.mutationObserver && container) {\n this.mutationObserver.observe(container, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: false,\n });\n }\n }\n\n private cleanup(): void {\n if (this.resizeCleanup) {\n this.resizeCleanup();\n this.resizeCleanup = undefined;\n }\n if (this.mutationObserver) {\n this.mutationObserver.disconnect();\n this.mutationObserver = undefined;\n }\n }\n\n /**\n * Manually trigger re-measurement of elements.\n * Useful when content changes programmatically.\n */\n remeasure(): void {\n if (this.isBrowser) {\n this.measureElements();\n }\n }\n}\n","// Public API Surface\nexport { ShimmerComponent } from './shimmer.component';\nexport {\n SHIMMER_CONFIG,\n provideShimmerConfig,\n injectShimmerConfig,\n shimmerDefaults,\n} from './shimmer-config.service';\nexport type { ShimmerInputs, ShimmerConfig, ShimmerContextValue, ElementInfo } from './types';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/index.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,36 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shimmer-from-structure/angular",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Angular adapter for shimmer-from-structure",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
7
7
|
"@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@shimmer-from-structure/core": "*"
|
|
11
|
-
|
|
12
|
-
"devDependencies": {
|
|
13
|
-
"@analogjs/vite-plugin-angular": "^2.2.3",
|
|
14
|
-
"@angular/common": "^19.0.0",
|
|
15
|
-
"@angular/compiler": "^19.0.0",
|
|
16
|
-
"@angular/compiler-cli": "^19.0.0",
|
|
17
|
-
"@angular/core": "^19.0.0",
|
|
18
|
-
"@angular/platform-browser": "^19.0.0",
|
|
19
|
-
"@angular/platform-browser-dynamic": "^19.0.0",
|
|
20
|
-
"@testing-library/dom": "^10.4.1",
|
|
21
|
-
"jsdom": "^27.4.0",
|
|
22
|
-
"ng-packagr": "^19.0.0",
|
|
23
|
-
"rxjs": "~7.8.0",
|
|
24
|
-
"typescript": "~5.6.0",
|
|
25
|
-
"vite": "^7.3.1",
|
|
26
|
-
"vitest": "^4.0.17",
|
|
27
|
-
"zone.js": "~0.15.0"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "ng-packagr -p ng-package.json",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"format": "prettier --write .",
|
|
33
|
-
"prepublishOnly": "cp ../../README.md ."
|
|
10
|
+
"@shimmer-from-structure/core": "*",
|
|
11
|
+
"tslib": "^2.3.0"
|
|
34
12
|
},
|
|
35
13
|
"keywords": [
|
|
36
14
|
"angular",
|
|
@@ -44,5 +22,17 @@
|
|
|
44
22
|
"directory": "packages/angular"
|
|
45
23
|
},
|
|
46
24
|
"author": "Olebogeng Mbedzi",
|
|
47
|
-
"license": "MIT"
|
|
48
|
-
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"module": "fesm2022/shimmer-from-structure-angular.mjs",
|
|
27
|
+
"typings": "index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
"./package.json": {
|
|
30
|
+
"default": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"default": "./fesm2022/shimmer-from-structure-angular.mjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Public API Surface
|
|
2
1
|
export { ShimmerComponent } from './shimmer.component';
|
|
3
|
-
export {
|
|
4
|
-
SHIMMER_CONFIG,
|
|
5
|
-
provideShimmerConfig,
|
|
6
|
-
injectShimmerConfig,
|
|
7
|
-
shimmerDefaults,
|
|
8
|
-
} from './shimmer-config.service';
|
|
2
|
+
export { SHIMMER_CONFIG, provideShimmerConfig, injectShimmerConfig, shimmerDefaults, } from './shimmer-config.service';
|
|
9
3
|
export type { ShimmerInputs, ShimmerConfig, ShimmerContextValue, ElementInfo } from './types';
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { InjectionToken
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
2
|
import type { ShimmerConfig, ShimmerContextValue } from '@shimmer-from-structure/core';
|
|
3
3
|
import { shimmerDefaults } from '@shimmer-from-structure/core';
|
|
4
|
-
|
|
5
4
|
/**
|
|
6
5
|
* Injection token for global shimmer configuration.
|
|
7
6
|
* Use `provideShimmerConfig()` to configure in your app.
|
|
8
7
|
*/
|
|
9
|
-
export const SHIMMER_CONFIG
|
|
10
|
-
|
|
8
|
+
export declare const SHIMMER_CONFIG: InjectionToken<ShimmerConfig>;
|
|
11
9
|
/**
|
|
12
10
|
* Provider function for shimmer configuration.
|
|
13
11
|
* Use in your app's providers array.
|
|
@@ -24,25 +22,14 @@ export const SHIMMER_CONFIG = new InjectionToken<ShimmerConfig>('SHIMMER_CONFIG'
|
|
|
24
22
|
* });
|
|
25
23
|
* ```
|
|
26
24
|
*/
|
|
27
|
-
export function provideShimmerConfig(config: ShimmerConfig) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
export declare function provideShimmerConfig(config: ShimmerConfig): {
|
|
26
|
+
provide: InjectionToken<ShimmerConfig>;
|
|
27
|
+
useValue: ShimmerConfig;
|
|
28
|
+
};
|
|
31
29
|
/**
|
|
32
30
|
* Inject and resolve shimmer configuration.
|
|
33
31
|
* Merges injected config with defaults.
|
|
34
32
|
* Returns fully resolved ShimmerContextValue with all properties defined.
|
|
35
33
|
*/
|
|
36
|
-
export function injectShimmerConfig(): ShimmerContextValue
|
|
37
|
-
const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,
|
|
41
|
-
backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,
|
|
42
|
-
duration: config.duration ?? shimmerDefaults.duration,
|
|
43
|
-
fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Re-export defaults for testing and reference
|
|
34
|
+
export declare function injectShimmerConfig(): ShimmerContextValue;
|
|
48
35
|
export { shimmerDefaults };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
|
|
2
|
+
import { type ElementInfo } from '@shimmer-from-structure/core';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* Shimmer component that creates loading skeleton overlays based on content structure.
|
|
6
|
+
* Automatically measures projected content and creates matching shimmer blocks.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```html
|
|
10
|
+
* <shimmer [loading]="isLoading">
|
|
11
|
+
* <div class="card">
|
|
12
|
+
* <h2>{{ title }}</h2>
|
|
13
|
+
* <p>{{ description }}</p>
|
|
14
|
+
* </div>
|
|
15
|
+
* </shimmer>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class ShimmerComponent implements AfterViewInit, OnDestroy {
|
|
19
|
+
loading: import("@angular/core").InputSignal<boolean>;
|
|
20
|
+
shimmerColor: import("@angular/core").InputSignal<string>;
|
|
21
|
+
backgroundColor: import("@angular/core").InputSignal<string>;
|
|
22
|
+
duration: import("@angular/core").InputSignal<number>;
|
|
23
|
+
fallbackBorderRadius: import("@angular/core").InputSignal<number>;
|
|
24
|
+
measureContainer: import("@angular/core").Signal<ElementRef<HTMLDivElement>>;
|
|
25
|
+
elements: import("@angular/core").WritableSignal<ElementInfo[]>;
|
|
26
|
+
private contextConfig;
|
|
27
|
+
private platformId;
|
|
28
|
+
private isBrowser;
|
|
29
|
+
resolvedShimmerColor: import("@angular/core").Signal<string>;
|
|
30
|
+
resolvedBackgroundColor: import("@angular/core").Signal<string>;
|
|
31
|
+
resolvedDuration: import("@angular/core").Signal<number>;
|
|
32
|
+
resolvedFallbackBorderRadius: import("@angular/core").Signal<number>;
|
|
33
|
+
private resizeCleanup;
|
|
34
|
+
private mutationObserver;
|
|
35
|
+
constructor();
|
|
36
|
+
ngAfterViewInit(): void;
|
|
37
|
+
ngOnDestroy(): void;
|
|
38
|
+
private setupObservers;
|
|
39
|
+
private measureElements;
|
|
40
|
+
private cleanup;
|
|
41
|
+
/**
|
|
42
|
+
* Manually trigger re-measurement of elements.
|
|
43
|
+
* Useful when content changes programmatically.
|
|
44
|
+
*/
|
|
45
|
+
remeasure(): void;
|
|
46
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ShimmerComponent, never>;
|
|
47
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<ShimmerComponent, "shimmer", never, { "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "shimmerColor": { "alias": "shimmerColor"; "required": false; "isSignal": true; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; "isSignal": true; }; "duration": { "alias": "duration"; "required": false; "isSignal": true; }; "fallbackBorderRadius": { "alias": "fallbackBorderRadius"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
|
|
48
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input properties for the Shimmer component
|
|
3
|
+
*/
|
|
4
|
+
export interface ShimmerInputs {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the component is in loading state
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Color of the shimmer effect gradient
|
|
12
|
+
*/
|
|
13
|
+
shimmerColor?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Background color of the shimmer blocks
|
|
16
|
+
*/
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Duration of one shimmer animation cycle in seconds
|
|
20
|
+
*/
|
|
21
|
+
duration?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Fallback border radius (in pixels) when element has no border-radius
|
|
24
|
+
* @default 4
|
|
25
|
+
*/
|
|
26
|
+
fallbackBorderRadius?: number;
|
|
27
|
+
}
|
|
28
|
+
export type { ShimmerConfig, ShimmerContextValue, ElementInfo } from '@shimmer-from-structure/core';
|
package/ng-package.json
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { TestBed } from '@angular/core/testing';
|
|
3
|
-
import {
|
|
4
|
-
provideShimmerConfig,
|
|
5
|
-
injectShimmerConfig,
|
|
6
|
-
shimmerDefaults,
|
|
7
|
-
SHIMMER_CONFIG,
|
|
8
|
-
} from './shimmer-config.service';
|
|
9
|
-
|
|
10
|
-
describe('shimmer-config.service', () => {
|
|
11
|
-
describe('shimmerDefaults', () => {
|
|
12
|
-
it('exports default values', () => {
|
|
13
|
-
expect(shimmerDefaults).toBeDefined();
|
|
14
|
-
expect(shimmerDefaults.shimmerColor).toBeDefined();
|
|
15
|
-
expect(shimmerDefaults.backgroundColor).toBeDefined();
|
|
16
|
-
expect(shimmerDefaults.duration).toBeDefined();
|
|
17
|
-
expect(shimmerDefaults.fallbackBorderRadius).toBeDefined();
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('provideShimmerConfig', () => {
|
|
22
|
-
it('creates a provider object', () => {
|
|
23
|
-
const config = { shimmerColor: '#fff', duration: 2 };
|
|
24
|
-
const provider = provideShimmerConfig(config);
|
|
25
|
-
|
|
26
|
-
expect(provider.provide).toBe(SHIMMER_CONFIG);
|
|
27
|
-
expect(provider.useValue).toBe(config);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('injectShimmerConfig', () => {
|
|
32
|
-
it('returns defaults when no provider is configured', () => {
|
|
33
|
-
TestBed.configureTestingModule({});
|
|
34
|
-
|
|
35
|
-
TestBed.runInInjectionContext(() => {
|
|
36
|
-
const config = injectShimmerConfig();
|
|
37
|
-
|
|
38
|
-
expect(config.shimmerColor).toBe(shimmerDefaults.shimmerColor);
|
|
39
|
-
expect(config.backgroundColor).toBe(shimmerDefaults.backgroundColor);
|
|
40
|
-
expect(config.duration).toBe(shimmerDefaults.duration);
|
|
41
|
-
expect(config.fallbackBorderRadius).toBe(shimmerDefaults.fallbackBorderRadius);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('merges provided config with defaults', () => {
|
|
46
|
-
TestBed.configureTestingModule({
|
|
47
|
-
providers: [
|
|
48
|
-
provideShimmerConfig({
|
|
49
|
-
shimmerColor: 'rgba(255, 255, 255, 0.8)',
|
|
50
|
-
duration: 3,
|
|
51
|
-
}),
|
|
52
|
-
],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
TestBed.runInInjectionContext(() => {
|
|
56
|
-
const config = injectShimmerConfig();
|
|
57
|
-
|
|
58
|
-
expect(config.shimmerColor).toBe('rgba(255, 255, 255, 0.8)');
|
|
59
|
-
expect(config.duration).toBe(3);
|
|
60
|
-
// Should use defaults for non-provided values
|
|
61
|
-
expect(config.backgroundColor).toBe(shimmerDefaults.backgroundColor);
|
|
62
|
-
expect(config.fallbackBorderRadius).toBe(shimmerDefaults.fallbackBorderRadius);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns fully resolved ShimmerContextValue', () => {
|
|
67
|
-
TestBed.configureTestingModule({
|
|
68
|
-
providers: [
|
|
69
|
-
provideShimmerConfig({
|
|
70
|
-
shimmerColor: '#aaa',
|
|
71
|
-
backgroundColor: '#bbb',
|
|
72
|
-
duration: 1.5,
|
|
73
|
-
fallbackBorderRadius: 10,
|
|
74
|
-
}),
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
TestBed.runInInjectionContext(() => {
|
|
79
|
-
const config = injectShimmerConfig();
|
|
80
|
-
|
|
81
|
-
expect(config.shimmerColor).toBe('#aaa');
|
|
82
|
-
expect(config.backgroundColor).toBe('#bbb');
|
|
83
|
-
expect(config.duration).toBe(1.5);
|
|
84
|
-
expect(config.fallbackBorderRadius).toBe(10);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { Component, NO_ERRORS_SCHEMA, signal, ChangeDetectorRef } from '@angular/core';
|
|
3
|
-
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
4
|
-
import { By } from '@angular/platform-browser';
|
|
5
|
-
import { ShimmerComponent } from './shimmer.component';
|
|
6
|
-
import { provideShimmerConfig, SHIMMER_CONFIG } from './shimmer-config.service';
|
|
7
|
-
|
|
8
|
-
// Test wrapper component for content projection
|
|
9
|
-
@Component({
|
|
10
|
-
selector: 'test-host',
|
|
11
|
-
standalone: true,
|
|
12
|
-
imports: [ShimmerComponent],
|
|
13
|
-
template: `
|
|
14
|
-
<shimmer [loading]="loading">
|
|
15
|
-
<div class="test-content" style="width: 100px; height: 50px;">Content</div>
|
|
16
|
-
</shimmer>
|
|
17
|
-
`,
|
|
18
|
-
schemas: [NO_ERRORS_SCHEMA],
|
|
19
|
-
})
|
|
20
|
-
class TestHostComponent {
|
|
21
|
-
loading = true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('ShimmerComponent', () => {
|
|
25
|
-
let fixture: ComponentFixture<TestHostComponent>;
|
|
26
|
-
let hostComponent: TestHostComponent;
|
|
27
|
-
|
|
28
|
-
beforeEach(async () => {
|
|
29
|
-
await TestBed.configureTestingModule({
|
|
30
|
-
imports: [TestHostComponent, ShimmerComponent],
|
|
31
|
-
}).compileComponents();
|
|
32
|
-
|
|
33
|
-
fixture = TestBed.createComponent(TestHostComponent);
|
|
34
|
-
hostComponent = fixture.componentInstance;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Helper to wait for async operations
|
|
38
|
-
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms || 50));
|
|
39
|
-
|
|
40
|
-
it('renders children normally when loading=false', async () => {
|
|
41
|
-
fixture.detectChanges();
|
|
42
|
-
const shimmerEl = fixture.debugElement.query(By.directive(ShimmerComponent));
|
|
43
|
-
const shimmerInstance = shimmerEl.componentInstance;
|
|
44
|
-
|
|
45
|
-
Object.defineProperty(shimmerInstance, 'loading', { value: signal(false) });
|
|
46
|
-
shimmerEl.injector.get(ChangeDetectorRef).detectChanges();
|
|
47
|
-
|
|
48
|
-
fixture.detectChanges();
|
|
49
|
-
await wait(0);
|
|
50
|
-
fixture.detectChanges();
|
|
51
|
-
|
|
52
|
-
const content = fixture.nativeElement.querySelector('.test-content');
|
|
53
|
-
expect(content).toBeTruthy();
|
|
54
|
-
expect(content.textContent).toBe('Content');
|
|
55
|
-
|
|
56
|
-
// Should not have the measure container when not loading
|
|
57
|
-
const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
|
|
58
|
-
expect(measureContainer).toBeFalsy();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('renders shimmer structure when loading=true', async () => {
|
|
62
|
-
hostComponent.loading = true;
|
|
63
|
-
fixture.detectChanges();
|
|
64
|
-
await wait(0);
|
|
65
|
-
fixture.detectChanges();
|
|
66
|
-
|
|
67
|
-
// Should render the measure container
|
|
68
|
-
const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
|
|
69
|
-
expect(measureContainer).toBeTruthy();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('applies transparent text style to measure container', async () => {
|
|
73
|
-
hostComponent.loading = true;
|
|
74
|
-
fixture.detectChanges();
|
|
75
|
-
await wait(0);
|
|
76
|
-
fixture.detectChanges();
|
|
77
|
-
|
|
78
|
-
// Check that the measure container has the class
|
|
79
|
-
const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
|
|
80
|
-
expect(measureContainer).toBeTruthy();
|
|
81
|
-
expect(measureContainer.classList.contains('shimmer-measure-container')).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('ShimmerComponent with config provider', () => {
|
|
86
|
-
let fixture: ComponentFixture<TestHostComponent>;
|
|
87
|
-
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms || 50));
|
|
88
|
-
|
|
89
|
-
beforeEach(async () => {
|
|
90
|
-
await TestBed.configureTestingModule({
|
|
91
|
-
imports: [TestHostComponent, ShimmerComponent],
|
|
92
|
-
providers: [
|
|
93
|
-
provideShimmerConfig({
|
|
94
|
-
shimmerColor: 'rgba(255, 0, 0, 0.5)',
|
|
95
|
-
backgroundColor: '#ff0000',
|
|
96
|
-
duration: 2,
|
|
97
|
-
fallbackBorderRadius: 8,
|
|
98
|
-
}),
|
|
99
|
-
],
|
|
100
|
-
}).compileComponents();
|
|
101
|
-
|
|
102
|
-
fixture = TestBed.createComponent(TestHostComponent);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('uses provided config values', async () => {
|
|
106
|
-
fixture.componentInstance.loading = true;
|
|
107
|
-
fixture.detectChanges();
|
|
108
|
-
await wait(0);
|
|
109
|
-
fixture.detectChanges();
|
|
110
|
-
|
|
111
|
-
// The config should be injected - we can verify by checking the SHIMMER_CONFIG token
|
|
112
|
-
const config = TestBed.inject(SHIMMER_CONFIG);
|
|
113
|
-
expect(config.shimmerColor).toBe('rgba(255, 0, 0, 0.5)');
|
|
114
|
-
expect(config.backgroundColor).toBe('#ff0000');
|
|
115
|
-
expect(config.duration).toBe(2);
|
|
116
|
-
expect(config.fallbackBorderRadius).toBe(8);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('ShimmerComponent input overrides', () => {
|
|
121
|
-
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms || 50));
|
|
122
|
-
|
|
123
|
-
@Component({
|
|
124
|
-
selector: 'test-override-host',
|
|
125
|
-
standalone: true,
|
|
126
|
-
imports: [ShimmerComponent],
|
|
127
|
-
template: `
|
|
128
|
-
<shimmer
|
|
129
|
-
[loading]="true"
|
|
130
|
-
[shimmerColor]="'#00ff00'"
|
|
131
|
-
[backgroundColor]="'#0000ff'"
|
|
132
|
-
[duration]="3"
|
|
133
|
-
[fallbackBorderRadius]="12"
|
|
134
|
-
>
|
|
135
|
-
<div>Content</div>
|
|
136
|
-
</shimmer>
|
|
137
|
-
`,
|
|
138
|
-
schemas: [NO_ERRORS_SCHEMA],
|
|
139
|
-
})
|
|
140
|
-
class TestOverrideHostComponent {}
|
|
141
|
-
|
|
142
|
-
beforeEach(async () => {
|
|
143
|
-
await TestBed.configureTestingModule({
|
|
144
|
-
imports: [TestOverrideHostComponent, ShimmerComponent],
|
|
145
|
-
providers: [
|
|
146
|
-
provideShimmerConfig({
|
|
147
|
-
shimmerColor: 'rgba(255, 0, 0, 0.5)',
|
|
148
|
-
duration: 1,
|
|
149
|
-
}),
|
|
150
|
-
],
|
|
151
|
-
}).compileComponents();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('component inputs override provider config', async () => {
|
|
155
|
-
const fixture = TestBed.createComponent(TestOverrideHostComponent);
|
|
156
|
-
fixture.detectChanges();
|
|
157
|
-
|
|
158
|
-
// Manual override for input signals
|
|
159
|
-
const shimmer = fixture.debugElement.children[0].componentInstance as ShimmerComponent;
|
|
160
|
-
Object.defineProperty(shimmer, 'shimmerColor', { value: signal('#00ff00') });
|
|
161
|
-
Object.defineProperty(shimmer, 'backgroundColor', { value: signal('#0000ff') });
|
|
162
|
-
Object.defineProperty(shimmer, 'duration', { value: signal(3) });
|
|
163
|
-
Object.defineProperty(shimmer, 'fallbackBorderRadius', { value: signal(12) });
|
|
164
|
-
|
|
165
|
-
fixture.detectChanges();
|
|
166
|
-
await wait(0);
|
|
167
|
-
fixture.detectChanges();
|
|
168
|
-
|
|
169
|
-
// The shimmer component should use input values over config
|
|
170
|
-
expect(shimmer.resolvedShimmerColor()).toBe('#00ff00');
|
|
171
|
-
expect(shimmer.resolvedBackgroundColor()).toBe('#0000ff');
|
|
172
|
-
expect(shimmer.resolvedDuration()).toBe(3);
|
|
173
|
-
expect(shimmer.resolvedFallbackBorderRadius()).toBe(12);
|
|
174
|
-
});
|
|
175
|
-
});
|
package/src/shimmer.component.ts
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Component,
|
|
3
|
-
input,
|
|
4
|
-
signal,
|
|
5
|
-
computed,
|
|
6
|
-
effect,
|
|
7
|
-
ElementRef,
|
|
8
|
-
viewChild,
|
|
9
|
-
AfterViewInit,
|
|
10
|
-
OnDestroy,
|
|
11
|
-
ChangeDetectionStrategy,
|
|
12
|
-
ViewEncapsulation,
|
|
13
|
-
inject,
|
|
14
|
-
PLATFORM_ID,
|
|
15
|
-
} from '@angular/core';
|
|
16
|
-
import { NgIf, NgFor, isPlatformBrowser } from '@angular/common';
|
|
17
|
-
import {
|
|
18
|
-
extractElementInfo,
|
|
19
|
-
createResizeObserver,
|
|
20
|
-
type ElementInfo,
|
|
21
|
-
} from '@shimmer-from-structure/core';
|
|
22
|
-
import { injectShimmerConfig } from './shimmer-config.service';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Shimmer component that creates loading skeleton overlays based on content structure.
|
|
26
|
-
* Automatically measures projected content and creates matching shimmer blocks.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```html
|
|
30
|
-
* <shimmer [loading]="isLoading">
|
|
31
|
-
* <div class="card">
|
|
32
|
-
* <h2>{{ title }}</h2>
|
|
33
|
-
* <p>{{ description }}</p>
|
|
34
|
-
* </div>
|
|
35
|
-
* </shimmer>
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
@Component({
|
|
39
|
-
selector: 'shimmer',
|
|
40
|
-
standalone: true,
|
|
41
|
-
imports: [NgIf, NgFor],
|
|
42
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
43
|
-
encapsulation: ViewEncapsulation.None,
|
|
44
|
-
template: `
|
|
45
|
-
<div style="position: relative;">
|
|
46
|
-
<!-- Always render content -->
|
|
47
|
-
<div
|
|
48
|
-
#measureContainer
|
|
49
|
-
[class.shimmer-measure-container]="loading()"
|
|
50
|
-
[attr.aria-hidden]="loading() ? 'true' : null"
|
|
51
|
-
[style.pointer-events]="loading() ? 'none' : null"
|
|
52
|
-
>
|
|
53
|
-
<ng-content></ng-content>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<!-- Shimmer overlay - only when loading -->
|
|
57
|
-
@if (loading()) {
|
|
58
|
-
<div
|
|
59
|
-
style="
|
|
60
|
-
position: absolute;
|
|
61
|
-
top: 0;
|
|
62
|
-
left: 0;
|
|
63
|
-
right: 0;
|
|
64
|
-
bottom: 0;
|
|
65
|
-
overflow: hidden;
|
|
66
|
-
pointer-events: none;
|
|
67
|
-
"
|
|
68
|
-
>
|
|
69
|
-
@for (element of elements(); track $index) {
|
|
70
|
-
<div
|
|
71
|
-
[style.position]="'absolute'"
|
|
72
|
-
[style.left.px]="element.x"
|
|
73
|
-
[style.top.px]="element.y"
|
|
74
|
-
[style.width.px]="element.width"
|
|
75
|
-
[style.height.px]="element.height"
|
|
76
|
-
[style.backgroundColor]="resolvedBackgroundColor()"
|
|
77
|
-
[style.borderRadius]="
|
|
78
|
-
element.borderRadius === '0px'
|
|
79
|
-
? resolvedFallbackBorderRadius() + 'px'
|
|
80
|
-
: element.borderRadius
|
|
81
|
-
"
|
|
82
|
-
[style.overflow]="'hidden'"
|
|
83
|
-
>
|
|
84
|
-
<div
|
|
85
|
-
class="shimmer-animation-element"
|
|
86
|
-
[style.background]="
|
|
87
|
-
'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
|
|
88
|
-
"
|
|
89
|
-
[style.animationDuration]="resolvedDuration() + 's'"
|
|
90
|
-
></div>
|
|
91
|
-
</div>
|
|
92
|
-
}
|
|
93
|
-
</div>
|
|
94
|
-
}
|
|
95
|
-
</div>
|
|
96
|
-
`,
|
|
97
|
-
styles: [
|
|
98
|
-
`
|
|
99
|
-
:host {
|
|
100
|
-
display: contents;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.shimmer-measure-container * {
|
|
104
|
-
color: transparent !important;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.shimmer-measure-container img,
|
|
108
|
-
.shimmer-measure-container svg,
|
|
109
|
-
.shimmer-measure-container video {
|
|
110
|
-
opacity: 0;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.shimmer-animation-element {
|
|
114
|
-
position: absolute;
|
|
115
|
-
top: 0;
|
|
116
|
-
left: 0;
|
|
117
|
-
width: 100%;
|
|
118
|
-
height: 100%;
|
|
119
|
-
animation: shimmer-animation 1.5s infinite;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@keyframes shimmer-animation {
|
|
123
|
-
0% {
|
|
124
|
-
transform: translateX(-100%);
|
|
125
|
-
}
|
|
126
|
-
100% {
|
|
127
|
-
transform: translateX(100%);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
`,
|
|
131
|
-
],
|
|
132
|
-
})
|
|
133
|
-
export class ShimmerComponent implements AfterViewInit, OnDestroy {
|
|
134
|
-
// Inputs using Angular signals
|
|
135
|
-
loading = input<boolean>(true);
|
|
136
|
-
shimmerColor = input<string | undefined>(undefined);
|
|
137
|
-
backgroundColor = input<string | undefined>(undefined);
|
|
138
|
-
duration = input<number | undefined>(undefined);
|
|
139
|
-
fallbackBorderRadius = input<number | undefined>(undefined);
|
|
140
|
-
|
|
141
|
-
// View child reference
|
|
142
|
-
measureContainer = viewChild<ElementRef<HTMLDivElement>>('measureContainer');
|
|
143
|
-
|
|
144
|
-
// Internal state
|
|
145
|
-
elements = signal<ElementInfo[]>([]);
|
|
146
|
-
|
|
147
|
-
// Inject dependencies
|
|
148
|
-
private contextConfig = injectShimmerConfig();
|
|
149
|
-
private platformId = inject(PLATFORM_ID);
|
|
150
|
-
private isBrowser = isPlatformBrowser(this.platformId);
|
|
151
|
-
|
|
152
|
-
// Resolved values (props > context > defaults)
|
|
153
|
-
resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);
|
|
154
|
-
resolvedBackgroundColor = computed(
|
|
155
|
-
() => this.backgroundColor() ?? this.contextConfig.backgroundColor
|
|
156
|
-
);
|
|
157
|
-
resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);
|
|
158
|
-
resolvedFallbackBorderRadius = computed(
|
|
159
|
-
() => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Cleanup function for ResizeObserver
|
|
163
|
-
private resizeCleanup: (() => void) | undefined;
|
|
164
|
-
private mutationObserver: MutationObserver | undefined;
|
|
165
|
-
|
|
166
|
-
constructor() {
|
|
167
|
-
// Effect to re-measure when loading state changes
|
|
168
|
-
effect((onCleanup) => {
|
|
169
|
-
// Skip effect on server
|
|
170
|
-
if (!this.isBrowser) return;
|
|
171
|
-
|
|
172
|
-
const isLoading = this.loading();
|
|
173
|
-
const container = this.measureContainer();
|
|
174
|
-
|
|
175
|
-
if (isLoading && container) {
|
|
176
|
-
// Clean up existing observers before setting up new ones
|
|
177
|
-
this.cleanup();
|
|
178
|
-
|
|
179
|
-
// Set up observers for this loading session
|
|
180
|
-
this.setupObservers();
|
|
181
|
-
|
|
182
|
-
// Defer measurement to next frame to ensure content is rendered
|
|
183
|
-
requestAnimationFrame(() => this.measureElements());
|
|
184
|
-
} else {
|
|
185
|
-
// Cleanup when not loading
|
|
186
|
-
this.cleanup();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Cleanup on effect re-run or component destruction
|
|
190
|
-
onCleanup(() => {
|
|
191
|
-
this.cleanup();
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
ngAfterViewInit(): void {
|
|
197
|
-
// Effect will handle setup when container becomes available
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
ngOnDestroy(): void {
|
|
201
|
-
this.cleanup();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private setupObservers(): void {
|
|
205
|
-
if (!this.isBrowser) return;
|
|
206
|
-
|
|
207
|
-
const container = this.measureContainer()?.nativeElement;
|
|
208
|
-
if (!container) return;
|
|
209
|
-
|
|
210
|
-
// Set up ResizeObserver
|
|
211
|
-
this.resizeCleanup = createResizeObserver(container, () => this.measureElements());
|
|
212
|
-
|
|
213
|
-
// Set up MutationObserver for content changes
|
|
214
|
-
this.mutationObserver = new MutationObserver(() => {
|
|
215
|
-
if (this.loading()) {
|
|
216
|
-
this.measureElements();
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
this.mutationObserver.observe(container, {
|
|
221
|
-
childList: true,
|
|
222
|
-
subtree: true,
|
|
223
|
-
characterData: true,
|
|
224
|
-
attributes: false,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private measureElements(): void {
|
|
229
|
-
if (!this.isBrowser) return;
|
|
230
|
-
|
|
231
|
-
const container = this.measureContainer()?.nativeElement;
|
|
232
|
-
if (!container || !this.loading()) return;
|
|
233
|
-
|
|
234
|
-
// Temporarily disconnect mutation observer to avoid recursion
|
|
235
|
-
this.mutationObserver?.disconnect();
|
|
236
|
-
|
|
237
|
-
const containerRect = container.getBoundingClientRect();
|
|
238
|
-
const extractedElements: ElementInfo[] = [];
|
|
239
|
-
|
|
240
|
-
Array.from(container.children).forEach((child) => {
|
|
241
|
-
extractedElements.push(...extractElementInfo(child, containerRect));
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
this.elements.set(extractedElements);
|
|
245
|
-
|
|
246
|
-
// Reconnect mutation observer
|
|
247
|
-
if (this.mutationObserver && container) {
|
|
248
|
-
this.mutationObserver.observe(container, {
|
|
249
|
-
childList: true,
|
|
250
|
-
subtree: true,
|
|
251
|
-
characterData: true,
|
|
252
|
-
attributes: false,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private cleanup(): void {
|
|
258
|
-
if (this.resizeCleanup) {
|
|
259
|
-
this.resizeCleanup();
|
|
260
|
-
this.resizeCleanup = undefined;
|
|
261
|
-
}
|
|
262
|
-
if (this.mutationObserver) {
|
|
263
|
-
this.mutationObserver.disconnect();
|
|
264
|
-
this.mutationObserver = undefined;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Manually trigger re-measurement of elements.
|
|
270
|
-
* Useful when content changes programmatically.
|
|
271
|
-
*/
|
|
272
|
-
remeasure(): void {
|
|
273
|
-
if (this.isBrowser) {
|
|
274
|
-
this.measureElements();
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
package/src/test/setup.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import 'zone.js';
|
|
2
|
-
import 'zone.js/testing';
|
|
3
|
-
import { vi, beforeAll } from 'vitest';
|
|
4
|
-
import { TestBed } from '@angular/core/testing';
|
|
5
|
-
import {
|
|
6
|
-
BrowserDynamicTestingModule,
|
|
7
|
-
platformBrowserDynamicTesting,
|
|
8
|
-
} from '@angular/platform-browser-dynamic/testing';
|
|
9
|
-
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// Mock getBoundingClientRect since jsdom doesn't support layout
|
|
15
|
-
Element.prototype.getBoundingClientRect = vi.fn(() => ({
|
|
16
|
-
width: 100,
|
|
17
|
-
height: 50,
|
|
18
|
-
top: 0,
|
|
19
|
-
left: 0,
|
|
20
|
-
bottom: 50,
|
|
21
|
-
right: 100,
|
|
22
|
-
x: 0,
|
|
23
|
-
y: 0,
|
|
24
|
-
toJSON: () => {},
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
// Mock ResizeObserver
|
|
28
|
-
global.ResizeObserver = class ResizeObserver {
|
|
29
|
-
observe() {}
|
|
30
|
-
unobserve() {}
|
|
31
|
-
disconnect() {}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Mock getComputedStyle
|
|
35
|
-
const originalGetComputedStyle = window.getComputedStyle;
|
|
36
|
-
window.getComputedStyle = (elt) => {
|
|
37
|
-
const styles = originalGetComputedStyle(elt);
|
|
38
|
-
// Add borderRadius support for our tests
|
|
39
|
-
if (!styles.borderRadius) {
|
|
40
|
-
Object.defineProperty(styles, 'borderRadius', {
|
|
41
|
-
value: '4px',
|
|
42
|
-
writable: true,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return styles;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Mock requestAnimationFrame
|
|
49
|
-
global.requestAnimationFrame = (cb) => {
|
|
50
|
-
setTimeout(cb, 0);
|
|
51
|
-
return 0;
|
|
52
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input properties for the Shimmer component
|
|
3
|
-
*/
|
|
4
|
-
export interface ShimmerInputs {
|
|
5
|
-
/**
|
|
6
|
-
* Whether the component is in loading state
|
|
7
|
-
* @default true
|
|
8
|
-
*/
|
|
9
|
-
loading?: boolean;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Color of the shimmer effect gradient
|
|
13
|
-
*/
|
|
14
|
-
shimmerColor?: string;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Background color of the shimmer blocks
|
|
18
|
-
*/
|
|
19
|
-
backgroundColor?: string;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Duration of one shimmer animation cycle in seconds
|
|
23
|
-
*/
|
|
24
|
-
duration?: number;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Fallback border radius (in pixels) when element has no border-radius
|
|
28
|
-
* @default 4
|
|
29
|
-
*/
|
|
30
|
-
fallbackBorderRadius?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Re-export core types for convenience
|
|
34
|
-
export type { ShimmerConfig, ShimmerContextValue, ElementInfo } from '@shimmer-from-structure/core';
|
package/tsconfig.eslint.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "./dist",
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"declarationMap": true,
|
|
7
|
-
"experimentalDecorators": true,
|
|
8
|
-
"emitDecoratorMetadata": true,
|
|
9
|
-
"target": "ES2022",
|
|
10
|
-
"module": "ES2022",
|
|
11
|
-
"moduleResolution": "bundler",
|
|
12
|
-
"useDefineForClassFields": false,
|
|
13
|
-
"lib": ["ES2022", "DOM"]
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*.ts"],
|
|
16
|
-
"exclude": ["src/**/*.test.ts", "node_modules", "dist"]
|
|
17
|
-
}
|
package/vitest.config.ts
DELETED