smart-skeleton-loader 1.0.1 β 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 +28 -18
- package/dist/index.cjs +256 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +235 -84
- package/dist/index.js.map +1 -1
- package/dist/styles.d.cts +1 -1
- package/dist/styles.d.ts +1 -1
- package/package.json +44 -42
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
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
|
-
[](https://www.npmjs.com/package/smart-skeleton-loader)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://buymeacoffee.com/dashnyamkkq)
|
|
9
9
|
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
`
|
|
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 `
|
|
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
|
|
35
|
+
npm install smart-skeleton-loader
|
|
36
36
|
# or
|
|
37
|
-
yarn add
|
|
37
|
+
yarn add smart-skeleton-loader
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
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 '
|
|
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. `
|
|
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
|
-
`
|
|
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,11 +121,16 @@ Customize the appearance via the `config` prop:
|
|
|
121
121
|
|
|
122
122
|
## π§© API Reference
|
|
123
123
|
|
|
124
|
-
| Prop
|
|
125
|
-
|
|
|
126
|
-
| `loading`
|
|
127
|
-
| `children`
|
|
128
|
-
| `config`
|
|
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
|
|
|
@@ -139,13 +144,18 @@ Customize the appearance via the `config` prop:
|
|
|
139
144
|
|
|
140
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.
|
|
141
146
|
2. **Smart Scan**: We use a `TreeWalker` to scan the DOM elements.
|
|
142
|
-
- If it finds text
|
|
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**.
|
|
143
149
|
- If it finds a styled container -> It creates a **Container Skeleton**.
|
|
144
|
-
3. **Overlay**: We render absolute positioned `divs`
|
|
145
|
-
4. **No Shift**: Because the hidden content is already holding the space, when `loading` becomes `false`,
|
|
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!
|
|
146
152
|
|
|
147
153
|
## β οΈ Notes
|
|
148
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' }}`.
|
|
149
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.
|
|
150
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.
|
|
151
161
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,75 +1,208 @@
|
|
|
1
|
-
'use
|
|
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
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
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__ */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
47
|
-
const
|
|
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
|
|
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 =
|
|
63
|
-
const hasBorder =
|
|
64
|
-
const hasShadow =
|
|
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:
|
|
70
|
-
border:
|
|
71
|
-
borderRadius:
|
|
72
|
-
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
|
|
85
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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__ */
|
|
322
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
174
323
|
"div",
|
|
175
324
|
{
|
|
176
|
-
|
|
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__ */
|
|
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:
|
|
185
|
-
|
|
186
|
-
|
|
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":
|
|
340
|
+
"aria-hidden": hidden,
|
|
341
|
+
inert: busy,
|
|
189
342
|
children
|
|
190
343
|
}
|
|
191
344
|
),
|
|
192
|
-
|
|
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:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
236
|
-
|
|
402
|
+
},
|
|
403
|
+
sk.key
|
|
404
|
+
))
|
|
405
|
+
]
|
|
237
406
|
}
|
|
238
407
|
)
|
|
239
408
|
]
|
|
240
409
|
}
|
|
241
410
|
);
|
|
242
411
|
};
|
|
243
|
-
|
|
244
|
-
exports
|
|
245
|
-
|
|
246
|
-
|
|
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
|