smart-skeleton-loader 1.0.1 β†’ 2.1.2

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,9 +1,9 @@
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
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)
9
9
 
@@ -13,10 +13,10 @@
13
13
 
14
14
  </div>
15
15
 
16
- `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.
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
17
 
18
18
  > [!IMPORTANT]
19
- > **Enjoying this project?** If you find `react-auto-skeleton` useful, please consider [buying me a coffee](https://buymeacoffee.com/dashnyamkkq) to support further development! πŸ’–
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! πŸ’–
20
20
 
21
21
  ## ✨ Features
22
22
 
@@ -32,12 +32,12 @@
32
32
  ## πŸ“¦ Installation
33
33
 
34
34
  ```bash
35
- npm install react-auto-skeleton framer-motion
35
+ npm install smart-skeleton-loader
36
36
  # or
37
- yarn add react-auto-skeleton framer-motion
37
+ yarn add smart-skeleton-loader
38
38
  ```
39
39
 
40
- _Note: `framer-motion` is a peer dependency used for the smooth pulse animations._
40
+ _Zero runtime dependencies β€” only `react` (19+) as a peer._
41
41
 
42
42
  ## πŸš€ Usage
43
43
 
@@ -46,7 +46,7 @@ _Note: `framer-motion` is a peer dependency used for the smooth pulse animations
46
46
  Wrap your component in `AutoSkeleton` and control the `loading` state.
47
47
 
48
48
  ```tsx
49
- import { AutoSkeleton } from 'react-auto-skeleton';
49
+ import { AutoSkeleton } from 'smart-skeleton-loader';
50
50
 
51
51
  const UserProfile = ({ loading, user }) => {
52
52
  return (
@@ -67,7 +67,7 @@ const UserProfile = ({ loading, user }) => {
67
67
 
68
68
  When fetching lists, you might start with an empty array. **How can we measure an empty array?**
69
69
 
70
- 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.
71
71
 
72
72
  ```tsx
73
73
  const UserList = () => {
@@ -95,7 +95,7 @@ const UserList = () => {
95
95
 
96
96
  ### 3. Container Detection (Cards)
97
97
 
98
- `react-auto-skeleton` intelligently distinguishes between **Content** and **Containers**.
98
+ `smart-skeleton-loader` intelligently distinguishes between **Content** and **Containers**.
99
99
 
100
100
  - **Content** (Text, Images, Buttons) -> Renders as a **Pulsing Gray Box**.
101
101
  - **Containers** (Elements with visible `border`, `background-color`, or `box-shadow`) -> Renders as a **Static Clone** (with your exact styles) and **NO** pulse.
@@ -121,31 +121,44 @@ Customize the appearance via the `config` prop:
121
121
 
122
122
  ## 🧩 API Reference
123
123
 
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. |
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). |
129
134
 
130
135
  ### Config Object
131
136
 
132
- | Key | Type | Default | Description |
133
- | ---------------- | -------- | --------- | ---------------------------------------------------- |
134
- | `color` | `string` | `#eee` | Background color of primitives. |
135
- | `highlightColor` | `string` | `#f5f5f5` | Animation highlight color. |
136
- | `borderRadius` | `string` | `4px` | Fallback radius (Auto-detection is attempted first). |
137
+ | Key | Type | Default | Description |
138
+ | ------------------------------------ | ----------------------------- | --------- | ------------------------------------------------------------------------------------------------- |
139
+ | `color` | `string` | `#eee` | Background color of primitives. |
140
+ | `highlightColor` | `string` | `#f5f5f5` | Animation highlight color. |
141
+ | `borderRadius` | `string` | `4px` | Fallback radius (Auto-detection is attempted first). |
142
+ | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Force a theme, or follow the OS color scheme. |
143
+ | `lightColor` / `lightHighlightColor` | `string` | - | Colors used when the (resolved) theme is light. |
144
+ | `darkColor` / `darkHighlightColor` | `string` | - | Colors used when the (resolved) theme is dark. With `'auto'` they switch live with the OS scheme. |
137
145
 
138
146
  ## πŸ›  How It Works
139
147
 
140
148
  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.
141
149
  2. **Smart Scan**: We use a `TreeWalker` to scan the DOM elements.
142
- - If it finds text/images -> It creates a **Primitive Skeleton**.
150
+ - If it finds text -> It creates **one bar per rendered line** (the last bar is as short as the last line).
151
+ - If it finds images/inputs/buttons -> It creates a **Primitive Skeleton**.
143
152
  - If it finds a styled container -> It creates a **Container Skeleton**.
144
- 3. **Overlay**: We render absolute positioned `divs` (using Framer Motion) exactly over the measured elements.
145
- 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!
153
+ 3. **Overlay**: We render absolute positioned `divs` exactly over the measured elements.
154
+ 4. **No Shift**: Because the hidden content is already holding the space, when `loading` becomes `false`, the skeleton and content crossfade. No jumping content!
146
155
 
147
156
  ## ⚠️ Notes
148
157
 
158
+ - **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.
159
+ - **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.
160
+ - **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.
161
+ - **Layout:** the wrapper is `display: block`, so it fills its parent like the content it replaces. For inline content pass `style={{ display: 'inline-block' }}`.
149
162
  - **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.
150
163
  - **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.
151
164
 
package/dist/index.cjs CHANGED
@@ -1,75 +1,227 @@
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_react3 = 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
  }
130
+ const timer = setTimeout(() => setShown(loading), wait);
131
+ return () => clearTimeout(timer);
132
+ }, [loading, shown, delay, minDuration]);
133
+ return shown;
134
+ }
135
+
136
+ // src/usePrefersDark.ts
137
+ var import_react2 = require("react");
138
+ var QUERY = "(prefers-color-scheme: dark)";
139
+ var getMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null;
140
+ var subscribe = (onChange) => {
141
+ const media = getMedia();
142
+ media?.addEventListener("change", onChange);
143
+ return () => media?.removeEventListener("change", onChange);
144
+ };
145
+ function usePrefersDark() {
146
+ return (0, import_react2.useSyncExternalStore)(
147
+ subscribe,
148
+ () => getMedia()?.matches ?? false,
149
+ () => false
38
150
  );
151
+ }
152
+
153
+ // src/AutoSkeleton.tsx
154
+ var import_jsx_runtime2 = require("react/jsx-runtime");
155
+ var TEXT_BAR_RATIO = 0.7;
156
+ var FADE_MS = 200;
157
+ var VISUALLY_HIDDEN = {
158
+ position: "absolute",
159
+ width: 1,
160
+ height: 1,
161
+ padding: 0,
162
+ margin: -1,
163
+ overflow: "hidden",
164
+ clip: "rect(0, 0, 0, 0)",
165
+ whiteSpace: "nowrap",
166
+ border: 0
39
167
  };
40
168
  var AutoSkeleton = ({
41
169
  loading,
42
170
  children,
171
+ loadingLabel = "Loading\u2026",
172
+ className,
173
+ style,
174
+ delay = 0,
175
+ minDuration = 0,
43
176
  config
44
177
  }) => {
45
178
  const theme = config?.theme || "auto";
46
- const containerRef = react.useRef(null);
47
- const [skeletons, setSkeletons] = react.useState([]);
179
+ const prefersDark = usePrefersDark();
180
+ const resolvedTheme = theme === "auto" ? prefersDark ? "dark" : "light" : theme;
181
+ const showSkeleton = useDisplayedLoading(loading, { delay, minDuration });
182
+ const busy = loading || showSkeleton;
183
+ const [revealed, setRevealed] = (0, import_react3.useState)(!busy);
184
+ if (!busy && !revealed) setRevealed(true);
185
+ const hidden = showSkeleton || loading && !revealed;
186
+ const [fadingOut, setFadingOut] = (0, import_react3.useState)(false);
187
+ const [prevShowSkeleton, setPrevShowSkeleton] = (0, import_react3.useState)(showSkeleton);
188
+ if (prevShowSkeleton !== showSkeleton) {
189
+ setPrevShowSkeleton(showSkeleton);
190
+ setFadingOut(!showSkeleton);
191
+ }
192
+ (0, import_react3.useEffect)(() => {
193
+ if (!fadingOut) return;
194
+ const timer = setTimeout(() => setFadingOut(false), FADE_MS);
195
+ return () => clearTimeout(timer);
196
+ }, [fadingOut]);
197
+ const containerRef = (0, import_react3.useRef)(null);
198
+ const [skeletons, setSkeletons] = (0, import_react3.useState)([]);
199
+ const [measured, setMeasured] = (0, import_react3.useState)(false);
48
200
  const updateSkeletons = () => {
49
201
  if (!containerRef.current) return;
50
202
  const containerRect = containerRef.current.getBoundingClientRect();
51
203
  const newSkeletons = [];
52
204
  const getSkeletonType = (el) => {
53
205
  const tagName = el.tagName.toLowerCase();
54
- const style = window.getComputedStyle(el);
206
+ const style2 = window.getComputedStyle(el);
55
207
  if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
56
208
  return { type: "primitive" };
57
209
  }
58
210
  const hasText = Array.from(el.childNodes).some(
59
211
  (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
60
212
  );
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";
213
+ if (hasText) return { type: "primitive", text: true };
214
+ const hasBackground = style2.backgroundColor !== "rgba(0, 0, 0, 0)" && style2.backgroundColor !== "transparent";
215
+ const hasBorder = style2.borderWidth !== "0px" && style2.borderStyle !== "none" && style2.borderColor !== "rgba(0, 0, 0, 0)" && style2.borderColor !== "transparent";
216
+ const hasShadow = style2.boxShadow !== "none";
65
217
  if (hasBackground || hasBorder || hasShadow) {
66
218
  return {
67
219
  type: "container",
68
220
  styles: {
69
- backgroundColor: style.backgroundColor,
70
- border: style.border,
71
- borderRadius: style.borderRadius,
72
- boxShadow: style.boxShadow
221
+ backgroundColor: style2.backgroundColor,
222
+ border: style2.border,
223
+ borderRadius: style2.borderRadius,
224
+ boxShadow: style2.boxShadow
73
225
  }
74
226
  };
75
227
  }
@@ -81,8 +233,8 @@ var AutoSkeleton = ({
81
233
  {
82
234
  acceptNode: (node2) => {
83
235
  const el = node2;
84
- const style = window.getComputedStyle(el);
85
- if (style.display === "none") return NodeFilter.FILTER_REJECT;
236
+ const style2 = window.getComputedStyle(el);
237
+ if (style2.display === "none") return NodeFilter.FILTER_REJECT;
86
238
  if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
87
239
  return NodeFilter.FILTER_SKIP;
88
240
  }
@@ -94,11 +246,26 @@ var AutoSkeleton = ({
94
246
  const el = node;
95
247
  const rect = el.getBoundingClientRect();
96
248
  const typeData = getSkeletonType(el);
97
- if (typeData) {
249
+ const lines = typeData?.text ? getTextLineBoxes(el) : [];
250
+ if (lines.length > 0) {
251
+ for (const line of lines) {
252
+ const height = Math.max(4, line.height * TEXT_BAR_RATIO);
253
+ newSkeletons.push({
254
+ key: `${idx++}`,
255
+ type: "primitive",
256
+ width: line.width,
257
+ height,
258
+ top: line.top + (line.height - height) / 2 - containerRect.top,
259
+ left: line.left - containerRect.left,
260
+ borderRadius: typeof config?.borderRadius === "number" ? `${config.borderRadius}px` : config?.borderRadius ?? "4px"
261
+ });
262
+ }
263
+ } else if (typeData) {
98
264
  let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
99
265
  if (typeData.type === "primitive") {
100
266
  if (borderRadius === "0px" || !borderRadius) {
101
- if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
267
+ if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) {
268
+ }
102
269
  borderRadius = "4px";
103
270
  if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
104
271
  if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
@@ -129,8 +296,9 @@ var AutoSkeleton = ({
129
296
  });
130
297
  }
131
298
  setSkeletons(newSkeletons);
299
+ setMeasured(true);
132
300
  };
133
- react.useEffect(() => {
301
+ (0, import_react3.useLayoutEffect)(() => {
134
302
  if (!containerRef.current) return;
135
303
  updateSkeletons();
136
304
  const observer = new ResizeObserver(() => {
@@ -147,7 +315,7 @@ var AutoSkeleton = ({
147
315
  observer.disconnect();
148
316
  mutationObserver.disconnect();
149
317
  };
150
- }, [loading, children]);
318
+ }, [showSkeleton, children]);
151
319
  const getEffectiveColors = () => {
152
320
  if (config?.color || config?.highlightColor) {
153
321
  return {
@@ -155,13 +323,13 @@ var AutoSkeleton = ({
155
323
  highlightColor: config.highlightColor
156
324
  };
157
325
  }
158
- if (theme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
326
+ if (resolvedTheme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
159
327
  return {
160
328
  color: config.darkColor,
161
329
  highlightColor: config.darkHighlightColor
162
330
  };
163
331
  }
164
- if (theme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
332
+ if (resolvedTheme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
165
333
  return {
166
334
  color: config.lightColor,
167
335
  highlightColor: config.lightHighlightColor
@@ -170,78 +338,99 @@ var AutoSkeleton = ({
170
338
  return { color: void 0, highlightColor: void 0 };
171
339
  };
172
340
  const effectiveColors = getEffectiveColors();
173
- return /* @__PURE__ */ jsxRuntime.jsxs(
341
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
174
342
  "div",
175
343
  {
176
- style: { position: "relative", display: "inline-block", width: "auto" },
344
+ className,
345
+ style: { position: "relative", display: "block", ...style },
177
346
  ...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
347
+ "aria-busy": busy,
178
348
  children: [
179
- /* @__PURE__ */ jsxRuntime.jsx(
349
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { role: "status", style: VISUALLY_HIDDEN, children: busy ? loadingLabel : "" }),
350
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
180
351
  "div",
181
352
  {
182
353
  ref: containerRef,
183
354
  style: {
184
- opacity: loading ? 0 : 1,
185
- transition: "opacity 0.2s ease"
186
- // Smooth transition when loading finishes
355
+ opacity: hidden ? 0 : 1,
356
+ // Hide instantly (no ghost content under the bars); fade only on reveal
357
+ transition: hidden ? "none" : `opacity ${FADE_MS}ms ease`
187
358
  },
188
- "aria-hidden": loading,
359
+ "aria-hidden": hidden,
360
+ inert: busy,
189
361
  children
190
362
  }
191
363
  ),
192
- loading && /* @__PURE__ */ jsxRuntime.jsx(
364
+ (showSkeleton || fadingOut) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
193
365
  "div",
194
366
  {
367
+ "data-testid": "skeleton-overlay",
195
368
  style: {
196
369
  position: "absolute",
197
370
  top: 0,
198
371
  left: 0,
199
372
  width: "100%",
200
373
  height: "100%",
201
- pointerEvents: "none"
374
+ pointerEvents: "none",
375
+ opacity: showSkeleton ? 1 : 0,
376
+ transition: `opacity ${FADE_MS}ms ease`
202
377
  },
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",
378
+ children: [
379
+ !measured && // Server render / before hydration: nothing can be measured yet, so
380
+ // cover the reserved space with one block instead of showing nothing
381
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
382
+ SkeletonPrimitive,
383
+ {
384
+ width: "100%",
385
+ height: "100%",
386
+ color: effectiveColors.color,
387
+ highlightColor: effectiveColors.highlightColor
388
+ }
389
+ ),
390
+ skeletons.map((sk) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
391
+ "div",
392
+ {
393
+ style: { position: "absolute", top: sk.top, left: sk.left },
394
+ children: sk.type === "primitive" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
395
+ SkeletonPrimitive,
220
396
  {
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
- }
397
+ width: sk.width,
398
+ height: sk.height,
399
+ borderRadius: sk.borderRadius,
400
+ color: effectiveColors.color,
401
+ highlightColor: effectiveColors.highlightColor
231
402
  }
403
+ ) : (
404
+ /* Container Clone: Renders static style */
405
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
406
+ "div",
407
+ {
408
+ style: {
409
+ width: sk.width,
410
+ height: sk.height,
411
+ borderRadius: sk.borderRadius,
412
+ ...sk.style,
413
+ // Apply captured container styles
414
+ // Ensure it doesn't hide children if z-index issues arise,
415
+ // but since we render in DOM order, children come later and sit on top.
416
+ boxSizing: "border-box"
417
+ }
418
+ }
419
+ )
232
420
  )
233
- )
234
- },
235
- sk.key
236
- ))
421
+ },
422
+ sk.key
423
+ ))
424
+ ]
237
425
  }
238
426
  )
239
427
  ]
240
428
  }
241
429
  );
242
430
  };
243
-
244
- exports.AutoSkeleton = AutoSkeleton;
245
- exports.SkeletonPrimitive = SkeletonPrimitive;
246
- //# sourceMappingURL=index.cjs.map
431
+ // Annotate the CommonJS export names for ESM import in node:
432
+ 0 && (module.exports = {
433
+ AutoSkeleton,
434
+ SkeletonPrimitive
435
+ });
247
436
  //# sourceMappingURL=index.cjs.map