smart-skeleton-loader 1.0.0 β†’ 2.1.1

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 CHANGED
@@ -1,12 +1,22 @@
1
- # react-auto-skeleton
1
+ # smart-skeleton-loader
2
2
 
3
3
  > **The smartest skeleton loader for React.**
4
4
  > Automatically generate perfectly matching skeletons by measuring your actual components.
5
5
 
6
- [![npm version](https://img.shields.io/npm/v/react-auto-skeleton.svg)](https://www.npmjs.com/package/react-auto-skeleton)
6
+ [![npm version](https://img.shields.io/npm/v/smart-skeleton-loader.svg)](https://www.npmjs.com/package/smart-skeleton-loader)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-Donate-orange.svg?logo=buy-me-a-coffee&logoColor=white)](https://buymeacoffee.com/dashnyamkkq)
8
9
 
9
- `react-auto-skeleton` eliminates the need to manually create and maintain skeleton versions of your components. It renders your actual component responsibly (hidden), measures its layout, and overlays precise skeletons for text, images, and containers.
10
+ <div align="center">
11
+
12
+ [![Buy Me A Coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/dashnyamkkq)
13
+
14
+ </div>
15
+
16
+ `smart-skeleton-loader` eliminates the need to manually create and maintain skeleton versions of your components. It renders your actual component responsibly (hidden), measures its layout, and overlays precise skeletons for text, images, and containers.
17
+
18
+ > [!IMPORTANT]
19
+ > **Enjoying this project?** If you find `smart-skeleton-loader` useful, please consider [buying me a coffee](https://buymeacoffee.com/dashnyamkkq) to support further development! πŸ’–
10
20
 
11
21
  ## ✨ Features
12
22
 
@@ -22,12 +32,12 @@
22
32
  ## πŸ“¦ Installation
23
33
 
24
34
  ```bash
25
- npm install react-auto-skeleton framer-motion
35
+ npm install smart-skeleton-loader
26
36
  # or
27
- yarn add react-auto-skeleton framer-motion
37
+ yarn add smart-skeleton-loader
28
38
  ```
29
39
 
30
- _Note: `framer-motion` is a peer dependency used for the smooth pulse animations._
40
+ _Zero runtime dependencies β€” only `react` (19+) as a peer._
31
41
 
32
42
  ## πŸš€ Usage
33
43
 
@@ -36,16 +46,16 @@ _Note: `framer-motion` is a peer dependency used for the smooth pulse animations
36
46
  Wrap your component in `AutoSkeleton` and control the `loading` state.
37
47
 
38
48
  ```tsx
39
- import { AutoSkeleton } from "react-auto-skeleton";
49
+ import { AutoSkeleton } from 'smart-skeleton-loader';
40
50
 
41
51
  const UserProfile = ({ loading, user }) => {
42
52
  return (
43
53
  <AutoSkeleton loading={loading}>
44
54
  <div className="card">
45
55
  {/* Use a placeholder fallback so the skeleton knows the size if data is null! */}
46
- <img src={user?.avatar || "/placeholder.png"} alt="Avatar" />
47
- <h3>{user?.name || "Loading Name..."}</h3>
48
- <p>{user?.bio || "This is a long line of text to simulate the bio."}</p>
56
+ <img src={user?.avatar || '/placeholder.png'} alt="Avatar" />
57
+ <h3>{user?.name || 'Loading Name...'}</h3>
58
+ <p>{user?.bio || 'This is a long line of text to simulate the bio.'}</p>
49
59
  <button>Follow</button>
50
60
  </div>
51
61
  </AutoSkeleton>
@@ -57,7 +67,7 @@ const UserProfile = ({ loading, user }) => {
57
67
 
58
68
  When fetching lists, you might start with an empty array. **How can we measure an empty array?**
59
69
 
60
- The solution is the **Template Pattern**: Render "dummy" items while loading. `react-auto-skeleton` will measure these dummies and create the skeleton layout.
70
+ The solution is the **Template Pattern**: Render "dummy" items while loading. `smart-skeleton-loader` will measure these dummies and create the skeleton layout.
61
71
 
62
72
  ```tsx
63
73
  const UserList = () => {
@@ -85,7 +95,7 @@ const UserList = () => {
85
95
 
86
96
  ### 3. Container Detection (Cards)
87
97
 
88
- `react-auto-skeleton` intelligently distinguishes between **Content** and **Containers**.
98
+ `smart-skeleton-loader` intelligently distinguishes between **Content** and **Containers**.
89
99
 
90
100
  - **Content** (Text, Images, Buttons) -> Renders as a **Pulsing Gray Box**.
91
101
  - **Containers** (Elements with visible `border`, `background-color`, or `box-shadow`) -> Renders as a **Static Clone** (with your exact styles) and **NO** pulse.
@@ -100,9 +110,9 @@ Customize the appearance via the `config` prop:
100
110
  <AutoSkeleton
101
111
  loading={true}
102
112
  config={{
103
- color: "#e0e0e0", // Base color of the skeleton pulse
104
- highlightColor: "#f5f5f5", // Shine color
105
- borderRadius: "8px", // Global border radius (auto-detects if not set)
113
+ color: '#e0e0e0', // Base color of the skeleton pulse
114
+ highlightColor: '#f5f5f5', // Shine color
115
+ borderRadius: '8px', // Global border radius (auto-detects if not set)
106
116
  }}
107
117
  >
108
118
  <MyComponent />
@@ -111,11 +121,16 @@ Customize the appearance via the `config` prop:
111
121
 
112
122
  ## 🧩 API Reference
113
123
 
114
- | Prop | Type | Default | Description |
115
- | ---------- | ----------- | ------- | --------------------------------------------------- |
116
- | `loading` | `boolean` | `true` | Whether to show the skeleton or the actual content. |
117
- | `children` | `ReactNode` | - | The content to measure. |
118
- | `config` | `object` | - | Customization options. |
124
+ | Prop | Type | Default | Description |
125
+ | -------------- | --------------- | ------------ | ------------------------------------------------------------------------------ |
126
+ | `loading` | `boolean` | `true` | Whether to show the skeleton or the actual content. |
127
+ | `children` | `ReactNode` | - | The content to measure. |
128
+ | `config` | `object` | - | Customization options. |
129
+ | `loadingLabel` | `string` | `"Loading…"` | Text announced to screen readers while loading. |
130
+ | `className` | `string` | - | Class name for the outer wrapper. |
131
+ | `style` | `CSSProperties` | - | Style for the outer wrapper, merged over `position: relative; display: block`. |
132
+ | `delay` | `number` | `0` | Wait this long (ms) before showing the skeleton, so fast loads never flash it. |
133
+ | `minDuration` | `number` | `0` | Once shown, keep the skeleton at least this long (ms). |
119
134
 
120
135
  ### Config Object
121
136
 
@@ -129,13 +144,18 @@ Customize the appearance via the `config` prop:
129
144
 
130
145
  1. **Invisible Measurement**: When `loading={true}`, your children are rendered in the DOM but hidden using `opacity: 0`. This allows them to take up space and be measured by the browser.
131
146
  2. **Smart Scan**: We use a `TreeWalker` to scan the DOM elements.
132
- - If it finds text/images -> It creates a **Primitive Skeleton**.
147
+ - If it finds text -> It creates **one bar per rendered line** (the last bar is as short as the last line).
148
+ - If it finds images/inputs/buttons -> It creates a **Primitive Skeleton**.
133
149
  - If it finds a styled container -> It creates a **Container Skeleton**.
134
- 3. **Overlay**: We render absolute positioned `divs` (using Framer Motion) exactly over the measured elements.
135
- 4. **No Shift**: Because the hidden content is already holding the space, when `loading` becomes `false`, we simply fade `opacity` to 1. No jumping content!
150
+ 3. **Overlay**: We render absolute positioned `divs` exactly over the measured elements.
151
+ 4. **No Shift**: Because the hidden content is already holding the space, when `loading` becomes `false`, the skeleton and content crossfade. No jumping content!
136
152
 
137
153
  ## ⚠️ Notes
138
154
 
155
+ - **Accessibility:** while `loading`, the real content is `inert` (no clicks, no Tab focus, hidden from assistive tech), the wrapper has `aria-busy`, and `loadingLabel` is announced through a `role="status"` region.
156
+ - **Avoiding flicker:** for most data fetching use `delay={150} minDuration={400}`: loads faster than 150ms show nothing, and a skeleton that does appear stays long enough to read as intentional. The skeleton and content crossfade over 200ms.
157
+ - **Server rendering / Next.js:** the package is marked `'use client'`, so it can be imported from Server Components. Server-rendered HTML shows one pulsing block over the reserved space until hydration, then switches to measured skeletons.
158
+ - **Layout:** the wrapper is `display: block`, so it fills its parent like the content it replaces. For inline content pass `style={{ display: 'inline-block' }}`.
139
159
  - **Placeholders**: For images, ensure you provide a `src` (even a placeholder) or explicit `width/height` style so the image takes up space to be measured.
140
160
  - **Opacity vs Visibility**: We use `opacity: 0` to hide content. This ensures that even elements that might behave strangely with `visibility: hidden` are correctly measured.
141
161
 
package/dist/index.cjs CHANGED
@@ -1,75 +1,208 @@
1
- 'use strict';
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
2
20
 
3
- var react = require('react');
4
- var framerMotion = require('framer-motion');
5
- var jsxRuntime = require('react/jsx-runtime');
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ AutoSkeleton: () => AutoSkeleton,
25
+ SkeletonPrimitive: () => SkeletonPrimitive
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
6
28
 
7
29
  // src/AutoSkeleton.tsx
30
+ var import_react2 = require("react");
31
+
32
+ // src/measure.ts
33
+ function groupRectsIntoLines(rects, tolerance = 2) {
34
+ const sorted = Array.from(rects).filter((r) => r.width > 0 && r.height > 0).sort((a, b) => a.top - b.top || a.left - b.left);
35
+ const lines = [];
36
+ for (const r of sorted) {
37
+ const line = lines[lines.length - 1];
38
+ if (line && Math.abs(r.top - line.top) <= tolerance) {
39
+ line.left = Math.min(line.left, r.left);
40
+ line.right = Math.max(line.right, r.left + r.width);
41
+ line.bottom = Math.max(line.bottom, r.top + r.height);
42
+ } else {
43
+ lines.push({
44
+ top: r.top,
45
+ left: r.left,
46
+ right: r.left + r.width,
47
+ bottom: r.top + r.height
48
+ });
49
+ }
50
+ }
51
+ return lines.map((l) => ({
52
+ top: l.top,
53
+ left: l.left,
54
+ width: l.right - l.left,
55
+ height: l.bottom - l.top
56
+ }));
57
+ }
58
+ function getTextLineBoxes(el) {
59
+ if (typeof document.createRange !== "function") return [];
60
+ const rects = [];
61
+ for (const node of Array.from(el.childNodes)) {
62
+ if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) continue;
63
+ const range = document.createRange();
64
+ range.selectNodeContents(node);
65
+ if (typeof range.getClientRects !== "function") return [];
66
+ rects.push(...Array.from(range.getClientRects()));
67
+ }
68
+ return groupRectsIntoLines(rects);
69
+ }
70
+
71
+ // src/SkeletonPrimitive.tsx
72
+ var import_jsx_runtime = require("react/jsx-runtime");
73
+ var PULSE_CSS = `
74
+ @keyframes auto-skeleton-pulse {
75
+ 0%, 100% { background-color: var(--auto-skeleton-c); }
76
+ 50% { background-color: var(--auto-skeleton-h); }
77
+ }
78
+ .auto-skeleton { animation: auto-skeleton-pulse 1.5s ease-in-out infinite; }
79
+ @media (prefers-reduced-motion: reduce) {
80
+ .auto-skeleton { animation: none; }
81
+ }
82
+ `;
8
83
  var SkeletonPrimitive = ({
9
84
  width,
10
85
  height,
11
86
  borderRadius = "4px",
12
87
  color = "var(--auto-skeleton-color, #eee)",
13
88
  highlightColor = "var(--auto-skeleton-highlight, #f5f5f5)",
89
+ className,
14
90
  style,
15
91
  ...props
16
92
  }) => {
17
- return /* @__PURE__ */ jsxRuntime.jsx(
18
- framerMotion.motion.div,
19
- {
20
- style: {
21
- width,
22
- height,
23
- borderRadius,
24
- backgroundColor: color,
25
- ...style
26
- },
27
- animate: {
28
- backgroundColor: [color, highlightColor, color]
29
- },
30
- transition: {
31
- duration: 1.5,
32
- repeat: Infinity,
33
- ease: "easeInOut"
34
- },
35
- "data-testid": "skeleton",
36
- ...props
93
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { href: "auto-skeleton-pulse", precedence: "default", children: PULSE_CSS }),
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
96
+ "div",
97
+ {
98
+ className: className ? `auto-skeleton ${className}` : "auto-skeleton",
99
+ style: {
100
+ width,
101
+ height,
102
+ borderRadius,
103
+ backgroundColor: color,
104
+ "--auto-skeleton-c": color,
105
+ "--auto-skeleton-h": highlightColor,
106
+ ...style
107
+ },
108
+ "data-testid": "skeleton",
109
+ ...props
110
+ }
111
+ )
112
+ ] });
113
+ };
114
+
115
+ // src/useDisplayedLoading.ts
116
+ var import_react = require("react");
117
+ function useDisplayedLoading(loading, { delay = 0, minDuration = 0 } = {}) {
118
+ const [shown, setShown] = (0, import_react.useState)(() => loading && delay <= 0);
119
+ const shownAt = (0, import_react.useRef)(0);
120
+ (0, import_react.useLayoutEffect)(() => {
121
+ if (shown) shownAt.current = Date.now();
122
+ }, [shown]);
123
+ (0, import_react.useLayoutEffect)(() => {
124
+ if (loading === shown) return;
125
+ const wait = loading ? delay : Math.max(0, minDuration - (Date.now() - shownAt.current));
126
+ if (wait <= 0) {
127
+ setShown(loading);
128
+ return;
37
129
  }
38
- );
130
+ const timer = setTimeout(() => setShown(loading), wait);
131
+ return () => clearTimeout(timer);
132
+ }, [loading, shown, delay, minDuration]);
133
+ return shown;
134
+ }
135
+
136
+ // src/AutoSkeleton.tsx
137
+ var import_jsx_runtime2 = require("react/jsx-runtime");
138
+ var TEXT_BAR_RATIO = 0.7;
139
+ var FADE_MS = 200;
140
+ var VISUALLY_HIDDEN = {
141
+ position: "absolute",
142
+ width: 1,
143
+ height: 1,
144
+ padding: 0,
145
+ margin: -1,
146
+ overflow: "hidden",
147
+ clip: "rect(0, 0, 0, 0)",
148
+ whiteSpace: "nowrap",
149
+ border: 0
39
150
  };
40
151
  var AutoSkeleton = ({
41
152
  loading,
42
153
  children,
154
+ loadingLabel = "Loading\u2026",
155
+ className,
156
+ style,
157
+ delay = 0,
158
+ minDuration = 0,
43
159
  config
44
160
  }) => {
45
161
  const theme = config?.theme || "auto";
46
- const containerRef = react.useRef(null);
47
- const [skeletons, setSkeletons] = react.useState([]);
162
+ const showSkeleton = useDisplayedLoading(loading, { delay, minDuration });
163
+ const busy = loading || showSkeleton;
164
+ const [revealed, setRevealed] = (0, import_react2.useState)(!busy);
165
+ if (!busy && !revealed) setRevealed(true);
166
+ const hidden = showSkeleton || loading && !revealed;
167
+ const [fadingOut, setFadingOut] = (0, import_react2.useState)(false);
168
+ const [prevShowSkeleton, setPrevShowSkeleton] = (0, import_react2.useState)(showSkeleton);
169
+ if (prevShowSkeleton !== showSkeleton) {
170
+ setPrevShowSkeleton(showSkeleton);
171
+ setFadingOut(!showSkeleton);
172
+ }
173
+ (0, import_react2.useEffect)(() => {
174
+ if (!fadingOut) return;
175
+ const timer = setTimeout(() => setFadingOut(false), FADE_MS);
176
+ return () => clearTimeout(timer);
177
+ }, [fadingOut]);
178
+ const containerRef = (0, import_react2.useRef)(null);
179
+ const [skeletons, setSkeletons] = (0, import_react2.useState)([]);
180
+ const [measured, setMeasured] = (0, import_react2.useState)(false);
48
181
  const updateSkeletons = () => {
49
182
  if (!containerRef.current) return;
50
183
  const containerRect = containerRef.current.getBoundingClientRect();
51
184
  const newSkeletons = [];
52
185
  const getSkeletonType = (el) => {
53
186
  const tagName = el.tagName.toLowerCase();
54
- const style = window.getComputedStyle(el);
187
+ const style2 = window.getComputedStyle(el);
55
188
  if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
56
189
  return { type: "primitive" };
57
190
  }
58
191
  const hasText = Array.from(el.childNodes).some(
59
192
  (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
60
193
  );
61
- if (hasText) return { type: "primitive" };
62
- const hasBackground = style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent";
63
- const hasBorder = style.borderWidth !== "0px" && style.borderStyle !== "none" && style.borderColor !== "rgba(0, 0, 0, 0)" && style.borderColor !== "transparent";
64
- const hasShadow = style.boxShadow !== "none";
194
+ if (hasText) return { type: "primitive", text: true };
195
+ const hasBackground = style2.backgroundColor !== "rgba(0, 0, 0, 0)" && style2.backgroundColor !== "transparent";
196
+ const hasBorder = style2.borderWidth !== "0px" && style2.borderStyle !== "none" && style2.borderColor !== "rgba(0, 0, 0, 0)" && style2.borderColor !== "transparent";
197
+ const hasShadow = style2.boxShadow !== "none";
65
198
  if (hasBackground || hasBorder || hasShadow) {
66
199
  return {
67
200
  type: "container",
68
201
  styles: {
69
- backgroundColor: style.backgroundColor,
70
- border: style.border,
71
- borderRadius: style.borderRadius,
72
- boxShadow: style.boxShadow
202
+ backgroundColor: style2.backgroundColor,
203
+ border: style2.border,
204
+ borderRadius: style2.borderRadius,
205
+ boxShadow: style2.boxShadow
73
206
  }
74
207
  };
75
208
  }
@@ -81,8 +214,8 @@ var AutoSkeleton = ({
81
214
  {
82
215
  acceptNode: (node2) => {
83
216
  const el = node2;
84
- const style = window.getComputedStyle(el);
85
- if (style.display === "none") return NodeFilter.FILTER_REJECT;
217
+ const style2 = window.getComputedStyle(el);
218
+ if (style2.display === "none") return NodeFilter.FILTER_REJECT;
86
219
  if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
87
220
  return NodeFilter.FILTER_SKIP;
88
221
  }
@@ -94,11 +227,26 @@ var AutoSkeleton = ({
94
227
  const el = node;
95
228
  const rect = el.getBoundingClientRect();
96
229
  const typeData = getSkeletonType(el);
97
- if (typeData) {
230
+ const lines = typeData?.text ? getTextLineBoxes(el) : [];
231
+ if (lines.length > 0) {
232
+ for (const line of lines) {
233
+ const height = Math.max(4, line.height * TEXT_BAR_RATIO);
234
+ newSkeletons.push({
235
+ key: `${idx++}`,
236
+ type: "primitive",
237
+ width: line.width,
238
+ height,
239
+ top: line.top + (line.height - height) / 2 - containerRect.top,
240
+ left: line.left - containerRect.left,
241
+ borderRadius: typeof config?.borderRadius === "number" ? `${config.borderRadius}px` : config?.borderRadius ?? "4px"
242
+ });
243
+ }
244
+ } else if (typeData) {
98
245
  let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
99
246
  if (typeData.type === "primitive") {
100
247
  if (borderRadius === "0px" || !borderRadius) {
101
- if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
248
+ if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) {
249
+ }
102
250
  borderRadius = "4px";
103
251
  if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
104
252
  if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
@@ -129,8 +277,9 @@ var AutoSkeleton = ({
129
277
  });
130
278
  }
131
279
  setSkeletons(newSkeletons);
280
+ setMeasured(true);
132
281
  };
133
- react.useEffect(() => {
282
+ (0, import_react2.useLayoutEffect)(() => {
134
283
  if (!containerRef.current) return;
135
284
  updateSkeletons();
136
285
  const observer = new ResizeObserver(() => {
@@ -147,7 +296,7 @@ var AutoSkeleton = ({
147
296
  observer.disconnect();
148
297
  mutationObserver.disconnect();
149
298
  };
150
- }, [loading, children]);
299
+ }, [showSkeleton, children]);
151
300
  const getEffectiveColors = () => {
152
301
  if (config?.color || config?.highlightColor) {
153
302
  return {
@@ -170,78 +319,99 @@ var AutoSkeleton = ({
170
319
  return { color: void 0, highlightColor: void 0 };
171
320
  };
172
321
  const effectiveColors = getEffectiveColors();
173
- return /* @__PURE__ */ jsxRuntime.jsxs(
322
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
174
323
  "div",
175
324
  {
176
- style: { position: "relative", display: "inline-block", width: "auto" },
325
+ className,
326
+ style: { position: "relative", display: "block", ...style },
177
327
  ...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
328
+ "aria-busy": busy,
178
329
  children: [
179
- /* @__PURE__ */ jsxRuntime.jsx(
330
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { role: "status", style: VISUALLY_HIDDEN, children: busy ? loadingLabel : "" }),
331
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
180
332
  "div",
181
333
  {
182
334
  ref: containerRef,
183
335
  style: {
184
- opacity: loading ? 0 : 1,
185
- transition: "opacity 0.2s ease"
186
- // Smooth transition when loading finishes
336
+ opacity: hidden ? 0 : 1,
337
+ // Hide instantly (no ghost content under the bars); fade only on reveal
338
+ transition: hidden ? "none" : `opacity ${FADE_MS}ms ease`
187
339
  },
188
- "aria-hidden": loading,
340
+ "aria-hidden": hidden,
341
+ inert: busy,
189
342
  children
190
343
  }
191
344
  ),
192
- loading && /* @__PURE__ */ jsxRuntime.jsx(
345
+ (showSkeleton || fadingOut) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
193
346
  "div",
194
347
  {
348
+ "data-testid": "skeleton-overlay",
195
349
  style: {
196
350
  position: "absolute",
197
351
  top: 0,
198
352
  left: 0,
199
353
  width: "100%",
200
354
  height: "100%",
201
- pointerEvents: "none"
355
+ pointerEvents: "none",
356
+ opacity: showSkeleton ? 1 : 0,
357
+ transition: `opacity ${FADE_MS}ms ease`
202
358
  },
203
- children: skeletons.map((sk) => /* @__PURE__ */ jsxRuntime.jsx(
204
- "div",
205
- {
206
- style: { position: "absolute", top: sk.top, left: sk.left },
207
- children: sk.type === "primitive" ? /* @__PURE__ */ jsxRuntime.jsx(
208
- SkeletonPrimitive,
209
- {
210
- width: sk.width,
211
- height: sk.height,
212
- borderRadius: sk.borderRadius,
213
- color: effectiveColors.color,
214
- highlightColor: effectiveColors.highlightColor
215
- }
216
- ) : (
217
- /* Container Clone: Renders static style */
218
- /* @__PURE__ */ jsxRuntime.jsx(
219
- "div",
359
+ children: [
360
+ !measured && // Server render / before hydration: nothing can be measured yet, so
361
+ // cover the reserved space with one block instead of showing nothing
362
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
363
+ SkeletonPrimitive,
364
+ {
365
+ width: "100%",
366
+ height: "100%",
367
+ color: effectiveColors.color,
368
+ highlightColor: effectiveColors.highlightColor
369
+ }
370
+ ),
371
+ skeletons.map((sk) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
372
+ "div",
373
+ {
374
+ style: { position: "absolute", top: sk.top, left: sk.left },
375
+ children: sk.type === "primitive" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
376
+ SkeletonPrimitive,
220
377
  {
221
- style: {
222
- width: sk.width,
223
- height: sk.height,
224
- borderRadius: sk.borderRadius,
225
- ...sk.style,
226
- // Apply captured container styles
227
- // Ensure it doesn't hide children if z-index issues arise,
228
- // but since we render in DOM order, children come later and sit on top.
229
- boxSizing: "border-box"
230
- }
378
+ width: sk.width,
379
+ height: sk.height,
380
+ borderRadius: sk.borderRadius,
381
+ color: effectiveColors.color,
382
+ highlightColor: effectiveColors.highlightColor
231
383
  }
384
+ ) : (
385
+ /* Container Clone: Renders static style */
386
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
387
+ "div",
388
+ {
389
+ style: {
390
+ width: sk.width,
391
+ height: sk.height,
392
+ borderRadius: sk.borderRadius,
393
+ ...sk.style,
394
+ // Apply captured container styles
395
+ // Ensure it doesn't hide children if z-index issues arise,
396
+ // but since we render in DOM order, children come later and sit on top.
397
+ boxSizing: "border-box"
398
+ }
399
+ }
400
+ )
232
401
  )
233
- )
234
- },
235
- sk.key
236
- ))
402
+ },
403
+ sk.key
404
+ ))
405
+ ]
237
406
  }
238
407
  )
239
408
  ]
240
409
  }
241
410
  );
242
411
  };
243
-
244
- exports.AutoSkeleton = AutoSkeleton;
245
- exports.SkeletonPrimitive = SkeletonPrimitive;
246
- //# sourceMappingURL=index.cjs.map
412
+ // Annotate the CommonJS export names for ESM import in node:
413
+ 0 && (module.exports = {
414
+ AutoSkeleton,
415
+ SkeletonPrimitive
416
+ });
247
417
  //# sourceMappingURL=index.cjs.map