shimmer-trace 1.1.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/AGENTS.md +657 -0
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/index.d.mts +224 -0
- package/dist/index.d.ts +224 -0
- package/dist/index.js +601 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +590 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# shimmer-trace — Agent & LLM Reference
|
|
2
|
+
|
|
3
|
+
> This file is written for AI agents, LLMs, and prompt engineers.
|
|
4
|
+
> It gives you everything needed to correctly generate, debug, and reason about shimmer-trace usage.
|
|
5
|
+
> Human-readable README is at `README.md`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What This Library Does (One Paragraph)
|
|
10
|
+
|
|
11
|
+
`shimmer-trace` is a React skeleton loading library. It renders your real component invisibly, walks the live DOM to find every visible leaf element (headings, paragraphs, images, inputs, buttons, etc.), measures each one's exact position and size via `getBoundingClientRect()`, then paints an absolutely-positioned shimmer overlay on top of those measured rects. The skeleton is therefore pixel-perfect and auto-updates via `ResizeObserver`. You never write a manual skeleton. You wrap your component and pass `loading={true}`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install shimmer-trace
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Peer deps required:** `react >= 18.0.0`, `react-dom >= 18.0.0`
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Public API — Full Export List
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// Components
|
|
29
|
+
import { Shimmer } from 'shimmer-trace'; // main component
|
|
30
|
+
import { ShimmerSuspense } from 'shimmer-trace'; // Suspense boundary with auto-skeleton
|
|
31
|
+
import { createShimmer } from 'shimmer-trace'; // factory — bakes config into component
|
|
32
|
+
|
|
33
|
+
// Hooks
|
|
34
|
+
import { useIsShimmering } from 'shimmer-trace'; // true when inside ShimmerSuspense fallback
|
|
35
|
+
import { useShimmerContext } from 'shimmer-trace'; // raw context — advanced use only
|
|
36
|
+
|
|
37
|
+
// Types
|
|
38
|
+
import type {
|
|
39
|
+
ShimmerProps,
|
|
40
|
+
ShimmerConfig,
|
|
41
|
+
ShimmerRect,
|
|
42
|
+
AnimationType,
|
|
43
|
+
ShimmerSuspenseProps,
|
|
44
|
+
} from 'shimmer-trace';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Types Reference
|
|
50
|
+
|
|
51
|
+
### `AnimationType`
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type AnimationType = 'wave' | 'pulse' | 'shine' | 'glow' | 'gradient';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Value | Behaviour |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `wave` | Horizontal gradient sweep left→right across full container width. Default. |
|
|
60
|
+
| `pulse` | Opacity oscillates 0.4→1→0.4. No movement. |
|
|
61
|
+
| `shine` | Diagonal sweep (115°) with skew — more premium feel than wave. |
|
|
62
|
+
| `glow` | Brightness oscillates 1→1.35→1 via CSS filter. |
|
|
63
|
+
| `gradient` | Block background itself animates as sliding gradient (no child layer). |
|
|
64
|
+
|
|
65
|
+
### `ShimmerConfig`
|
|
66
|
+
|
|
67
|
+
All fields optional. Used by `Shimmer` props, `createShimmer`, and `ShimmerSuspense`.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
interface ShimmerConfig {
|
|
71
|
+
animation?: AnimationType; // default: 'wave'
|
|
72
|
+
baseColor?: string; // default: '#e0e0e0' (CSS color, any format)
|
|
73
|
+
highlightColor?: string; // default: '#f5f5f5' (CSS color, any format)
|
|
74
|
+
speed?: number; // default: 1.5 (seconds, float)
|
|
75
|
+
borderRadius?: string; // default: '' (CSS value e.g. '8px'. Empty = auto-detect)
|
|
76
|
+
preserveBackground?: boolean; // default: true
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**`preserveBackground` explained:**
|
|
81
|
+
- `true` (default): Master container stays visible. CSS rules hide text (`color:transparent`) and media (`opacity:0`) on leaf tags while keeping div backgrounds, borders, padding visible.
|
|
82
|
+
- `false`: Legacy mode. Master container gets `visibility:hidden`. Everything hidden. Overlay punches through with `visibility:visible`.
|
|
83
|
+
|
|
84
|
+
### `ShimmerProps`
|
|
85
|
+
|
|
86
|
+
Extends `ShimmerConfig`. All `ShimmerConfig` fields inherit plus:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
interface ShimmerProps extends ShimmerConfig {
|
|
90
|
+
loading?: boolean; // default: false
|
|
91
|
+
children: ReactNode; // required
|
|
92
|
+
dummyLength?: number; // clone count for list mode
|
|
93
|
+
dummyData?: Record<string, any>; // props merged into children while loading
|
|
94
|
+
as?: React.ComponentType<any>; // component type for skeleton shape
|
|
95
|
+
stopPropagation?: boolean; // force Master even when nested
|
|
96
|
+
className?: string; // applied to Master container div
|
|
97
|
+
style?: React.CSSProperties; // merged into Master container div
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `ShimmerSuspenseProps`
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
interface ShimmerSuspenseProps extends ShimmerConfig {
|
|
105
|
+
children: ReactNode;
|
|
106
|
+
template?: ReactNode; // explicit skeleton shape; if omitted uses useIsShimmering pattern
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `ShimmerRect` (internal, rarely needed by consumers)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
interface ShimmerRect {
|
|
114
|
+
x: number; // left offset from Master container
|
|
115
|
+
y: number; // top offset from Master container
|
|
116
|
+
width: number;
|
|
117
|
+
height: number;
|
|
118
|
+
borderRadius: string;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Architecture — How It Works Internally
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
<Shimmer loading={true}> ← MasterShimmer
|
|
128
|
+
children render hidden ← visibility:hidden OR color:transparent
|
|
129
|
+
useTrace() walks DOM ← collectTraceableElements → getBoundingClientRect
|
|
130
|
+
ResizeObserver re-traces ← on container resize
|
|
131
|
+
<ShimmerOverlay rects={...} /> ← absolutely positioned, z-index:1
|
|
132
|
+
one <div> per traced rect ← base color + optional SweepLayer child
|
|
133
|
+
SweepLayer spans container ← gradient child, full container width, synced wave
|
|
134
|
+
|
|
135
|
+
<Shimmer> nested inside ← ReporterShimmer (auto-detected via context)
|
|
136
|
+
useTrace() walks own DOM ← measures relative to Master container
|
|
137
|
+
register(id, rects) to Master ← rects bubble up to single overlay
|
|
138
|
+
unregister on unmount ← cleanup
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Traceable elements** (auto-detected by tag name):
|
|
142
|
+
`H1 H2 H3 H4 H5 H6 P SPAN A LI LABEL TD TH BLOCKQUOTE CODE PRE IMG VIDEO SVG CANVAS PICTURE INPUT TEXTAREA SELECT BUTTON HR`
|
|
143
|
+
|
|
144
|
+
Plus any element with `data-shimmer` attribute, or leaf elements (no children) with non-zero dimensions.
|
|
145
|
+
|
|
146
|
+
**Ignored elements:**
|
|
147
|
+
- Any element with `data-shimmer-ignore` attribute
|
|
148
|
+
- Any element with `data-shimmer-reporter` attribute (Reporter wrappers)
|
|
149
|
+
|
|
150
|
+
**Fallback dimensions** (used when element has zero size):
|
|
151
|
+
`INPUT→200×36 BUTTON→120×36 TEXTAREA→300×80 SELECT→200×36 IMG→100×100 H1→300×36 H2→260×30 H3→220×26 H4→200×22 H5→180×20 H6→160×18 P→250×16 SPAN→100×16`
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Pattern 1 — Basic Wrap (Most Common)
|
|
156
|
+
|
|
157
|
+
Use when: component renders fine with real data, just need skeleton while loading.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { Shimmer } from 'shimmer-trace';
|
|
161
|
+
|
|
162
|
+
function ProfilePage() {
|
|
163
|
+
const [user, setUser] = useState(null);
|
|
164
|
+
const [loading, setLoading] = useState(true);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
fetchUser().then(u => { setUser(u); setLoading(false); });
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Shimmer loading={loading}>
|
|
172
|
+
<UserCard user={user} />
|
|
173
|
+
</Shimmer>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**What happens:** while `loading=true`, `UserCard` renders with `user=null`. shimmer-trace traces whatever DOM `UserCard` produces and paints shimmer over it.
|
|
179
|
+
|
|
180
|
+
**Problem:** if `UserCard` renders nothing when `user=null` (early return, conditional rendering), skeleton is empty.
|
|
181
|
+
|
|
182
|
+
**Solution:** use `dummyData`.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Pattern 2 — `dummyData` (Recommended for Null-Safe Components)
|
|
187
|
+
|
|
188
|
+
Use when: component conditionally renders / early-returns when data is null.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const userTemplate = {
|
|
192
|
+
name: 'Loading name',
|
|
193
|
+
role: 'Loading role',
|
|
194
|
+
bio: 'Loading bio text here',
|
|
195
|
+
avatar: '',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
<Shimmer loading={loading} dummyData={{ user: userTemplate }}>
|
|
199
|
+
<UserCard user={user} />
|
|
200
|
+
</Shimmer>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**What happens:** while `loading=true`, `UserCard` is cloned with `{ user: userTemplate }` merged as props. Component renders with dummy data → full DOM → shimmer traces it.
|
|
204
|
+
|
|
205
|
+
**Rule:** keys in `dummyData` must match prop names of the direct child component.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Pattern 3 — `as` Prop (Clean List Pattern)
|
|
210
|
+
|
|
211
|
+
Use when: rendering a list where real data starts empty `[]` and you want N skeleton cards.
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
const movieTemplate = {
|
|
215
|
+
movie: {
|
|
216
|
+
id: 0,
|
|
217
|
+
title: 'Loading title',
|
|
218
|
+
poster_path: '',
|
|
219
|
+
release_date: '0000-00-00',
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
<Shimmer
|
|
224
|
+
loading={loading}
|
|
225
|
+
as={MovieCard}
|
|
226
|
+
dummyData={movieTemplate}
|
|
227
|
+
dummyLength={10}
|
|
228
|
+
className="movies-grid"
|
|
229
|
+
>
|
|
230
|
+
{movies.map(m => <MovieCard movie={m} key={m.id} />)}
|
|
231
|
+
</Shimmer>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**What happens:** while `loading=true`, children ignored. Renders `10` instances of `<MovieCard {...movieTemplate} />`. shimmer traces them. When `loading=false`, real `movies.map(...)` renders.
|
|
235
|
+
|
|
236
|
+
**`as` vs `dummyData` distinction:**
|
|
237
|
+
- `as={MovieCard}` + `dummyData` + `dummyLength` → renders N instances of the component, ignores children
|
|
238
|
+
- `dummyData` only (no `as`) → clones existing children with dummy props merged in
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Pattern 4 — `dummyLength` Without `as` (Inline List)
|
|
243
|
+
|
|
244
|
+
Use when: children already render a list, data starts empty, no separate component ref needed.
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
const items = loading ? Array(5).fill({ name: 'Loading', price: '$0.00' }) : realItems;
|
|
248
|
+
|
|
249
|
+
<Shimmer loading={loading}>
|
|
250
|
+
{items.map((item, i) => (
|
|
251
|
+
<ItemCard item={item} key={i} />
|
|
252
|
+
))}
|
|
253
|
+
</Shimmer>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Or with `dummyLength` on first child clone:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
<Shimmer loading={loading} dummyLength={5} dummyData={{ item: { name: 'Loading', price: '$0.00' } }}>
|
|
260
|
+
<ItemCard item={realItem} />
|
|
261
|
+
</Shimmer>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Pattern 5 — Nested Shimmer (Single Sync Wave)
|
|
267
|
+
|
|
268
|
+
Use when: multiple sub-sections exist but you want one synchronized shimmer across all.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// Master wraps everything — one overlay, one wave
|
|
272
|
+
<Shimmer loading={loading} style={{ display: 'flex', gap: '1rem' }}>
|
|
273
|
+
<StatCard value="4,821" label="Users" />
|
|
274
|
+
<StatCard value="98.4%" label="Uptime" />
|
|
275
|
+
<StatCard value="142ms" label="Latency" />
|
|
276
|
+
</Shimmer>
|
|
277
|
+
|
|
278
|
+
// Reporter auto-detected when Shimmer is nested inside another Shimmer
|
|
279
|
+
<Shimmer loading={loading}>
|
|
280
|
+
<Header />
|
|
281
|
+
<Shimmer> {/* ← auto-becomes Reporter, bubbles rects to Master above */}
|
|
282
|
+
<Sidebar />
|
|
283
|
+
</Shimmer>
|
|
284
|
+
<Content />
|
|
285
|
+
</Shimmer>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Key rule:** inner `<Shimmer>` without `loading` prop inside an outer `<Shimmer>` auto-becomes a Reporter. It measures its own subtree and sends rects to the Master. One overlay covers everything.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Pattern 6 — `createShimmer` Factory
|
|
293
|
+
|
|
294
|
+
Use when: same config needed in many places. Avoids prop repetition.
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { createShimmer } from 'shimmer-trace';
|
|
298
|
+
|
|
299
|
+
// Define once — usually in a shared file
|
|
300
|
+
const AppShimmer = createShimmer({
|
|
301
|
+
baseColor: '#1e1e3a',
|
|
302
|
+
highlightColor: '#2d2d52',
|
|
303
|
+
animation: 'shine',
|
|
304
|
+
speed: 1.2,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Use like a normal component anywhere
|
|
308
|
+
<AppShimmer loading={loading}>
|
|
309
|
+
<UserCard user={user} />
|
|
310
|
+
</AppShimmer>
|
|
311
|
+
|
|
312
|
+
// Props still overridable per-instance
|
|
313
|
+
<AppShimmer loading={loading} animation="glow" dummyLength={5} as={MovieCard} dummyData={template}>
|
|
314
|
+
{movies.map(m => <MovieCard movie={m} key={m.id} />)}
|
|
315
|
+
</AppShimmer>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Pattern 7 — `ShimmerSuspense` (React Suspense Integration)
|
|
321
|
+
|
|
322
|
+
Use when: component uses `use()`, `useSuspenseQuery`, or similar — throws a Promise while loading.
|
|
323
|
+
|
|
324
|
+
### Option A — `template` prop (component has zero shimmer awareness)
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
import { ShimmerSuspense } from 'shimmer-trace';
|
|
328
|
+
|
|
329
|
+
// Skeleton template: same structure, no real data
|
|
330
|
+
const UserCardTemplate = () => (
|
|
331
|
+
<div className="profile-card">
|
|
332
|
+
<img className="avatar" src="" alt="" />
|
|
333
|
+
<div className="info">
|
|
334
|
+
<h3> </h3>
|
|
335
|
+
<span> </span>
|
|
336
|
+
<p> </p>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
<ShimmerSuspense template={<UserCardTemplate />} animation="wave">
|
|
342
|
+
<UserCard /> {/* throws Promise while fetching — shimmer shows automatically */}
|
|
343
|
+
</ShimmerSuspense>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Option B — `useIsShimmering` hook (component skips fetch in shimmer mode)
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { ShimmerSuspense, useIsShimmering } from 'shimmer-trace';
|
|
350
|
+
|
|
351
|
+
function UserCard() {
|
|
352
|
+
const isShimmering = useIsShimmering();
|
|
353
|
+
|
|
354
|
+
// Skip the suspending call when rendering as skeleton shape
|
|
355
|
+
const user = isShimmering ? null : use(userPromise);
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<div className="profile-card">
|
|
359
|
+
<img className="avatar" src={user?.avatar ?? ''} alt="" />
|
|
360
|
+
<div className="info">
|
|
361
|
+
<h3>{user?.name ?? ' '}</h3>
|
|
362
|
+
<span>{user?.role ?? ' '}</span>
|
|
363
|
+
<p>{user?.bio ?? ' '}</p>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
<ShimmerSuspense animation="shine">
|
|
370
|
+
<UserCard />
|
|
371
|
+
</ShimmerSuspense>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Which option to use:**
|
|
375
|
+
- Option A → component is a library component or you can't modify it
|
|
376
|
+
- Option B → you own the component and want it to self-describe its skeleton shape
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Pattern 8 — `stopPropagation` (Force Independent Master)
|
|
381
|
+
|
|
382
|
+
Use when: nested Shimmer should NOT bubble rects to parent — it manages its own overlay.
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
<Shimmer loading={outerLoading}>
|
|
386
|
+
<Header />
|
|
387
|
+
<Shimmer loading={innerLoading} stopPropagation={true}>
|
|
388
|
+
{/* This is a fully independent Master, not a Reporter */}
|
|
389
|
+
<Widget />
|
|
390
|
+
</Shimmer>
|
|
391
|
+
</Shimmer>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Pattern 9 — `data-shimmer` / `data-shimmer-ignore` Escape Hatches
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
// Force trace on a custom element not in the tag whitelist
|
|
400
|
+
<div data-shimmer>
|
|
401
|
+
<CustomCanvas />
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
// Exclude an element from tracing (e.g. decorative icon)
|
|
405
|
+
<span data-shimmer-ignore>
|
|
406
|
+
<DecorativeIcon />
|
|
407
|
+
</span>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Config Inheritance Chain
|
|
413
|
+
|
|
414
|
+
When props are omitted, they resolve in this order:
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
prop value → parent Shimmer context config → DEFAULTS
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`DEFAULTS`:
|
|
421
|
+
```ts
|
|
422
|
+
{
|
|
423
|
+
animation: 'wave',
|
|
424
|
+
baseColor: '#e0e0e0',
|
|
425
|
+
highlightColor: '#f5f5f5',
|
|
426
|
+
speed: 1.5,
|
|
427
|
+
borderRadius: '', // empty = auto-detect per element
|
|
428
|
+
preserveBackground: true,
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Decision Tree — Which Pattern to Use
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
Need skeleton loader?
|
|
438
|
+
│
|
|
439
|
+
├─ Component uses Suspense (throws Promise)?
|
|
440
|
+
│ └─ YES → ShimmerSuspense
|
|
441
|
+
│ ├─ Can modify component? → Option B (useIsShimmering)
|
|
442
|
+
│ └─ Cannot modify? → Option A (template prop)
|
|
443
|
+
│
|
|
444
|
+
└─ NO → Shimmer with loading prop
|
|
445
|
+
│
|
|
446
|
+
├─ List of N items, data starts empty []?
|
|
447
|
+
│ └─ Use as={MyCard} + dummyData + dummyLength
|
|
448
|
+
│
|
|
449
|
+
├─ Single component, renders null when data=null?
|
|
450
|
+
│ └─ Use dummyData with template values
|
|
451
|
+
│
|
|
452
|
+
├─ Single component, renders partial shape with null data?
|
|
453
|
+
│ └─ Plain wrap, no dummyData needed
|
|
454
|
+
│
|
|
455
|
+
├─ Same config used across many places?
|
|
456
|
+
│ └─ createShimmer factory, use factory component
|
|
457
|
+
│
|
|
458
|
+
└─ Multiple nested Shimmers, want one sync wave?
|
|
459
|
+
└─ Wrap all in single Master <Shimmer>, inner become Reporters automatically
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Common Mistakes Agents Must Avoid
|
|
465
|
+
|
|
466
|
+
### ❌ Wrong — `dummyData` key doesn't match prop name
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
// Component receives prop named `user`
|
|
470
|
+
<UserCard user={data} />
|
|
471
|
+
|
|
472
|
+
// WRONG — key is `userData`, not `user`
|
|
473
|
+
<Shimmer loading={loading} dummyData={{ userData: template }}>
|
|
474
|
+
<UserCard user={data} />
|
|
475
|
+
</Shimmer>
|
|
476
|
+
|
|
477
|
+
// CORRECT — key matches prop name exactly
|
|
478
|
+
<Shimmer loading={loading} dummyData={{ user: template }}>
|
|
479
|
+
<UserCard user={data} />
|
|
480
|
+
</Shimmer>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### ❌ Wrong — using `as` without `dummyData` when component needs props
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
// WRONG — MovieCard will crash because movie=undefined
|
|
487
|
+
<Shimmer loading={loading} as={MovieCard} dummyLength={5}>
|
|
488
|
+
...
|
|
489
|
+
</Shimmer>
|
|
490
|
+
|
|
491
|
+
// CORRECT
|
|
492
|
+
<Shimmer loading={loading} as={MovieCard} dummyData={{ movie: movieTemplate }} dummyLength={5}>
|
|
493
|
+
...
|
|
494
|
+
</Shimmer>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### ❌ Wrong — forgetting that `as` prop ignores `children` during loading
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
// WRONG — dev expects children to also show during loading
|
|
501
|
+
<Shimmer loading={loading} as={SkeletonShape}>
|
|
502
|
+
<RealContent /> {/* This is ignored while loading=true when `as` is set */}
|
|
503
|
+
</Shimmer>
|
|
504
|
+
|
|
505
|
+
// CORRECT — `as` is for the skeleton shape only; real children render when loading=false
|
|
506
|
+
// That is the intended behavior — not a bug.
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### ❌ Wrong — `ShimmerSuspense` Option B without `useIsShimmering` guard
|
|
510
|
+
|
|
511
|
+
```tsx
|
|
512
|
+
// WRONG — component always calls use(promise), throws inside fallback, empty skeleton
|
|
513
|
+
function UserCard() {
|
|
514
|
+
const user = use(userPromise); // throws in fallback → skeleton has no shape
|
|
515
|
+
return <div><h3>{user.name}</h3></div>;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// CORRECT
|
|
519
|
+
function UserCard() {
|
|
520
|
+
const isShimmering = useIsShimmering();
|
|
521
|
+
const user = isShimmering ? null : use(userPromise);
|
|
522
|
+
return <div><h3>{user?.name ?? ' '}</h3></div>;
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### ❌ Wrong — expecting `display:flex` on children, not on Shimmer container
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
// WRONG — Shimmer container is position:relative, flex on children has no effect on layout
|
|
530
|
+
<div style={{ display: 'flex' }}>
|
|
531
|
+
<Shimmer loading={loading}>
|
|
532
|
+
<Card /><Card /><Card />
|
|
533
|
+
</Shimmer>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
// CORRECT — put flex on Shimmer itself via style prop
|
|
537
|
+
<Shimmer loading={loading} style={{ display: 'flex', gap: '1rem' }}>
|
|
538
|
+
<Card /><Card /><Card />
|
|
539
|
+
</Shimmer>
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### ❌ Wrong — passing `loading` to inner nested Shimmer expecting independent behavior
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
// This creates a Reporter (ignores its own loading prop), not an independent Master
|
|
546
|
+
<Shimmer loading={outerLoading}>
|
|
547
|
+
<Shimmer loading={innerLoading}> {/* loading ignored — behaves as Reporter */}
|
|
548
|
+
<Widget />
|
|
549
|
+
</Shimmer>
|
|
550
|
+
</Shimmer>
|
|
551
|
+
|
|
552
|
+
// To make it independent, add stopPropagation
|
|
553
|
+
<Shimmer loading={outerLoading}>
|
|
554
|
+
<Shimmer loading={innerLoading} stopPropagation={true}>
|
|
555
|
+
<Widget />
|
|
556
|
+
</Shimmer>
|
|
557
|
+
</Shimmer>
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## CSS Side Effects
|
|
563
|
+
|
|
564
|
+
Library injects one `<style>` tag with id `shimmer-trace-styles` into `document.head` on first render. Contains only `@keyframes` definitions and `preserveBackground` CSS attribute selectors. Safe to SSR — guard is `typeof document === 'undefined'`.
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## TypeScript Usage
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
import type { AnimationType, ShimmerConfig, ShimmerProps } from 'shimmer-trace';
|
|
572
|
+
|
|
573
|
+
// Type a config object
|
|
574
|
+
const config: ShimmerConfig = {
|
|
575
|
+
animation: 'shine',
|
|
576
|
+
baseColor: '#1a1a2e',
|
|
577
|
+
speed: 1.2,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Type guard for animation values
|
|
581
|
+
const anim: AnimationType = 'wave'; // 'wave'|'pulse'|'shine'|'glow'|'gradient'
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Full Working Example — Movie App
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
import { Shimmer, createShimmer } from 'shimmer-trace';
|
|
590
|
+
|
|
591
|
+
// Themed factory
|
|
592
|
+
const DarkShimmer = createShimmer({
|
|
593
|
+
baseColor: '#1e1e3a',
|
|
594
|
+
highlightColor: '#2d2d52',
|
|
595
|
+
animation: 'wave',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Template data matching MovieCard's prop shape
|
|
599
|
+
const movieTemplate = {
|
|
600
|
+
movie: {
|
|
601
|
+
id: 0,
|
|
602
|
+
title: 'Loading title',
|
|
603
|
+
poster_path: '',
|
|
604
|
+
release_date: '0000-00-00',
|
|
605
|
+
vote_average: 0,
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
function MoviePage() {
|
|
610
|
+
const [movies, setMovies] = useState<Movie[]>([]);
|
|
611
|
+
const [loading, setLoading] = useState(true);
|
|
612
|
+
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
getPopularMovies()
|
|
615
|
+
.then(setMovies)
|
|
616
|
+
.finally(() => setLoading(false));
|
|
617
|
+
}, []);
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<DarkShimmer
|
|
621
|
+
loading={loading}
|
|
622
|
+
as={MovieCard}
|
|
623
|
+
dummyData={movieTemplate}
|
|
624
|
+
dummyLength={12}
|
|
625
|
+
className="movies-grid"
|
|
626
|
+
>
|
|
627
|
+
{movies.map(m => <MovieCard movie={m} key={m.id} />)}
|
|
628
|
+
</DarkShimmer>
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Minimal Reproducible Snippet (for debugging)
|
|
636
|
+
|
|
637
|
+
```tsx
|
|
638
|
+
import { Shimmer } from 'shimmer-trace';
|
|
639
|
+
|
|
640
|
+
export default function Debug() {
|
|
641
|
+
const [loading, setLoading] = React.useState(true);
|
|
642
|
+
return (
|
|
643
|
+
<>
|
|
644
|
+
<button onClick={() => setLoading(l => !l)}>Toggle</button>
|
|
645
|
+
<Shimmer loading={loading}>
|
|
646
|
+
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
|
|
647
|
+
<h3>Title here</h3>
|
|
648
|
+
<p>Description text goes here in this paragraph.</p>
|
|
649
|
+
<button>Action</button>
|
|
650
|
+
</div>
|
|
651
|
+
</Shimmer>
|
|
652
|
+
</>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
If this produces no skeleton rects, the children rendered nothing (empty DOM). Add `dummyData` or use `as` prop.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeet Vora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|