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 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>&nbsp;</h3>
335
+ <span>&nbsp;</span>
336
+ <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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.