@shimmer-from-structure/angular 2.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/README.md +911 -0
- package/ng-package.json +8 -0
- package/package.json +46 -0
- package/src/public-api.ts +14 -0
- package/src/shimmer-config.service.test.ts +88 -0
- package/src/shimmer-config.service.ts +48 -0
- package/src/shimmer.component.test.ts +153 -0
- package/src/shimmer.component.ts +264 -0
- package/src/test/setup.ts +42 -0
- package/src/types.ts +35 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
# ✨ Shimmer From Structure
|
|
2
|
+
|
|
3
|
+
A **React, Vue, Svelte & Angular** 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
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Why This Library?
|
|
13
|
+
|
|
14
|
+
Traditional shimmer libraries require you to:
|
|
15
|
+
|
|
16
|
+
- Manually create skeleton components that mirror your real components
|
|
17
|
+
- Maintain two versions of each component (real + skeleton)
|
|
18
|
+
- Update skeletons every time your layout changes
|
|
19
|
+
|
|
20
|
+
**Shimmer From Structure** eliminates all of that:
|
|
21
|
+
|
|
22
|
+
- ✅ **Works with React, Vue, Svelte & Angular** - Simple, framework-specific adapters
|
|
23
|
+
- ✅ Automatically measures your component's structure at runtime
|
|
24
|
+
- ✅ Generates shimmer effects that match actual dimensions
|
|
25
|
+
- ✅ Zero maintenance - works with any layout changes
|
|
26
|
+
- ✅ Works with complex nested structures
|
|
27
|
+
- ✅ Supports dynamic data with `templateProps`
|
|
28
|
+
- ✅ Preserves container backgrounds during loading
|
|
29
|
+
- ✅ Auto-detects border-radius from your CSS
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install shimmer-from-structure
|
|
35
|
+
# or
|
|
36
|
+
yarn add shimmer-from-structure
|
|
37
|
+
# or
|
|
38
|
+
pnpm add shimmer-from-structure
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 🎯 Framework Support
|
|
42
|
+
|
|
43
|
+
Shimmer From Structure provides dedicated packages for **React and Vue**.
|
|
44
|
+
|
|
45
|
+
### React
|
|
46
|
+
|
|
47
|
+
React support is built-in to the main package for backward compatibility:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// React projects (or @shimmer-from-structure/react)
|
|
51
|
+
import { Shimmer } from 'shimmer-from-structure';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Vue 3
|
|
55
|
+
|
|
56
|
+
Vue support requires importing from the specific adapter:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Vue 3 projects
|
|
60
|
+
import { Shimmer } from '@shimmer-from-structure/vue';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Svelte
|
|
64
|
+
|
|
65
|
+
Svelte support is provided via its own adapter:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// Svelte projects
|
|
69
|
+
import { Shimmer } from '@shimmer-from-structure/svelte';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Angular
|
|
73
|
+
|
|
74
|
+
Angular support requires importing from the specific adapter:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Angular projects
|
|
78
|
+
import { ShimmerComponent } from '@shimmer-from-structure/angular';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# 📖 Basic Usage
|
|
84
|
+
|
|
85
|
+
## React
|
|
86
|
+
|
|
87
|
+
### Static Content
|
|
88
|
+
|
|
89
|
+
For components with hardcoded/static content:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { Shimmer } from 'shimmer-from-structure';
|
|
93
|
+
|
|
94
|
+
function UserCard() {
|
|
95
|
+
return (
|
|
96
|
+
<Shimmer loading={isLoading}>
|
|
97
|
+
<div className="card">
|
|
98
|
+
<img src="avatar.jpg" className="avatar" />
|
|
99
|
+
<h2>John Doe</h2>
|
|
100
|
+
<p>Software Engineer</p>
|
|
101
|
+
</div>
|
|
102
|
+
</Shimmer>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Vue
|
|
108
|
+
|
|
109
|
+
### Static Content
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<script setup>
|
|
113
|
+
import { ref } from 'vue';
|
|
114
|
+
import { Shimmer } from '@shimmer-from-structure/vue';
|
|
115
|
+
|
|
116
|
+
const isLoading = ref(true);
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<Shimmer :loading="isLoading">
|
|
121
|
+
<div class="card">
|
|
122
|
+
<img src="avatar.jpg" class="avatar" />
|
|
123
|
+
<h2>John Doe</h2>
|
|
124
|
+
<p>Software Engineer</p>
|
|
125
|
+
</div>
|
|
126
|
+
</Shimmer>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Svelte
|
|
131
|
+
|
|
132
|
+
### Static Content
|
|
133
|
+
|
|
134
|
+
```svelte
|
|
135
|
+
<script>
|
|
136
|
+
import { Shimmer } from '@shimmer-from-structure/svelte';
|
|
137
|
+
|
|
138
|
+
let isLoading = $state(true);
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<Shimmer loading={isLoading}>
|
|
142
|
+
<div class="card">
|
|
143
|
+
<img src="avatar.jpg" class="avatar" />
|
|
144
|
+
<h2>John Doe</h2>
|
|
145
|
+
<p>Software Engineer</p>
|
|
146
|
+
</div>
|
|
147
|
+
</Shimmer>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Angular
|
|
151
|
+
|
|
152
|
+
### Static Content
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Component, signal } from '@angular/core';
|
|
156
|
+
import { ShimmerComponent } from '@shimmer-from-structure/angular';
|
|
157
|
+
|
|
158
|
+
@Component({
|
|
159
|
+
selector: 'app-user-card',
|
|
160
|
+
standalone: true,
|
|
161
|
+
imports: [ShimmerComponent],
|
|
162
|
+
template: `
|
|
163
|
+
<shimmer [loading]="isLoading()">
|
|
164
|
+
<div class="card">
|
|
165
|
+
<img src="avatar.jpg" class="avatar" />
|
|
166
|
+
<h2>John Doe</h2>
|
|
167
|
+
<p>Software Engineer</p>
|
|
168
|
+
</div>
|
|
169
|
+
</shimmer>
|
|
170
|
+
`,
|
|
171
|
+
})
|
|
172
|
+
export class UserCardComponent {
|
|
173
|
+
isLoading = signal(true);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Dynamic Content with `templateProps`
|
|
180
|
+
|
|
181
|
+
For components that receive dynamic data via props, use `templateProps` to provide mock data for skeleton generation:
|
|
182
|
+
|
|
183
|
+
**React**
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { Shimmer } from 'shimmer-from-structure';
|
|
187
|
+
|
|
188
|
+
// Your component that accepts props
|
|
189
|
+
const UserCard = ({ user }) => (
|
|
190
|
+
<div className="card">
|
|
191
|
+
<img src={user.avatar} className="avatar" />
|
|
192
|
+
<h2>{user.name}</h2>
|
|
193
|
+
<p>{user.role}</p>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Template data for the skeleton
|
|
198
|
+
const userTemplate = {
|
|
199
|
+
name: 'Loading...',
|
|
200
|
+
role: 'Loading role...',
|
|
201
|
+
avatar: 'placeholder.jpg',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
function App() {
|
|
205
|
+
const [loading, setLoading] = useState(true);
|
|
206
|
+
const [user, setUser] = useState(null);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Shimmer loading={loading} templateProps={{ user: userTemplate }}>
|
|
210
|
+
<UserCard user={user || userTemplate} />
|
|
211
|
+
</Shimmer>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Vue**
|
|
217
|
+
|
|
218
|
+
```vue
|
|
219
|
+
<script setup>
|
|
220
|
+
import { ref } from 'vue';
|
|
221
|
+
import { Shimmer } from '@shimmer-from-structure/vue';
|
|
222
|
+
import UserCard from './UserCard.vue';
|
|
223
|
+
|
|
224
|
+
const loading = ref(true);
|
|
225
|
+
const userTemplate = {
|
|
226
|
+
name: 'Loading...',
|
|
227
|
+
role: 'Loading role...',
|
|
228
|
+
avatar: 'placeholder.jpg',
|
|
229
|
+
};
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<template>
|
|
233
|
+
<Shimmer :loading="loading" :templateProps="{ user: userTemplate }">
|
|
234
|
+
<UserCard :user="user || userTemplate" />
|
|
235
|
+
</Shimmer>
|
|
236
|
+
</template>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Svelte**
|
|
240
|
+
|
|
241
|
+
```svelte
|
|
242
|
+
<script>
|
|
243
|
+
import { Shimmer } from '@shimmer-from-structure/svelte';
|
|
244
|
+
import UserCard from './UserCard.svelte';
|
|
245
|
+
|
|
246
|
+
let { user } = $props();
|
|
247
|
+
let loading = $state(true);
|
|
248
|
+
|
|
249
|
+
const userTemplate = {
|
|
250
|
+
name: 'Loading...',
|
|
251
|
+
role: 'Loading role...',
|
|
252
|
+
avatar: 'placeholder.jpg',
|
|
253
|
+
};
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<Shimmer loading={loading} templateProps={{ user: userTemplate }}>
|
|
257
|
+
<UserCard user={user || userTemplate} />
|
|
258
|
+
</Shimmer>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Angular**
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Component, signal } from '@angular/core';
|
|
265
|
+
import { ShimmerComponent } from '@shimmer-from-structure/angular';
|
|
266
|
+
import { UserCardComponent } from './user-card.component';
|
|
267
|
+
|
|
268
|
+
@Component({
|
|
269
|
+
selector: 'app-root',
|
|
270
|
+
standalone: true,
|
|
271
|
+
imports: [ShimmerComponent, UserCardComponent],
|
|
272
|
+
template: `
|
|
273
|
+
<shimmer [loading]="loading()" [templateProps]="{ user: userTemplate }">
|
|
274
|
+
<app-user-card [user]="user() || userTemplate" />
|
|
275
|
+
</shimmer>
|
|
276
|
+
`,
|
|
277
|
+
})
|
|
278
|
+
export class AppComponent {
|
|
279
|
+
loading = signal(true);
|
|
280
|
+
user = signal<User | null>(null);
|
|
281
|
+
|
|
282
|
+
userTemplate = {
|
|
283
|
+
name: 'Loading...',
|
|
284
|
+
role: 'Loading role...',
|
|
285
|
+
avatar: 'placeholder.jpg',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The `templateProps` object is spread onto the first child component when loading, allowing it to render with mock data for measurement.
|
|
291
|
+
|
|
292
|
+
## 🎨 API Reference
|
|
293
|
+
|
|
294
|
+
### `<Shimmer>` Props
|
|
295
|
+
|
|
296
|
+
| Prop | Type | Default | Description |
|
|
297
|
+
| ---------------------- | ------------------------- | -------------------------- | --------------------------------------------------------- |
|
|
298
|
+
| `loading` | `boolean` | `true` | Whether to show shimmer effect or actual content |
|
|
299
|
+
| `children` | `React.ReactNode` | required | The content to render/measure |
|
|
300
|
+
| `shimmerColor` | `string` | `'rgba(255,255,255,0.15)'` | Color of the shimmer wave |
|
|
301
|
+
| `backgroundColor` | `string` | `'rgba(255,255,255,0.08)'` | Background color of shimmer blocks |
|
|
302
|
+
| `duration` | `number` | `1.5` | Animation duration in seconds |
|
|
303
|
+
| `fallbackBorderRadius` | `number` | `4` | Border radius (px) for elements with no CSS border-radius |
|
|
304
|
+
| `templateProps` | `Record<string, unknown>` | - | Props to inject into first child for skeleton rendering |
|
|
305
|
+
|
|
306
|
+
### Example with All Props
|
|
307
|
+
|
|
308
|
+
**React**
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
<Shimmer
|
|
312
|
+
loading={isLoading}
|
|
313
|
+
shimmerColor="rgba(255, 255, 255, 0.2)"
|
|
314
|
+
backgroundColor="rgba(255, 255, 255, 0.1)"
|
|
315
|
+
duration={2}
|
|
316
|
+
fallbackBorderRadius={8}
|
|
317
|
+
templateProps={{
|
|
318
|
+
user: userTemplate,
|
|
319
|
+
settings: settingsTemplate,
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
<MyComponent user={user} settings={settings} />
|
|
323
|
+
</Shimmer>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Vue**
|
|
327
|
+
|
|
328
|
+
```vue
|
|
329
|
+
<Shimmer
|
|
330
|
+
:loading="isLoading"
|
|
331
|
+
shimmerColor="rgba(255, 255, 255, 0.2)"
|
|
332
|
+
backgroundColor="rgba(255, 255, 255, 0.1)"
|
|
333
|
+
:duration="2"
|
|
334
|
+
:fallbackBorderRadius="8"
|
|
335
|
+
:templateProps="{
|
|
336
|
+
user: userTemplate,
|
|
337
|
+
settings: settingsTemplate,
|
|
338
|
+
}"
|
|
339
|
+
>
|
|
340
|
+
<MyComponent :user="user" :settings="settings" />
|
|
341
|
+
</Shimmer>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Svelte**
|
|
345
|
+
|
|
346
|
+
```svelte
|
|
347
|
+
<Shimmer
|
|
348
|
+
loading={isLoading}
|
|
349
|
+
shimmerColor="rgba(255, 255, 255, 0.2)"
|
|
350
|
+
backgroundColor="rgba(255, 255, 255, 0.1)"
|
|
351
|
+
duration={2}
|
|
352
|
+
fallbackBorderRadius={8}
|
|
353
|
+
templateProps={{
|
|
354
|
+
user: userTemplate,
|
|
355
|
+
settings: settingsTemplate,
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
<MyComponent {user} {settings} />
|
|
359
|
+
</Shimmer>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Angular**
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
<shimmer
|
|
366
|
+
[loading]="isLoading()"
|
|
367
|
+
shimmerColor="rgba(255, 255, 255, 0.2)"
|
|
368
|
+
backgroundColor="rgba(255, 255, 255, 0.1)"
|
|
369
|
+
[duration]="2"
|
|
370
|
+
[fallbackBorderRadius]="8"
|
|
371
|
+
[templateProps]="{
|
|
372
|
+
user: userTemplate,
|
|
373
|
+
settings: settingsTemplate
|
|
374
|
+
}">
|
|
375
|
+
<app-my-component
|
|
376
|
+
[user]="user()"
|
|
377
|
+
[settings]="settings()" />
|
|
378
|
+
</shimmer>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## 🔧 How It Works
|
|
382
|
+
|
|
383
|
+
1. **Visible Container Rendering**: When `loading={true}`, your component renders with transparent text but **visible container backgrounds**
|
|
384
|
+
2. **Template Props Injection**: If `templateProps` is provided, it's spread onto the first child so dynamic components can render
|
|
385
|
+
3. **DOM Measurement**: Uses `useLayoutEffect` to synchronously measure all leaf elements via `getBoundingClientRect()`
|
|
386
|
+
4. **Border Radius Detection**: Automatically captures each element's computed `border-radius` from CSS
|
|
387
|
+
5. **Shimmer Generation**: Creates absolutely-positioned shimmer blocks matching measured dimensions
|
|
388
|
+
6. **Animation**: Applies smooth gradient animation that sweeps across each block
|
|
389
|
+
|
|
390
|
+
### Key Features
|
|
391
|
+
|
|
392
|
+
- **Container backgrounds visible**: Unlike `opacity: 0`, we use `color: transparent` so card backgrounds/borders show during loading
|
|
393
|
+
- **Auto border-radius**: Circular avatars get circular shimmer blocks automatically
|
|
394
|
+
- **Fallback radius**: Text elements (which have `border-radius: 0`) use `fallbackBorderRadius` to avoid sharp rectangles
|
|
395
|
+
- **Dark-mode friendly**: Default colors use semi-transparent whites that work on any background
|
|
396
|
+
|
|
397
|
+
## Examples
|
|
398
|
+
|
|
399
|
+
### Dashboard with Multiple Sections
|
|
400
|
+
|
|
401
|
+
Each section can have its own independent loading state:
|
|
402
|
+
|
|
403
|
+
**React**
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
function Dashboard() {
|
|
407
|
+
const [loadingUser, setLoadingUser] = useState(true);
|
|
408
|
+
const [loadingStats, setLoadingStats] = useState(true);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<>
|
|
412
|
+
{/* User profile section */}
|
|
413
|
+
<Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
|
|
414
|
+
<UserProfile user={user} />
|
|
415
|
+
</Shimmer>
|
|
416
|
+
|
|
417
|
+
{/* Stats section - with custom colors */}
|
|
418
|
+
<Shimmer
|
|
419
|
+
loading={loadingStats}
|
|
420
|
+
templateProps={{ stats: statsTemplate }}
|
|
421
|
+
shimmerColor="rgba(20, 184, 166, 0.2)"
|
|
422
|
+
>
|
|
423
|
+
<StatsGrid stats={stats} />
|
|
424
|
+
</Shimmer>
|
|
425
|
+
</>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Vue**
|
|
431
|
+
|
|
432
|
+
```vue
|
|
433
|
+
<template>
|
|
434
|
+
<!-- User profile section -->
|
|
435
|
+
<Shimmer :loading="loadingUser" :templateProps="{ user: userTemplate }">
|
|
436
|
+
<UserProfile :user="user" />
|
|
437
|
+
</Shimmer>
|
|
438
|
+
|
|
439
|
+
<!-- Stats section - with custom colors -->
|
|
440
|
+
<Shimmer
|
|
441
|
+
:loading="loadingStats"
|
|
442
|
+
:templateProps="{ stats: statsTemplate }"
|
|
443
|
+
shimmerColor="rgba(20, 184, 166, 0.2)"
|
|
444
|
+
>
|
|
445
|
+
<StatsGrid :stats="stats" />
|
|
446
|
+
</Shimmer>
|
|
447
|
+
</template>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Svelte**
|
|
451
|
+
|
|
452
|
+
```svelte
|
|
453
|
+
<Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
|
|
454
|
+
<UserProfile {user} />
|
|
455
|
+
</Shimmer>
|
|
456
|
+
|
|
457
|
+
<Shimmer
|
|
458
|
+
loading={loadingStats}
|
|
459
|
+
templateProps={{ stats: statsTemplate }}
|
|
460
|
+
shimmerColor="rgba(20, 184, 166, 0.2)"
|
|
461
|
+
>
|
|
462
|
+
<StatsGrid {stats} />
|
|
463
|
+
</Shimmer>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Angular**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
@Component({
|
|
470
|
+
template: `
|
|
471
|
+
<!-- User profile section -->
|
|
472
|
+
<shimmer [loading]="loadingUser()" [templateProps]="{ user: userTemplate }">
|
|
473
|
+
<app-user-profile [user]="user()" />
|
|
474
|
+
</shimmer>
|
|
475
|
+
|
|
476
|
+
<!-- Stats section - with custom colors -->
|
|
477
|
+
<shimmer
|
|
478
|
+
[loading]="loadingStats()"
|
|
479
|
+
[templateProps]="{ stats: statsTemplate }"
|
|
480
|
+
shimmerColor="rgba(20, 184, 166, 0.2)"
|
|
481
|
+
>
|
|
482
|
+
<app-stats-grid [stats]="stats()" />
|
|
483
|
+
</shimmer>
|
|
484
|
+
`,
|
|
485
|
+
})
|
|
486
|
+
export class DashboardComponent {
|
|
487
|
+
loadingUser = signal(true);
|
|
488
|
+
loadingStats = signal(true);
|
|
489
|
+
// ...
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Transactions List
|
|
494
|
+
|
|
495
|
+
**React**
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
<Shimmer loading={loadingTransactions} templateProps={{ transactions: transactionsTemplate }}>
|
|
499
|
+
<TransactionsList transactions={transactions} />
|
|
500
|
+
</Shimmer>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Vue**
|
|
504
|
+
|
|
505
|
+
```vue
|
|
506
|
+
<Shimmer :loading="loadingTransactions" :templateProps="{ transactions: transactionsTemplate }">
|
|
507
|
+
<TransactionsList :transactions="transactions" />
|
|
508
|
+
</Shimmer>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Svelte**
|
|
512
|
+
|
|
513
|
+
```svelte
|
|
514
|
+
<Shimmer loading={loadingTransactions} templateProps={{ transactions: transactionsTemplate }}>
|
|
515
|
+
<TransactionsList {transactions} />
|
|
516
|
+
</Shimmer>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Angular**
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
<shimmer
|
|
523
|
+
[loading]="loadingTransactions()"
|
|
524
|
+
[templateProps]="{ transactions: transactionsTemplate }">
|
|
525
|
+
<app-transactions-list [transactions]="transactions()" />
|
|
526
|
+
</shimmer>
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Team Members Grid
|
|
530
|
+
|
|
531
|
+
**React**
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
<Shimmer loading={loadingTeam} templateProps={{ members: teamTemplate }}>
|
|
535
|
+
<TeamMembers members={team} />
|
|
536
|
+
</Shimmer>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Vue**
|
|
540
|
+
|
|
541
|
+
```vue
|
|
542
|
+
<Shimmer :loading="loadingTeam" :templateProps="{ members: teamTemplate }">
|
|
543
|
+
<TeamMembers :members="team" />
|
|
544
|
+
</Shimmer>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Svelte**
|
|
548
|
+
|
|
549
|
+
```svelte
|
|
550
|
+
<Shimmer loading={loadingTeam} templateProps={{ members: teamTemplate }}>
|
|
551
|
+
<TeamMembers members={team} />
|
|
552
|
+
</Shimmer>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Angular**
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
<shimmer
|
|
559
|
+
[loading]="loadingTeam()"
|
|
560
|
+
[templateProps]="{ members: teamTemplate }">
|
|
561
|
+
<app-team-members [members]="team()" />
|
|
562
|
+
</shimmer>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## 🔄 Using with React Suspense
|
|
566
|
+
|
|
567
|
+
Shimmer works seamlessly as a Suspense fallback. When used this way, `loading` is always `true` because React automatically unmounts the fallback and replaces it with the resolved component.
|
|
568
|
+
|
|
569
|
+
### Basic Suspense Pattern
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
import { Suspense, lazy } from 'react';
|
|
573
|
+
import { Shimmer } from 'shimmer-from-structure';
|
|
574
|
+
|
|
575
|
+
const UserProfile = lazy(() => import('./UserProfile'));
|
|
576
|
+
|
|
577
|
+
function App() {
|
|
578
|
+
return (
|
|
579
|
+
<Suspense
|
|
580
|
+
fallback={
|
|
581
|
+
<Shimmer loading={true} templateProps={{ user: userTemplate }}>
|
|
582
|
+
<UserProfile />
|
|
583
|
+
</Shimmer>
|
|
584
|
+
}
|
|
585
|
+
>
|
|
586
|
+
<UserProfile userId="123" />
|
|
587
|
+
</Suspense>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Why `loading={true}` is Always Set
|
|
593
|
+
|
|
594
|
+
When using Shimmer as a Suspense fallback:
|
|
595
|
+
|
|
596
|
+
1. **Suspend**: React renders the fallback → Shimmer shows with `loading={true}`
|
|
597
|
+
2. **Resolve**: React **replaces** the entire fallback with the real component
|
|
598
|
+
3. The Shimmer is **unmounted**, not updated — so you never need to toggle `loading`
|
|
599
|
+
|
|
600
|
+
### Performance Tips for Suspense
|
|
601
|
+
|
|
602
|
+
**Memoize the fallback** to prevent re-renders:
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
const ShimmerFallback = React.memo(() => (
|
|
606
|
+
<Shimmer loading={true} templateProps={{ user: userTemplate }}>
|
|
607
|
+
<UserProfile />
|
|
608
|
+
</Shimmer>
|
|
609
|
+
));
|
|
610
|
+
|
|
611
|
+
// Usage
|
|
612
|
+
<Suspense fallback={<ShimmerFallback />}>
|
|
613
|
+
<UserProfile userId="123" />
|
|
614
|
+
</Suspense>;
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Keep templates lightweight** — the DOM is measured synchronously via `useLayoutEffect`, so avoid complex logic in your template.
|
|
618
|
+
|
|
619
|
+
## Global Configuration
|
|
620
|
+
|
|
621
|
+
You can set default configuration for your entire app (or specific sections) using the context/provider pattern. This is perfect for maintaining consistent themes without repeating props.
|
|
622
|
+
|
|
623
|
+
### React (Context API)
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import { Shimmer, ShimmerProvider } from '@shimmer-from-structure/react';
|
|
627
|
+
|
|
628
|
+
function App() {
|
|
629
|
+
return (
|
|
630
|
+
// Set global defaults
|
|
631
|
+
<ShimmerProvider
|
|
632
|
+
config={{
|
|
633
|
+
shimmerColor: 'rgba(56, 189, 248, 0.4)', // Blue shimmer
|
|
634
|
+
backgroundColor: 'rgba(56, 189, 248, 0.1)', // Blue background
|
|
635
|
+
duration: 2.5,
|
|
636
|
+
fallbackBorderRadius: 8,
|
|
637
|
+
}}
|
|
638
|
+
>
|
|
639
|
+
<Dashboard />
|
|
640
|
+
</ShimmerProvider>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Vue (Provide/Inject)
|
|
646
|
+
|
|
647
|
+
```vue
|
|
648
|
+
<!-- App.vue -->
|
|
649
|
+
<script setup>
|
|
650
|
+
import { provideShimmerConfig } from '@shimmer-from-structure/vue';
|
|
651
|
+
|
|
652
|
+
provideShimmerConfig({
|
|
653
|
+
shimmerColor: 'rgba(56, 189, 248, 0.4)',
|
|
654
|
+
backgroundColor: 'rgba(56, 189, 248, 0.1)',
|
|
655
|
+
duration: 2.5,
|
|
656
|
+
fallbackBorderRadius: 8,
|
|
657
|
+
});
|
|
658
|
+
</script>
|
|
659
|
+
|
|
660
|
+
<template>
|
|
661
|
+
<router-view />
|
|
662
|
+
</template>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Svelte (setShimmerConfig)
|
|
666
|
+
|
|
667
|
+
```svelte
|
|
668
|
+
<!-- App.svelte or any parent component -->
|
|
669
|
+
<script>
|
|
670
|
+
import { setShimmerConfig } from '@shimmer-from-structure/svelte';
|
|
671
|
+
import Dashboard from './Dashboard.svelte';
|
|
672
|
+
|
|
673
|
+
// Must be called at the top level during component initialization
|
|
674
|
+
setShimmerConfig({
|
|
675
|
+
shimmerColor: 'rgba(56, 189, 248, 0.4)',
|
|
676
|
+
backgroundColor: 'rgba(56, 189, 248, 0.1)',
|
|
677
|
+
duration: 2.5,
|
|
678
|
+
fallbackBorderRadius: 8,
|
|
679
|
+
});
|
|
680
|
+
</script>
|
|
681
|
+
|
|
682
|
+
<Dashboard />
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Angular (Dependency Injection)
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
// main.ts or bootstrapApplication
|
|
689
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
690
|
+
import { provideShimmerConfig } from '@shimmer-from-structure/angular';
|
|
691
|
+
import { AppComponent } from './app/app.component';
|
|
692
|
+
|
|
693
|
+
bootstrapApplication(AppComponent, {
|
|
694
|
+
providers: [
|
|
695
|
+
provideShimmerConfig({
|
|
696
|
+
shimmerColor: 'rgba(56, 189, 248, 0.4)',
|
|
697
|
+
backgroundColor: 'rgba(56, 189, 248, 0.1)',
|
|
698
|
+
duration: 2.5,
|
|
699
|
+
fallbackBorderRadius: 8,
|
|
700
|
+
}),
|
|
701
|
+
],
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
Components inside the provider automatically inherit values. You can still override them locally:
|
|
708
|
+
|
|
709
|
+
**React**
|
|
710
|
+
|
|
711
|
+
```tsx
|
|
712
|
+
// Inherits blue theme from provider
|
|
713
|
+
<Shimmer loading={true}><UserCard /></Shimmer>
|
|
714
|
+
|
|
715
|
+
// Overrides provider settings
|
|
716
|
+
<Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**Vue**
|
|
720
|
+
|
|
721
|
+
```vue
|
|
722
|
+
<!-- Inherits blue theme from provider -->
|
|
723
|
+
<Shimmer :loading="true"><UserCard /></Shimmer>
|
|
724
|
+
|
|
725
|
+
<!-- Overrides provider settings -->
|
|
726
|
+
<Shimmer :loading="true" :duration="0.5"><FastCard /></Shimmer>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Svelte**
|
|
730
|
+
|
|
731
|
+
```svelte
|
|
732
|
+
<!-- Inherits blue theme from provider -->
|
|
733
|
+
<Shimmer loading={true}><UserCard /></Shimmer>
|
|
734
|
+
|
|
735
|
+
<!-- Overrides provider settings -->
|
|
736
|
+
<Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Angular**
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
<!-- Inherits blue theme from injected config -->
|
|
743
|
+
<shimmer [loading]="true"><app-user-card /></shimmer>
|
|
744
|
+
|
|
745
|
+
<!-- Overrides injected settings -->
|
|
746
|
+
<shimmer [loading]="true" [duration]="0.5"><app-fast-card /></shimmer>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Accessing Config in Hooks/Composables
|
|
750
|
+
|
|
751
|
+
If you need to access the current configuration in your own components:
|
|
752
|
+
|
|
753
|
+
**React**
|
|
754
|
+
|
|
755
|
+
```tsx
|
|
756
|
+
import { useShimmerConfig } from 'shimmer-from-structure';
|
|
757
|
+
|
|
758
|
+
function MyComponent() {
|
|
759
|
+
const config = useShimmerConfig();
|
|
760
|
+
return <div style={{ background: config.backgroundColor }}>...</div>;
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Vue**
|
|
765
|
+
|
|
766
|
+
```javascript
|
|
767
|
+
import { useShimmerConfig } from '@shimmer-from-structure/vue';
|
|
768
|
+
|
|
769
|
+
const config = useShimmerConfig();
|
|
770
|
+
console.log(config.value.backgroundColor);
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Svelte**
|
|
774
|
+
|
|
775
|
+
```javascript
|
|
776
|
+
import { getShimmerConfig } from '@shimmer-from-structure/svelte';
|
|
777
|
+
|
|
778
|
+
const config = getShimmerConfig();
|
|
779
|
+
console.log(config.backgroundColor);
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Angular**
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
import { Component, inject } from '@angular/core';
|
|
786
|
+
import { injectShimmerConfig } from '@shimmer-from-structure/angular';
|
|
787
|
+
|
|
788
|
+
@Component({
|
|
789
|
+
selector: 'app-my-component',
|
|
790
|
+
template: `<div [style.background]="config.backgroundColor">...</div>`,
|
|
791
|
+
})
|
|
792
|
+
export class MyComponent {
|
|
793
|
+
config = injectShimmerConfig();
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
## Best Practices
|
|
798
|
+
|
|
799
|
+
### 1. Use `templateProps` for Dynamic Data
|
|
800
|
+
|
|
801
|
+
When your component receives data via props, always provide `templateProps` with mock data that matches the expected structure.
|
|
802
|
+
|
|
803
|
+
### 2. Match Template Structure to Real Data
|
|
804
|
+
|
|
805
|
+
Ensure your template data has the same array length and property structure as real data for accurate shimmer layout.
|
|
806
|
+
|
|
807
|
+
### 3. Use Individual Shimmer Components
|
|
808
|
+
|
|
809
|
+
Wrap each section in its own Shimmer for independent loading states:
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
// ✅ Good - independent loading
|
|
813
|
+
<Shimmer loading={loadingUsers}><UserList /></Shimmer>
|
|
814
|
+
<Shimmer loading={loadingPosts}><PostList /></Shimmer>
|
|
815
|
+
|
|
816
|
+
// ❌ Avoid - all-or-nothing loading
|
|
817
|
+
<Shimmer loading={loadingUsers || loadingPosts}>
|
|
818
|
+
<UserList />
|
|
819
|
+
<PostList />
|
|
820
|
+
</Shimmer>
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### 4. Consider Element Widths
|
|
824
|
+
|
|
825
|
+
Block elements like `<h1>`, `<p>` take full container width. If you want shimmer to match text width:
|
|
826
|
+
|
|
827
|
+
```css
|
|
828
|
+
.title {
|
|
829
|
+
width: fit-content;
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### 5. Provide Container Dimensions
|
|
834
|
+
|
|
835
|
+
For async components (like charts), ensure containers have explicit dimensions so shimmer has something to measure.
|
|
836
|
+
|
|
837
|
+
## ⚡ Performance Considerations
|
|
838
|
+
|
|
839
|
+
- Measurement happens only when `loading` changes to `true`
|
|
840
|
+
- Uses `useLayoutEffect` for synchronous measurement (no flicker)
|
|
841
|
+
- Minimal re-renders - only updates when loading state or children change
|
|
842
|
+
- Lightweight DOM measurements using native browser APIs
|
|
843
|
+
|
|
844
|
+
- Lightweight DOM measurements using native browser APIs
|
|
845
|
+
|
|
846
|
+
## 🛠️ Development
|
|
847
|
+
|
|
848
|
+
This is a monorepo managed with npm workspaces. Each package can be built independently:
|
|
849
|
+
|
|
850
|
+
```bash
|
|
851
|
+
# Install dependencies
|
|
852
|
+
npm install
|
|
853
|
+
|
|
854
|
+
# Build all packages
|
|
855
|
+
npm run build
|
|
856
|
+
|
|
857
|
+
# Build individual packages
|
|
858
|
+
npm run build:core
|
|
859
|
+
npm run build:react
|
|
860
|
+
npm run build:vue
|
|
861
|
+
npm run build:svelte
|
|
862
|
+
npm run build:main
|
|
863
|
+
|
|
864
|
+
# Run tests
|
|
865
|
+
npm test
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## 📝 License
|
|
869
|
+
|
|
870
|
+
MIT
|
|
871
|
+
|
|
872
|
+
## 🤝 Contributing
|
|
873
|
+
|
|
874
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
875
|
+
|
|
876
|
+
## 🐛 Known Limitations
|
|
877
|
+
|
|
878
|
+
- **Async components**: Components that render asynchronously (like charts using `ResponsiveContainer`) may need explicit container dimensions
|
|
879
|
+
- **Zero-dimension elements**: Elements with `display: none` or zero dimensions won't be captured
|
|
880
|
+
- **SVG internals**: Only the outer `<svg>` element is captured, not internal paths/shapes
|
|
881
|
+
|
|
882
|
+
## 🏗️ Monorepo Structure
|
|
883
|
+
|
|
884
|
+
This library is organized as a monorepo with four packages:
|
|
885
|
+
|
|
886
|
+
| Package | Description | Size |
|
|
887
|
+
| --------------------------------- | ------------------------------------------- | -------- |
|
|
888
|
+
| `@shimmer-from-structure/core` | Framework-agnostic DOM utilities | 1.44 kB |
|
|
889
|
+
| `@shimmer-from-structure/react` | React adapter | 12.84 kB |
|
|
890
|
+
| `@shimmer-from-structure/vue` | Vue 3 adapter | 3.89 kB |
|
|
891
|
+
| `@shimmer-from-structure/svelte` | Svelte adapter | 4.60 kB |
|
|
892
|
+
| `@shimmer-from-structure/angular` | Angular adapter | 6.83 kB |
|
|
893
|
+
| `shimmer-from-structure` | Main package (React backward compatibility) | 0.93 kB |
|
|
894
|
+
|
|
895
|
+
The core package contains all DOM measurement logic, while React, Vue, Svelte and Angular packages are thin wrappers that provide framework-specific APIs.
|
|
896
|
+
|
|
897
|
+
## 🚧 Roadmap
|
|
898
|
+
|
|
899
|
+
- [x] Dynamic data support via `templateProps`
|
|
900
|
+
- [x] Auto border-radius detection
|
|
901
|
+
- [x] Container background visibility
|
|
902
|
+
- [x] **Vue.js adapter**
|
|
903
|
+
- [x] **Svelte adapter**
|
|
904
|
+
- [x] **Angular adapter**
|
|
905
|
+
- [ ] Better async component support
|
|
906
|
+
- [ ] Customizable shimmer direction (vertical, diagonal)
|
|
907
|
+
- [ ] React Native support
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
Made with ❤️ for developers tired of maintaining skeleton screens
|