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/dist/index.mjs ADDED
@@ -0,0 +1,590 @@
1
+ import React2, { createContext, useContext, useId, useMemo, useRef, useState, useCallback, createElement, useLayoutEffect } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/Shimmer.tsx
5
+
6
+ // src/types.ts
7
+ var DEFAULTS = {
8
+ animation: "wave",
9
+ baseColor: "#e0e0e0",
10
+ highlightColor: "#f5f5f5",
11
+ speed: 1.5,
12
+ borderRadius: "",
13
+ preserveBackground: true
14
+ };
15
+ var ShimmerContext = createContext(null);
16
+ function useShimmerContext() {
17
+ return useContext(ShimmerContext);
18
+ }
19
+ var IsShimmeringContext = createContext(false);
20
+ function useIsShimmering() {
21
+ return useContext(IsShimmeringContext);
22
+ }
23
+ function getBlockStyles(animation, baseColor, highlightColor, speed, rect) {
24
+ const base = {
25
+ position: "absolute",
26
+ top: rect.y,
27
+ left: rect.x,
28
+ width: rect.width,
29
+ height: rect.height,
30
+ borderRadius: rect.borderRadius,
31
+ overflow: "hidden"
32
+ };
33
+ switch (animation) {
34
+ case "wave":
35
+ case "shine":
36
+ return { ...base, background: baseColor };
37
+ case "pulse":
38
+ return {
39
+ ...base,
40
+ background: baseColor,
41
+ animation: `shimmer-pulse ${speed}s ease-in-out infinite`
42
+ };
43
+ case "glow":
44
+ return {
45
+ ...base,
46
+ background: baseColor,
47
+ animation: `shimmer-glow ${speed}s ease-in-out infinite`
48
+ };
49
+ case "gradient":
50
+ return {
51
+ ...base,
52
+ backgroundImage: `linear-gradient(90deg, ${baseColor}, ${highlightColor}, ${baseColor})`,
53
+ backgroundSize: "200% 100%",
54
+ animation: `shimmer-gradient ${speed * 1.5}s ease-in-out infinite`
55
+ };
56
+ }
57
+ }
58
+ var SweepLayer = ({ rect, highlightColor, speed, containerWidth, variant }) => {
59
+ const isShine = variant === "shine";
60
+ return /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ style: {
64
+ position: "absolute",
65
+ top: 0,
66
+ left: -rect.x,
67
+ width: containerWidth > 0 ? containerWidth : "100vw",
68
+ height: "100%",
69
+ background: isShine ? `linear-gradient(115deg, transparent 30%, ${highlightColor} 50%, transparent 70%)` : `linear-gradient(90deg, transparent 0%, ${highlightColor} 50%, transparent 100%)`,
70
+ animation: `${isShine ? "shimmer-shine" : "shimmer-wave"} ${speed}s ease-in-out infinite`
71
+ }
72
+ }
73
+ );
74
+ };
75
+ var ShimmerOverlay = ({
76
+ rects,
77
+ animation,
78
+ baseColor,
79
+ highlightColor,
80
+ speed
81
+ }) => {
82
+ const overlayRef = React2.useRef(null);
83
+ const [containerWidth, setContainerWidth] = React2.useState(0);
84
+ React2.useLayoutEffect(() => {
85
+ if (!overlayRef.current?.parentElement) return;
86
+ setContainerWidth(overlayRef.current.parentElement.offsetWidth);
87
+ }, [rects]);
88
+ if (rects.length === 0) return null;
89
+ const isSweep = animation === "wave" || animation === "shine";
90
+ return /* @__PURE__ */ jsx(
91
+ "div",
92
+ {
93
+ ref: overlayRef,
94
+ role: "status",
95
+ "aria-busy": "true",
96
+ "aria-label": "Loading content",
97
+ "data-shimmer-ignore": "true",
98
+ style: {
99
+ position: "absolute",
100
+ top: 0,
101
+ left: 0,
102
+ width: "100%",
103
+ height: "100%",
104
+ zIndex: 1,
105
+ pointerEvents: "none",
106
+ visibility: "visible"
107
+ },
108
+ children: rects.map((rect, i) => /* @__PURE__ */ jsx(
109
+ "div",
110
+ {
111
+ style: getBlockStyles(animation, baseColor, highlightColor, speed, rect),
112
+ children: isSweep && /* @__PURE__ */ jsx(
113
+ SweepLayer,
114
+ {
115
+ rect,
116
+ highlightColor,
117
+ speed,
118
+ containerWidth,
119
+ variant: animation
120
+ }
121
+ )
122
+ },
123
+ i
124
+ ))
125
+ }
126
+ );
127
+ };
128
+
129
+ // src/utils.ts
130
+ var counter = 0;
131
+ function generateShimmerKey(prefix = "shimmer") {
132
+ return `${prefix}-clone-${++counter}`;
133
+ }
134
+ var FALLBACK_DIMENSIONS = {
135
+ INPUT: { width: 200, height: 36 },
136
+ BUTTON: { width: 120, height: 36 },
137
+ TEXTAREA: { width: 300, height: 80 },
138
+ SELECT: { width: 200, height: 36 },
139
+ IMG: { width: 100, height: 100 },
140
+ H1: { width: 300, height: 36 },
141
+ H2: { width: 260, height: 30 },
142
+ H3: { width: 220, height: 26 },
143
+ H4: { width: 200, height: 22 },
144
+ H5: { width: 180, height: 20 },
145
+ H6: { width: 160, height: 18 },
146
+ P: { width: 250, height: 16 },
147
+ SPAN: { width: 100, height: 16 }
148
+ };
149
+
150
+ // src/useTrace.ts
151
+ var TRACEABLE_TAGS = /* @__PURE__ */ new Set([
152
+ // Text
153
+ "H1",
154
+ "H2",
155
+ "H3",
156
+ "H4",
157
+ "H5",
158
+ "H6",
159
+ "P",
160
+ "SPAN",
161
+ "A",
162
+ "LI",
163
+ "LABEL",
164
+ "TD",
165
+ "TH",
166
+ "BLOCKQUOTE",
167
+ "CODE",
168
+ "PRE",
169
+ // Media
170
+ "IMG",
171
+ "VIDEO",
172
+ "SVG",
173
+ "CANVAS",
174
+ "PICTURE",
175
+ // Form
176
+ "INPUT",
177
+ "TEXTAREA",
178
+ "SELECT",
179
+ "BUTTON",
180
+ // Misc
181
+ "HR"
182
+ ]);
183
+ function isTraceable(el) {
184
+ if (el.hasAttribute("data-shimmer-ignore")) return false;
185
+ if (el.hasAttribute("data-shimmer")) return true;
186
+ if (TRACEABLE_TAGS.has(el.tagName)) return true;
187
+ if (el.children.length === 0) {
188
+ const rect = el.getBoundingClientRect();
189
+ return rect.width > 0 && rect.height > 0;
190
+ }
191
+ return false;
192
+ }
193
+ function collectTraceableElements(root) {
194
+ const result = [];
195
+ function walk(el) {
196
+ if (el.hasAttribute("data-shimmer-ignore")) return;
197
+ if (el.hasAttribute("data-shimmer-reporter")) return;
198
+ if (isTraceable(el)) {
199
+ result.push(el);
200
+ return;
201
+ }
202
+ for (let i = 0; i < el.children.length; i++) {
203
+ walk(el.children[i]);
204
+ }
205
+ }
206
+ for (let i = 0; i < root.children.length; i++) {
207
+ walk(root.children[i]);
208
+ }
209
+ return result;
210
+ }
211
+ function measureElement(el, containerRect, globalBorderRadius) {
212
+ const elRect = el.getBoundingClientRect();
213
+ if (elRect.width === 0 && elRect.height === 0 && elRect.left === 0 && elRect.top === 0) {
214
+ return null;
215
+ }
216
+ const computedStyle = window.getComputedStyle(el);
217
+ let borderRadius = globalBorderRadius || computedStyle.borderRadius;
218
+ const isZero = !borderRadius || borderRadius === "none" || borderRadius.split(" ").every((v) => v === "0" || v === "0px" || v === "0%");
219
+ if (isZero) {
220
+ borderRadius = "4px";
221
+ }
222
+ let width = elRect.width;
223
+ let height = elRect.height;
224
+ if (width === 0 || height === 0) {
225
+ const fallback = FALLBACK_DIMENSIONS[el.tagName];
226
+ if (fallback) {
227
+ width = width || fallback.width;
228
+ height = height || fallback.height;
229
+ }
230
+ }
231
+ return {
232
+ x: elRect.left - containerRect.left,
233
+ y: elRect.top - containerRect.top,
234
+ width,
235
+ height,
236
+ borderRadius
237
+ };
238
+ }
239
+ function performTrace(container, globalBorderRadius, anchorRef) {
240
+ const anchor = anchorRef?.current ?? container;
241
+ const anchorRect = anchor.getBoundingClientRect();
242
+ const elements = collectTraceableElements(container);
243
+ return elements.map((el) => measureElement(el, anchorRect, globalBorderRadius)).filter((r) => r !== null && r.width > 0 && r.height > 0);
244
+ }
245
+ function useTrace(containerRef, loading, globalBorderRadius, anchorRef) {
246
+ const [rects, setRects] = useState([]);
247
+ const trace = useCallback(() => {
248
+ if (!containerRef.current) return;
249
+ const traced = performTrace(containerRef.current, globalBorderRadius, anchorRef);
250
+ setRects(traced);
251
+ }, [containerRef, globalBorderRadius, anchorRef]);
252
+ useLayoutEffect(() => {
253
+ if (!loading || !containerRef.current) {
254
+ setRects([]);
255
+ return;
256
+ }
257
+ trace();
258
+ const observer = new ResizeObserver(() => {
259
+ trace();
260
+ });
261
+ observer.observe(containerRef.current);
262
+ return () => observer.disconnect();
263
+ }, [loading, trace]);
264
+ return rects;
265
+ }
266
+
267
+ // src/styles.ts
268
+ var SHIMMER_STYLES_ID = "shimmer-trace-styles";
269
+ var CSS = `
270
+ @keyframes shimmer-wave {
271
+ 0% { transform: translateX(-100%); }
272
+ 100% { transform: translateX(100%); }
273
+ }
274
+
275
+ @keyframes shimmer-pulse {
276
+ 0%, 100% { opacity: 0.4; }
277
+ 50% { opacity: 1; }
278
+ }
279
+
280
+ @keyframes shimmer-shine {
281
+ 0% { transform: translateX(-150%) skewX(-20deg); }
282
+ 100% { transform: translateX(150%) skewX(-20deg); }
283
+ }
284
+
285
+ @keyframes shimmer-glow {
286
+ 0%, 100% { filter: brightness(1); }
287
+ 50% { filter: brightness(1.35); }
288
+ }
289
+
290
+ @keyframes shimmer-gradient {
291
+ 0% { background-position: 0% 50%; }
292
+ 50% { background-position: 100% 50%; }
293
+ 100% { background-position: 0% 50%; }
294
+ }
295
+
296
+ /* preserveBackground mode: hide text + media but keep container styles */
297
+ [data-shimmer-master][data-shimmer-preserve-bg="true"] :is(h1,h2,h3,h4,h5,h6,p,span,a,li,label,td,th,blockquote,code,pre,strong,em,small) {
298
+ color: transparent !important;
299
+ text-shadow: none !important;
300
+ }
301
+ [data-shimmer-master][data-shimmer-preserve-bg="true"] :is(img,video,svg,canvas,picture) {
302
+ opacity: 0 !important;
303
+ }
304
+ [data-shimmer-master][data-shimmer-preserve-bg="true"] :is(input,textarea,select,button) {
305
+ color: transparent !important;
306
+ opacity: 0 !important;
307
+ }
308
+ [data-shimmer-master][data-shimmer-preserve-bg="true"] {
309
+ pointer-events: none !important;
310
+ user-select: none !important;
311
+ }
312
+ `;
313
+ function injectStyles() {
314
+ if (typeof document === "undefined") return;
315
+ if (document.getElementById(SHIMMER_STYLES_ID)) return;
316
+ const style = document.createElement("style");
317
+ style.id = SHIMMER_STYLES_ID;
318
+ style.textContent = CSS;
319
+ document.head.appendChild(style);
320
+ }
321
+ function Shimmer({
322
+ loading = false,
323
+ children,
324
+ dummyLength,
325
+ dummyData,
326
+ as,
327
+ stopPropagation = false,
328
+ animation,
329
+ baseColor,
330
+ highlightColor,
331
+ speed,
332
+ borderRadius,
333
+ preserveBackground,
334
+ className,
335
+ style
336
+ }) {
337
+ const parentContext = useShimmerContext();
338
+ const isMaster = !parentContext || stopPropagation;
339
+ const id = useId();
340
+ const config = useMemo(
341
+ () => ({
342
+ animation: animation ?? parentContext?.config.animation ?? DEFAULTS.animation,
343
+ baseColor: baseColor ?? parentContext?.config.baseColor ?? DEFAULTS.baseColor,
344
+ highlightColor: highlightColor ?? parentContext?.config.highlightColor ?? DEFAULTS.highlightColor,
345
+ speed: speed ?? parentContext?.config.speed ?? DEFAULTS.speed,
346
+ borderRadius: borderRadius ?? parentContext?.config.borderRadius ?? DEFAULTS.borderRadius,
347
+ preserveBackground: preserveBackground ?? parentContext?.config.preserveBackground ?? DEFAULTS.preserveBackground
348
+ }),
349
+ [
350
+ animation,
351
+ baseColor,
352
+ highlightColor,
353
+ speed,
354
+ borderRadius,
355
+ preserveBackground,
356
+ parentContext?.config
357
+ ]
358
+ );
359
+ if (isMaster) {
360
+ return /* @__PURE__ */ jsx(
361
+ MasterShimmer,
362
+ {
363
+ id,
364
+ loading,
365
+ config,
366
+ dummyLength,
367
+ dummyData,
368
+ as,
369
+ className,
370
+ style,
371
+ children
372
+ }
373
+ );
374
+ }
375
+ return /* @__PURE__ */ jsx(
376
+ ReporterShimmer,
377
+ {
378
+ id,
379
+ parentContext,
380
+ config,
381
+ dummyLength,
382
+ dummyData,
383
+ as,
384
+ children
385
+ }
386
+ );
387
+ }
388
+ function MasterShimmer({
389
+ id,
390
+ loading,
391
+ config,
392
+ children,
393
+ dummyLength,
394
+ dummyData,
395
+ as,
396
+ className,
397
+ style
398
+ }) {
399
+ const containerRef = useRef(null);
400
+ const [reporterRects, setReporterRects] = useState({});
401
+ const register = useCallback((rid, rects) => {
402
+ setReporterRects((prev) => ({ ...prev, [rid]: rects }));
403
+ }, []);
404
+ const unregister = useCallback((rid) => {
405
+ setReporterRects((prev) => {
406
+ const next = { ...prev };
407
+ delete next[rid];
408
+ return next;
409
+ });
410
+ }, []);
411
+ React2.useEffect(() => {
412
+ injectStyles();
413
+ }, []);
414
+ const tracedRects = useTrace(
415
+ containerRef,
416
+ loading,
417
+ config.borderRadius || void 0
418
+ );
419
+ const allRects = useMemo(() => {
420
+ const reported = Object.values(reporterRects).flat();
421
+ return [...tracedRects, ...reported];
422
+ }, [tracedRects, reporterRects]);
423
+ const renderedChildren = useSkeletonChildren({
424
+ loading,
425
+ children,
426
+ dummyLength,
427
+ dummyData,
428
+ as,
429
+ id
430
+ });
431
+ const contextValue = useMemo(
432
+ () => ({
433
+ register,
434
+ unregister,
435
+ masterRef: containerRef,
436
+ loading,
437
+ config
438
+ }),
439
+ [register, unregister, loading, config]
440
+ );
441
+ return /* @__PURE__ */ jsx(ShimmerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
442
+ "div",
443
+ {
444
+ ref: containerRef,
445
+ className,
446
+ style: {
447
+ position: "relative",
448
+ visibility: loading && !config.preserveBackground ? "hidden" : void 0,
449
+ ...style
450
+ },
451
+ "aria-hidden": loading || void 0,
452
+ "data-shimmer-master": true,
453
+ "data-shimmer-preserve-bg": loading && config.preserveBackground ? "true" : void 0,
454
+ children: [
455
+ renderedChildren,
456
+ loading && /* @__PURE__ */ jsx(
457
+ ShimmerOverlay,
458
+ {
459
+ rects: allRects,
460
+ animation: config.animation,
461
+ baseColor: config.baseColor,
462
+ highlightColor: config.highlightColor,
463
+ speed: config.speed
464
+ }
465
+ )
466
+ ]
467
+ }
468
+ ) });
469
+ }
470
+ function ReporterShimmer({
471
+ id,
472
+ parentContext,
473
+ config,
474
+ children,
475
+ dummyLength,
476
+ dummyData,
477
+ as
478
+ }) {
479
+ const containerRef = useRef(null);
480
+ const tracedRects = useTrace(
481
+ containerRef,
482
+ parentContext.loading,
483
+ config.borderRadius || void 0,
484
+ parentContext.masterRef
485
+ );
486
+ React2.useLayoutEffect(() => {
487
+ if (!parentContext.loading || tracedRects.length === 0) {
488
+ parentContext.unregister(id);
489
+ return;
490
+ }
491
+ parentContext.register(id, tracedRects);
492
+ return () => {
493
+ parentContext.unregister(id);
494
+ };
495
+ }, [tracedRects, parentContext, id]);
496
+ const renderedChildren = useSkeletonChildren({
497
+ loading: parentContext.loading,
498
+ children,
499
+ dummyLength,
500
+ dummyData,
501
+ as,
502
+ id
503
+ });
504
+ return /* @__PURE__ */ jsx(
505
+ "div",
506
+ {
507
+ ref: containerRef,
508
+ "data-shimmer-reporter": true,
509
+ style: { display: "contents" },
510
+ children: renderedChildren
511
+ }
512
+ );
513
+ }
514
+ function useSkeletonChildren({
515
+ loading,
516
+ children,
517
+ dummyLength,
518
+ dummyData,
519
+ as,
520
+ id
521
+ }) {
522
+ if (!loading) return children;
523
+ if (as) {
524
+ const count = dummyLength && dummyLength > 0 ? dummyLength : 1;
525
+ const Component = as;
526
+ return Array.from({ length: count }, (_, i) => /* @__PURE__ */ createElement(
527
+ Component,
528
+ {
529
+ ...dummyData || {},
530
+ key: generateShimmerKey(`${id}-as-${i}`)
531
+ }
532
+ ));
533
+ }
534
+ const childArray = React2.Children.toArray(children);
535
+ const templated = childArray.map((c, i) => {
536
+ if (!React2.isValidElement(c)) return c;
537
+ const key = generateShimmerKey(`${id}-tpl-${i}`);
538
+ const props = dummyData ? { ...dummyData, key } : { key };
539
+ return React2.cloneElement(c, props);
540
+ });
541
+ if (dummyLength && dummyLength > 0) {
542
+ const first = templated.find((c) => React2.isValidElement(c));
543
+ if (!first) return null;
544
+ return Array.from(
545
+ { length: dummyLength },
546
+ (_, i) => React2.cloneElement(first, {
547
+ key: generateShimmerKey(`${id}-clone-${i}`)
548
+ })
549
+ );
550
+ }
551
+ return templated;
552
+ }
553
+ function createShimmer(config = {}) {
554
+ const mergedConfig = {
555
+ animation: config.animation ?? DEFAULTS.animation,
556
+ baseColor: config.baseColor ?? DEFAULTS.baseColor,
557
+ highlightColor: config.highlightColor ?? DEFAULTS.highlightColor,
558
+ speed: config.speed ?? DEFAULTS.speed,
559
+ borderRadius: config.borderRadius ?? DEFAULTS.borderRadius,
560
+ preserveBackground: config.preserveBackground ?? DEFAULTS.preserveBackground
561
+ };
562
+ function ConfiguredShimmer(props) {
563
+ return /* @__PURE__ */ jsx(
564
+ Shimmer,
565
+ {
566
+ ...mergedConfig,
567
+ ...props
568
+ }
569
+ );
570
+ }
571
+ return ConfiguredShimmer;
572
+ }
573
+ function ShimmerSuspense({
574
+ children,
575
+ template,
576
+ ...shimmerConfig
577
+ }) {
578
+ const skeletonContent = template !== void 0 ? template : /* @__PURE__ */ jsx(IsShimmeringContext.Provider, { value: true, children });
579
+ return /* @__PURE__ */ jsx(
580
+ React2.Suspense,
581
+ {
582
+ fallback: /* @__PURE__ */ jsx(Shimmer, { loading: true, ...shimmerConfig, children: skeletonContent }),
583
+ children
584
+ }
585
+ );
586
+ }
587
+
588
+ export { Shimmer, ShimmerContext, ShimmerSuspense, createShimmer, useIsShimmering, useShimmerContext };
589
+ //# sourceMappingURL=index.mjs.map
590
+ //# sourceMappingURL=index.mjs.map