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