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 +36 -23
- package/dist/index.cjs +276 -87
- 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 +255 -85
- 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,31 +121,44 @@ 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
|
|
|
132
|
-
| Key
|
|
133
|
-
|
|
|
134
|
-
| `color`
|
|
135
|
-
| `highlightColor`
|
|
136
|
-
| `borderRadius`
|
|
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
|
|
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`
|
|
145
|
-
4. **No Shift**: Because the hidden content is already holding the space, when `loading` becomes `false`,
|
|
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
|
|
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_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__ */
|
|
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
|
}
|
|
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
|
|
47
|
-
const
|
|
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
|
|
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 =
|
|
63
|
-
const hasBorder =
|
|
64
|
-
const hasShadow =
|
|
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:
|
|
70
|
-
border:
|
|
71
|
-
borderRadius:
|
|
72
|
-
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
|
|
85
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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 (
|
|
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__ */
|
|
341
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
174
342
|
"div",
|
|
175
343
|
{
|
|
176
|
-
|
|
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__ */
|
|
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:
|
|
185
|
-
|
|
186
|
-
|
|
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":
|
|
359
|
+
"aria-hidden": hidden,
|
|
360
|
+
inert: busy,
|
|
189
361
|
children
|
|
190
362
|
}
|
|
191
363
|
),
|
|
192
|
-
|
|
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:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
236
|
-
|
|
421
|
+
},
|
|
422
|
+
sk.key
|
|
423
|
+
))
|
|
424
|
+
]
|
|
237
425
|
}
|
|
238
426
|
)
|
|
239
427
|
]
|
|
240
428
|
}
|
|
241
429
|
);
|
|
242
430
|
};
|
|
243
|
-
|
|
244
|
-
exports
|
|
245
|
-
|
|
246
|
-
|
|
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
|