smart-skeleton-loader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/index.cjs +247 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +244 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +24 -0
- package/dist/styles.css.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# react-auto-skeleton
|
|
2
|
+
|
|
3
|
+
> **The smartest skeleton loader for React.**
|
|
4
|
+
> Automatically generate perfectly matching skeletons by measuring your actual components.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/react-auto-skeleton)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 📏 **Auto-Measurement**: Measures rendered elements to perfectly match dimensions.
|
|
14
|
+
- 🧠 **Smart Detection**:
|
|
15
|
+
- **Text**: Detects text blocks and wraps them.
|
|
16
|
+
- **Content**: Automatically handles `<img>`, `<input>`, `<button>`, `<textarea>`.
|
|
17
|
+
- **Containers**: **NEW!** Detects cards and sections with borders/shadows and renders a static structural skeleton for them.
|
|
18
|
+
- 🧘 **No Layout Shift**: Preserves the exact container space while loading to prevent CLS (Cumulative Layout Shift).
|
|
19
|
+
- 🎨 **Customizable**: Control colors, border radius, and animations.
|
|
20
|
+
- ♻️ **Template Pattern**: Use your actual components mock data to generate skeletons for data-fetching lists.
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install react-auto-skeleton framer-motion
|
|
26
|
+
# or
|
|
27
|
+
yarn add react-auto-skeleton framer-motion
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
_Note: `framer-motion` is a peer dependency used for the smooth pulse animations._
|
|
31
|
+
|
|
32
|
+
## 🚀 Usage
|
|
33
|
+
|
|
34
|
+
### 1. Basic Example
|
|
35
|
+
|
|
36
|
+
Wrap your component in `AutoSkeleton` and control the `loading` state.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { AutoSkeleton } from "react-auto-skeleton";
|
|
40
|
+
|
|
41
|
+
const UserProfile = ({ loading, user }) => {
|
|
42
|
+
return (
|
|
43
|
+
<AutoSkeleton loading={loading}>
|
|
44
|
+
<div className="card">
|
|
45
|
+
{/* 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>
|
|
49
|
+
<button>Follow</button>
|
|
50
|
+
</div>
|
|
51
|
+
</AutoSkeleton>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Data Fetching (The Template Pattern)
|
|
57
|
+
|
|
58
|
+
When fetching lists, you might start with an empty array. **How can we measure an empty array?**
|
|
59
|
+
|
|
60
|
+
The solution is the **Template Pattern**: Render "dummy" items while loading. `react-auto-skeleton` will measure these dummies and create the skeleton layout.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const UserList = () => {
|
|
64
|
+
const { data, isLoading } = useFetchUsers();
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="list">
|
|
68
|
+
{isLoading
|
|
69
|
+
? // RENDER 3 TEMPLATES WHILE LOADING
|
|
70
|
+
Array.from({ length: 3 }).map((_, i) => (
|
|
71
|
+
<AutoSkeleton key={i} loading={true}>
|
|
72
|
+
<UserCard
|
|
73
|
+
name="Template User Name"
|
|
74
|
+
bio="This is a template bio for measurement."
|
|
75
|
+
avatar="/placeholder.png"
|
|
76
|
+
/>
|
|
77
|
+
</AutoSkeleton>
|
|
78
|
+
))
|
|
79
|
+
: // RENDER REAL DATA
|
|
80
|
+
data.map((user) => <UserCard key={user.id} {...user} />)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Container Detection (Cards)
|
|
87
|
+
|
|
88
|
+
`react-auto-skeleton` intelligently distinguishes between **Content** and **Containers**.
|
|
89
|
+
|
|
90
|
+
- **Content** (Text, Images, Buttons) -> Renders as a **Pulsing Gray Box**.
|
|
91
|
+
- **Containers** (Elements with visible `border`, `background-color`, or `box-shadow`) -> Renders as a **Static Clone** (with your exact styles) and **NO** pulse.
|
|
92
|
+
|
|
93
|
+
This means a "Card" looks like a card (white background, shadow), and the content inside it pulses.
|
|
94
|
+
|
|
95
|
+
## ⚙️ Configuration
|
|
96
|
+
|
|
97
|
+
Customize the appearance via the `config` prop:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<AutoSkeleton
|
|
101
|
+
loading={true}
|
|
102
|
+
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)
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<MyComponent />
|
|
109
|
+
</AutoSkeleton>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 🧩 API Reference
|
|
113
|
+
|
|
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. |
|
|
119
|
+
|
|
120
|
+
### Config Object
|
|
121
|
+
|
|
122
|
+
| Key | Type | Default | Description |
|
|
123
|
+
| ---------------- | -------- | --------- | ---------------------------------------------------- |
|
|
124
|
+
| `color` | `string` | `#eee` | Background color of primitives. |
|
|
125
|
+
| `highlightColor` | `string` | `#f5f5f5` | Animation highlight color. |
|
|
126
|
+
| `borderRadius` | `string` | `4px` | Fallback radius (Auto-detection is attempted first). |
|
|
127
|
+
|
|
128
|
+
## 🛠 How It Works
|
|
129
|
+
|
|
130
|
+
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
|
+
2. **Smart Scan**: We use a `TreeWalker` to scan the DOM elements.
|
|
132
|
+
- If it finds text/images -> It creates a **Primitive Skeleton**.
|
|
133
|
+
- 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!
|
|
136
|
+
|
|
137
|
+
## ⚠️ Notes
|
|
138
|
+
|
|
139
|
+
- **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
|
+
- **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
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT © 2026
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var framerMotion = require('framer-motion');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/AutoSkeleton.tsx
|
|
8
|
+
var SkeletonPrimitive = ({
|
|
9
|
+
width,
|
|
10
|
+
height,
|
|
11
|
+
borderRadius = "4px",
|
|
12
|
+
color = "var(--auto-skeleton-color, #eee)",
|
|
13
|
+
highlightColor = "var(--auto-skeleton-highlight, #f5f5f5)",
|
|
14
|
+
style,
|
|
15
|
+
...props
|
|
16
|
+
}) => {
|
|
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
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
var AutoSkeleton = ({
|
|
41
|
+
loading,
|
|
42
|
+
children,
|
|
43
|
+
config
|
|
44
|
+
}) => {
|
|
45
|
+
const theme = config?.theme || "auto";
|
|
46
|
+
const containerRef = react.useRef(null);
|
|
47
|
+
const [skeletons, setSkeletons] = react.useState([]);
|
|
48
|
+
const updateSkeletons = () => {
|
|
49
|
+
if (!containerRef.current) return;
|
|
50
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
51
|
+
const newSkeletons = [];
|
|
52
|
+
const getSkeletonType = (el) => {
|
|
53
|
+
const tagName = el.tagName.toLowerCase();
|
|
54
|
+
const style = window.getComputedStyle(el);
|
|
55
|
+
if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
|
|
56
|
+
return { type: "primitive" };
|
|
57
|
+
}
|
|
58
|
+
const hasText = Array.from(el.childNodes).some(
|
|
59
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
|
|
60
|
+
);
|
|
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";
|
|
65
|
+
if (hasBackground || hasBorder || hasShadow) {
|
|
66
|
+
return {
|
|
67
|
+
type: "container",
|
|
68
|
+
styles: {
|
|
69
|
+
backgroundColor: style.backgroundColor,
|
|
70
|
+
border: style.border,
|
|
71
|
+
borderRadius: style.borderRadius,
|
|
72
|
+
boxShadow: style.boxShadow
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
const walker = document.createTreeWalker(
|
|
79
|
+
containerRef.current,
|
|
80
|
+
NodeFilter.SHOW_ELEMENT,
|
|
81
|
+
{
|
|
82
|
+
acceptNode: (node2) => {
|
|
83
|
+
const el = node2;
|
|
84
|
+
const style = window.getComputedStyle(el);
|
|
85
|
+
if (style.display === "none") return NodeFilter.FILTER_REJECT;
|
|
86
|
+
if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
|
|
87
|
+
return NodeFilter.FILTER_SKIP;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
let node = walker.nextNode();
|
|
92
|
+
let idx = 0;
|
|
93
|
+
while (node) {
|
|
94
|
+
const el = node;
|
|
95
|
+
const rect = el.getBoundingClientRect();
|
|
96
|
+
const typeData = getSkeletonType(el);
|
|
97
|
+
if (typeData) {
|
|
98
|
+
let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
|
|
99
|
+
if (typeData.type === "primitive") {
|
|
100
|
+
if (borderRadius === "0px" || !borderRadius) {
|
|
101
|
+
if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
|
|
102
|
+
borderRadius = "4px";
|
|
103
|
+
if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
|
|
104
|
+
if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
newSkeletons.push({
|
|
108
|
+
key: `${idx++}`,
|
|
109
|
+
type: typeData.type,
|
|
110
|
+
width: rect.width,
|
|
111
|
+
height: rect.height,
|
|
112
|
+
top: rect.top - containerRect.top,
|
|
113
|
+
left: rect.left - containerRect.left,
|
|
114
|
+
borderRadius,
|
|
115
|
+
style: typeData.styles
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
node = walker.nextNode();
|
|
119
|
+
}
|
|
120
|
+
if (newSkeletons.length === 0 && (containerRect.width > 0 || containerRect.height > 0)) {
|
|
121
|
+
newSkeletons.push({
|
|
122
|
+
key: "fallback",
|
|
123
|
+
type: "primitive",
|
|
124
|
+
width: containerRect.width,
|
|
125
|
+
height: containerRect.height,
|
|
126
|
+
top: 0,
|
|
127
|
+
left: 0,
|
|
128
|
+
borderRadius: config?.borderRadius || "4px"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
setSkeletons(newSkeletons);
|
|
132
|
+
};
|
|
133
|
+
react.useEffect(() => {
|
|
134
|
+
if (!containerRef.current) return;
|
|
135
|
+
updateSkeletons();
|
|
136
|
+
const observer = new ResizeObserver(() => {
|
|
137
|
+
updateSkeletons();
|
|
138
|
+
});
|
|
139
|
+
observer.observe(containerRef.current);
|
|
140
|
+
const mutationObserver = new MutationObserver(() => updateSkeletons());
|
|
141
|
+
mutationObserver.observe(containerRef.current, {
|
|
142
|
+
childList: true,
|
|
143
|
+
subtree: true,
|
|
144
|
+
characterData: true
|
|
145
|
+
});
|
|
146
|
+
return () => {
|
|
147
|
+
observer.disconnect();
|
|
148
|
+
mutationObserver.disconnect();
|
|
149
|
+
};
|
|
150
|
+
}, [loading, children]);
|
|
151
|
+
const getEffectiveColors = () => {
|
|
152
|
+
if (config?.color || config?.highlightColor) {
|
|
153
|
+
return {
|
|
154
|
+
color: config.color,
|
|
155
|
+
highlightColor: config.highlightColor
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (theme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
|
|
159
|
+
return {
|
|
160
|
+
color: config.darkColor,
|
|
161
|
+
highlightColor: config.darkHighlightColor
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (theme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
|
|
165
|
+
return {
|
|
166
|
+
color: config.lightColor,
|
|
167
|
+
highlightColor: config.lightHighlightColor
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { color: void 0, highlightColor: void 0 };
|
|
171
|
+
};
|
|
172
|
+
const effectiveColors = getEffectiveColors();
|
|
173
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
174
|
+
"div",
|
|
175
|
+
{
|
|
176
|
+
style: { position: "relative", display: "inline-block", width: "auto" },
|
|
177
|
+
...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
180
|
+
"div",
|
|
181
|
+
{
|
|
182
|
+
ref: containerRef,
|
|
183
|
+
style: {
|
|
184
|
+
opacity: loading ? 0 : 1,
|
|
185
|
+
transition: "opacity 0.2s ease"
|
|
186
|
+
// Smooth transition when loading finishes
|
|
187
|
+
},
|
|
188
|
+
"aria-hidden": loading,
|
|
189
|
+
children
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
loading && /* @__PURE__ */ jsxRuntime.jsx(
|
|
193
|
+
"div",
|
|
194
|
+
{
|
|
195
|
+
style: {
|
|
196
|
+
position: "absolute",
|
|
197
|
+
top: 0,
|
|
198
|
+
left: 0,
|
|
199
|
+
width: "100%",
|
|
200
|
+
height: "100%",
|
|
201
|
+
pointerEvents: "none"
|
|
202
|
+
},
|
|
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",
|
|
220
|
+
{
|
|
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
|
+
}
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
sk.key
|
|
236
|
+
))
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
exports.AutoSkeleton = AutoSkeleton;
|
|
245
|
+
exports.SkeletonPrimitive = SkeletonPrimitive;
|
|
246
|
+
//# sourceMappingURL=index.cjs.map
|
|
247
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/SkeletonPrimitive.tsx","../src/AutoSkeleton.tsx"],"names":["jsx","motion","useRef","useState","node","useEffect","jsxs"],"mappings":";;;;;;;AAWO,IAAM,oBAAsD,CAAC;AAAA,EAClE,KAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAA,GAAe,KAAA;AAAA,EACf,KAAA,GAAQ,kCAAA;AAAA,EACR,cAAA,GAAiB,yCAAA;AAAA,EACjB,KAAA;AAAA,EACA,GAAG;AACL,CAAA,KAAM;AACJ,EAAA,uBACEA,cAAA;AAAA,IAACC,mBAAA,CAAO,GAAA;AAAA,IAAP;AAAA,MACC,KAAA,EAAO;AAAA,QACL,KAAA;AAAA,QACA,MAAA;AAAA,QACA,YAAA;AAAA,QACA,eAAA,EAAiB,KAAA;AAAA,QACjB,GAAG;AAAA,OACL;AAAA,MACA,OAAA,EAAS;AAAA,QACP,eAAA,EAAiB,CAAC,KAAA,EAAO,cAAA,EAAgB,KAAK;AAAA,OAChD;AAAA,MACA,UAAA,EAAY;AAAA,QACV,QAAA,EAAU,GAAA;AAAA,QACV,MAAA,EAAQ,QAAA;AAAA,QACR,IAAA,EAAM;AAAA,OACR;AAAA,MACA,aAAA,EAAY,UAAA;AAAA,MACX,GAAG;AAAA;AAAA,GACN;AAEJ;ACtBO,IAAM,eAA4C,CAAC;AAAA,EACxD,OAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,KAAM;AACJ,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,MAAA;AAC/B,EAAA,MAAM,YAAA,GAAeC,aAAuB,IAAI,CAAA;AAChD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIC,cAAA,CAWhC,EAAE,CAAA;AAEJ,EAAA,MAAM,kBAAkB,MAAM;AAC5B,IAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAE3B,IAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,OAAA,CAAQ,qBAAA,EAAsB;AACjE,IAAA,MAAM,eAAiC,EAAC;AAGxC,IAAA,MAAM,eAAA,GAAkB,CACtB,EAAA,KAIU;AACV,MAAA,MAAM,OAAA,GAAU,EAAA,CAAG,OAAA,CAAQ,WAAA,EAAY;AACvC,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAA;AAGxC,MAAA,IAAI,CAAC,OAAO,OAAA,EAAS,QAAA,EAAU,YAAY,QAAQ,CAAA,CAAE,QAAA,CAAS,OAAO,CAAA,EAAG;AACtE,QAAA,OAAO,EAAE,MAAM,WAAA,EAAY;AAAA,MAC7B;AAGA,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,IAAA,CAAK,EAAA,CAAG,UAAU,CAAA,CAAE,IAAA;AAAA,QACxC,CAAC,MAAM,CAAA,CAAE,QAAA,KAAa,KAAK,SAAA,IAAa,CAAA,CAAE,aAAa,IAAA;AAAK,OAC9D;AACA,MAAA,IAAI,OAAA,EAAS,OAAO,EAAE,IAAA,EAAM,WAAA,EAAY;AAIxC,MAAA,MAAM,aAAA,GACJ,KAAA,CAAM,eAAA,KAAoB,kBAAA,IAC1B,MAAM,eAAA,KAAoB,aAAA;AAC5B,MAAA,MAAM,SAAA,GACJ,KAAA,CAAM,WAAA,KAAgB,KAAA,IACtB,KAAA,CAAM,WAAA,KAAgB,MAAA,IACtB,KAAA,CAAM,WAAA,KAAgB,kBAAA,IACtB,KAAA,CAAM,WAAA,KAAgB,aAAA;AACxB,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,KAAc,MAAA;AAEtC,MAAA,IAAI,aAAA,IAAiB,aAAa,SAAA,EAAW;AAC3C,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,WAAA;AAAA,UACN,MAAA,EAAQ;AAAA,YACN,iBAAiB,KAAA,CAAM,eAAA;AAAA,YACvB,QAAQ,KAAA,CAAM,MAAA;AAAA,YACd,cAAc,KAAA,CAAM,YAAA;AAAA,YACpB,WAAW,KAAA,CAAM;AAAA;AACnB,SACF;AAAA,MACF;AAEA,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAGA,IAAA,MAAM,SAAS,QAAA,CAAS,gBAAA;AAAA,MACtB,YAAA,CAAa,OAAA;AAAA,MACb,UAAA,CAAW,YAAA;AAAA,MACX;AAAA,QACE,UAAA,EAAY,CAACC,KAAAA,KAAS;AACpB,UAAA,MAAM,EAAA,GAAKA,KAAAA;AACX,UAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAA;AACxC,UAAA,IAAI,KAAA,CAAM,OAAA,KAAY,MAAA,EAAQ,OAAO,UAAA,CAAW,aAAA;AAKhD,UAAA,IAAI,eAAA,CAAgB,EAAE,CAAA,EAAG,OAAO,UAAA,CAAW,aAAA;AAE3C,UAAA,OAAO,UAAA,CAAW,WAAA;AAAA,QACpB;AAAA;AACF,KACF;AAEA,IAAA,IAAI,IAAA,GAAO,OAAO,QAAA,EAAS;AAC3B,IAAA,IAAI,GAAA,GAAM,CAAA;AACV,IAAA,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,EAAA,GAAK,IAAA;AACX,MAAA,MAAM,IAAA,GAAO,GAAG,qBAAA,EAAsB;AACtC,MAAA,MAAM,QAAA,GAAW,gBAAgB,EAAE,CAAA;AAEnC,MAAA,IAAI,QAAA,EAAU;AAEZ,QAAA,IAAI,YAAA,GACF,QAAQ,YAAA,IACR,QAAA,CAAS,QAAQ,YAAA,IACjB,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAA,CAAE,YAAA;AAE9B,QAAA,IAAI,QAAA,CAAS,SAAS,WAAA,EAAa;AAEjC,UAAA,IAAI,YAAA,KAAiB,KAAA,IAAS,CAAC,YAAA,EAAc;AAC3C,YAAA,IACE,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAA,CAAE,YAAA,KAAiB,KAAA,IAC5C,IAAA,CAAK,KAAA,KAAU,IAAA,CAAK,MAAA,IAAU,IAAA,CAAK,KAAA,GAAQ,EAAA,EAC5C;AAGF,YAAA,YAAA,GAAe,KAAA;AACf,YAAA,IAAI,EAAA,CAAG,OAAA,CAAQ,WAAA,EAAY,KAAM,UAAU,YAAA,GAAe,KAAA;AAC1D,YAAA,IAAI,EAAA,CAAG,OAAA,CAAQ,WAAA,EAAY,KAAM,OAAO,YAAA,GAAe,KAAA;AAAA,UACzD;AAAA,QACF;AAEA,QAAA,YAAA,CAAa,IAAA,CAAK;AAAA,UAChB,GAAA,EAAK,GAAG,GAAA,EAAK,CAAA,CAAA;AAAA,UACb,MAAM,QAAA,CAAS,IAAA;AAAA,UACf,OAAO,IAAA,CAAK,KAAA;AAAA,UACZ,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK,IAAA,CAAK,GAAA,GAAM,aAAA,CAAc,GAAA;AAAA,UAC9B,IAAA,EAAM,IAAA,CAAK,IAAA,GAAO,aAAA,CAAc,IAAA;AAAA,UAChC,YAAA;AAAA,UACA,OAAO,QAAA,CAAS;AAAA,SACjB,CAAA;AAAA,MACH;AAEA,MAAA,IAAA,GAAO,OAAO,QAAA,EAAS;AAAA,IACzB;AAKA,IAAA,IACE,YAAA,CAAa,WAAW,CAAA,KACvB,aAAA,CAAc,QAAQ,CAAA,IAAK,aAAA,CAAc,SAAS,CAAA,CAAA,EACnD;AACA,MAAA,YAAA,CAAa,IAAA,CAAK;AAAA,QAChB,GAAA,EAAK,UAAA;AAAA,QACL,IAAA,EAAM,WAAA;AAAA,QACN,OAAO,aAAA,CAAc,KAAA;AAAA,QACrB,QAAQ,aAAA,CAAc,MAAA;AAAA,QACtB,GAAA,EAAK,CAAA;AAAA,QACL,IAAA,EAAM,CAAA;AAAA,QACN,YAAA,EAAe,QAAQ,YAAA,IAA2B;AAAA,OACnD,CAAA;AAAA,IACH;AAEA,IAAA,YAAA,CAAa,YAAY,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAG3B,IAAA,eAAA,EAAgB;AAEhB,IAAA,MAAM,QAAA,GAAW,IAAI,cAAA,CAAe,MAAM;AACxC,MAAA,eAAA,EAAgB;AAAA,IAClB,CAAC,CAAA;AAED,IAAA,QAAA,CAAS,OAAA,CAAQ,aAAa,OAAO,CAAA;AAGrC,IAAA,MAAM,gBAAA,GAAmB,IAAI,gBAAA,CAAiB,MAAM,iBAAiB,CAAA;AACrE,IAAA,gBAAA,CAAiB,OAAA,CAAQ,aAAa,OAAA,EAAS;AAAA,MAC7C,SAAA,EAAW,IAAA;AAAA,MACX,OAAA,EAAS,IAAA;AAAA,MACT,aAAA,EAAe;AAAA,KAChB,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,gBAAA,CAAiB,UAAA,EAAW;AAAA,IAC9B,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAA,EAAS,QAAQ,CAAC,CAAA;AAGtB,EAAA,MAAM,qBAAqB,MAAM;AAE/B,IAAA,IAAI,MAAA,EAAQ,KAAA,IAAS,MAAA,EAAQ,cAAA,EAAgB;AAC3C,MAAA,OAAO;AAAA,QACL,OAAO,MAAA,CAAO,KAAA;AAAA,QACd,gBAAgB,MAAA,CAAO;AAAA,OACzB;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,KAAU,MAAA,KAAW,MAAA,EAAQ,SAAA,IAAa,QAAQ,kBAAA,CAAA,EAAqB;AACzE,MAAA,OAAO;AAAA,QACL,OAAO,MAAA,CAAO,SAAA;AAAA,QACd,gBAAgB,MAAA,CAAO;AAAA,OACzB;AAAA,IACF;AACA,IAAA,IACE,KAAA,KAAU,OAAA,KACT,MAAA,EAAQ,UAAA,IAAc,QAAQ,mBAAA,CAAA,EAC/B;AACA,MAAA,OAAO;AAAA,QACL,OAAO,MAAA,CAAO,UAAA;AAAA,QACd,gBAAgB,MAAA,CAAO;AAAA,OACzB;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,cAAA,EAAgB,MAAA,EAAU;AAAA,EACvD,CAAA;AAEA,EAAA,MAAM,kBAAkB,kBAAA,EAAmB;AAE3C,EAAA,uBACEC,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAO,EAAE,QAAA,EAAU,YAAY,OAAA,EAAS,cAAA,EAAgB,OAAO,MAAA,EAAO;AAAA,MACrE,GAAI,KAAA,KAAU,MAAA,GAAS,EAAE,qBAAA,EAAuB,KAAA,KAAU,EAAC;AAAA,MAE5D,QAAA,EAAA;AAAA,wBAAAN,cAAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,GAAA,EAAK,YAAA;AAAA,YACL,KAAA,EAAO;AAAA,cACL,OAAA,EAAS,UAAU,CAAA,GAAI,CAAA;AAAA,cACvB,UAAA,EAAY;AAAA;AAAA,aACd;AAAA,YACA,aAAA,EAAa,OAAA;AAAA,YAEZ;AAAA;AAAA,SACH;AAAA,QAEC,2BACCA,cAAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,KAAA,EAAO;AAAA,cACL,QAAA,EAAU,UAAA;AAAA,cACV,GAAA,EAAK,CAAA;AAAA,cACL,IAAA,EAAM,CAAA;AAAA,cACN,KAAA,EAAO,MAAA;AAAA,cACP,MAAA,EAAQ,MAAA;AAAA,cACR,aAAA,EAAe;AAAA,aACjB;AAAA,YAEC,QAAA,EAAA,SAAA,CAAU,GAAA,CAAI,CAAC,EAAA,qBACdA,cAAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBAEC,KAAA,EAAO,EAAE,QAAA,EAAU,UAAA,EAAY,KAAK,EAAA,CAAG,GAAA,EAAK,IAAA,EAAM,EAAA,CAAG,IAAA,EAAK;AAAA,gBAEzD,QAAA,EAAA,EAAA,CAAG,IAAA,KAAS,WAAA,mBACXA,cAAAA;AAAA,kBAAC,iBAAA;AAAA,kBAAA;AAAA,oBACC,OAAO,EAAA,CAAG,KAAA;AAAA,oBACV,QAAQ,EAAA,CAAG,MAAA;AAAA,oBACX,cAAc,EAAA,CAAG,YAAA;AAAA,oBACjB,OAAO,eAAA,CAAgB,KAAA;AAAA,oBACvB,gBAAgB,eAAA,CAAgB;AAAA;AAAA,iBAClC;AAAA;AAAA,kCAGAA,cAAAA;AAAA,oBAAC,KAAA;AAAA,oBAAA;AAAA,sBACC,KAAA,EAAO;AAAA,wBACL,OAAO,EAAA,CAAG,KAAA;AAAA,wBACV,QAAQ,EAAA,CAAG,MAAA;AAAA,wBACX,cAAc,EAAA,CAAG,YAAA;AAAA,wBACjB,GAAG,EAAA,CAAG,KAAA;AAAA;AAAA;AAAA;AAAA,wBAGN,SAAA,EAAW;AAAA;AACb;AAAA;AACF;AAAA,eAAA;AAAA,cAvBG,EAAA,CAAG;AAAA,aA0BX;AAAA;AAAA;AACH;AAAA;AAAA,GAEJ;AAEJ","file":"index.cjs","sourcesContent":["import React from \"react\";\nimport { motion, HTMLMotionProps } from \"framer-motion\";\n\nexport interface SkeletonPrimitiveProps extends HTMLMotionProps<\"div\"> {\n width?: string | number;\n height?: string | number;\n borderRadius?: string | number;\n color?: string;\n highlightColor?: string;\n}\n\nexport const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps> = ({\n width,\n height,\n borderRadius = \"4px\",\n color = \"var(--auto-skeleton-color, #eee)\",\n highlightColor = \"var(--auto-skeleton-highlight, #f5f5f5)\",\n style,\n ...props\n}) => {\n return (\n <motion.div\n style={{\n width,\n height,\n borderRadius,\n backgroundColor: color,\n ...style,\n }}\n animate={{\n backgroundColor: [color, highlightColor, color],\n }}\n transition={{\n duration: 1.5,\n repeat: Infinity,\n ease: \"easeInOut\",\n }}\n data-testid=\"skeleton\"\n {...props}\n />\n );\n};\n","import React, { useRef, useEffect, useState, ReactNode } from \"react\";\nimport { SkeletonPrimitive } from \"./SkeletonPrimitive\";\n\nexport interface AutoSkeletonProps {\n loading: boolean;\n children: ReactNode;\n config?: {\n color?: string;\n highlightColor?: string;\n borderRadius?: string | number;\n theme?: \"light\" | \"dark\" | \"auto\";\n // Theme-specific colors (override CSS variables)\n lightColor?: string;\n lightHighlightColor?: string;\n darkColor?: string;\n darkHighlightColor?: string;\n };\n}\n\nexport const AutoSkeleton: React.FC<AutoSkeletonProps> = ({\n loading,\n children,\n config,\n}) => {\n const theme = config?.theme || \"auto\";\n const containerRef = useRef<HTMLDivElement>(null);\n const [skeletons, setSkeletons] = useState<\n Array<{\n key: string;\n type: \"primitive\" | \"container\";\n width: number;\n height: number;\n top: number;\n left: number;\n borderRadius: string;\n style?: React.CSSProperties; // For container styles\n }>\n >([]);\n\n const updateSkeletons = () => {\n if (!containerRef.current) return;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const newSkeletons: typeof skeletons = [];\n\n // Helper to categorize node\n const getSkeletonType = (\n el: Element,\n ): {\n type: \"primitive\" | \"container\";\n styles?: React.CSSProperties;\n } | null => {\n const tagName = el.tagName.toLowerCase();\n const style = window.getComputedStyle(el);\n\n // 1. Atomic Content (always pulse)\n if ([\"img\", \"input\", \"button\", \"textarea\", \"select\"].includes(tagName)) {\n return { type: \"primitive\" };\n }\n\n // 2. Text Content (always pulse)\n const hasText = Array.from(el.childNodes).some(\n (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim(),\n );\n if (hasText) return { type: \"primitive\" };\n\n // 3. Structural Container (static clone)\n // If it has visible styles but no direct text => it's a container\n const hasBackground =\n style.backgroundColor !== \"rgba(0, 0, 0, 0)\" &&\n style.backgroundColor !== \"transparent\";\n const hasBorder =\n style.borderWidth !== \"0px\" &&\n style.borderStyle !== \"none\" &&\n style.borderColor !== \"rgba(0, 0, 0, 0)\" &&\n style.borderColor !== \"transparent\";\n const hasShadow = style.boxShadow !== \"none\";\n\n if (hasBackground || hasBorder || hasShadow) {\n return {\n type: \"container\",\n styles: {\n backgroundColor: style.backgroundColor,\n border: style.border,\n borderRadius: style.borderRadius,\n boxShadow: style.boxShadow,\n },\n };\n }\n\n return null;\n };\n\n // Traverse\n const walker = document.createTreeWalker(\n containerRef.current,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (node) => {\n const el = node as Element;\n const style = window.getComputedStyle(el);\n if (style.display === \"none\") return NodeFilter.FILTER_REJECT;\n\n // If it matches a type, accept it.\n // Note: We want to accept containers AND traverse their children.\n // FILTER_ACCEPT does both.\n if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;\n\n return NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n let node = walker.nextNode();\n let idx = 0;\n while (node) {\n const el = node as Element;\n const rect = el.getBoundingClientRect();\n const typeData = getSkeletonType(el);\n\n if (typeData) {\n // Determine border radius logic\n let borderRadius =\n config?.borderRadius ||\n typeData.styles?.borderRadius ||\n window.getComputedStyle(el).borderRadius;\n\n if (typeData.type === \"primitive\") {\n // Auto-detect circle for rounded primitives\n if (borderRadius === \"0px\" || !borderRadius) {\n if (\n window.getComputedStyle(el).borderRadius === \"50%\" ||\n (rect.width === rect.height && rect.width < 50)\n ) {\n // Keep heuristic or default\n }\n borderRadius = \"4px\";\n if (el.tagName.toLowerCase() === \"button\") borderRadius = \"6px\";\n if (el.tagName.toLowerCase() === \"img\") borderRadius = \"4px\";\n }\n }\n\n newSkeletons.push({\n key: `${idx++}`,\n type: typeData.type,\n width: rect.width,\n height: rect.height,\n top: rect.top - containerRect.top,\n left: rect.left - containerRect.left,\n borderRadius: borderRadius as string,\n style: typeData.styles,\n });\n }\n\n node = walker.nextNode();\n }\n\n // If no content nodes found, but container has size (maybe a canvas or simple div w/ background),\n // Fallback? Or maybe user wants the single block behavior.\n // Let's add that fallback if newSkeletons is empty but container has dimension.\n if (\n newSkeletons.length === 0 &&\n (containerRect.width > 0 || containerRect.height > 0)\n ) {\n newSkeletons.push({\n key: \"fallback\",\n type: \"primitive\",\n width: containerRect.width,\n height: containerRect.height,\n top: 0,\n left: 0,\n borderRadius: (config?.borderRadius as string) || \"4px\",\n });\n }\n\n setSkeletons(newSkeletons);\n };\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n // Initial measure\n updateSkeletons();\n\n const observer = new ResizeObserver(() => {\n updateSkeletons();\n });\n\n observer.observe(containerRef.current);\n // Also observe children mutations if possible, but ResizeObserver on container catches layout changes often.\n // For robust child changes:\n const mutationObserver = new MutationObserver(() => updateSkeletons());\n mutationObserver.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n mutationObserver.disconnect();\n };\n }, [loading, children]); // Re-scan when children change\n\n // Determine effective colors based on theme\n const getEffectiveColors = () => {\n // If user passed explicit color/highlightColor, use those\n if (config?.color || config?.highlightColor) {\n return {\n color: config.color,\n highlightColor: config.highlightColor,\n };\n }\n // If theme-specific colors are provided, use them\n if (theme === \"dark\" && (config?.darkColor || config?.darkHighlightColor)) {\n return {\n color: config.darkColor,\n highlightColor: config.darkHighlightColor,\n };\n }\n if (\n theme === \"light\" &&\n (config?.lightColor || config?.lightHighlightColor)\n ) {\n return {\n color: config.lightColor,\n highlightColor: config.lightHighlightColor,\n };\n }\n // Otherwise, let CSS variables handle it (undefined = use CSS var defaults)\n return { color: undefined, highlightColor: undefined };\n };\n\n const effectiveColors = getEffectiveColors();\n\n return (\n <div\n style={{ position: \"relative\", display: \"inline-block\", width: \"auto\" }}\n {...(theme !== \"auto\" ? { \"data-skeleton-theme\": theme } : {})}\n >\n <div\n ref={containerRef}\n style={{\n opacity: loading ? 0 : 1,\n transition: \"opacity 0.2s ease\", // Smooth transition when loading finishes\n }}\n aria-hidden={loading}\n >\n {children}\n </div>\n\n {loading && (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n }}\n >\n {skeletons.map((sk) => (\n <div\n key={sk.key}\n style={{ position: \"absolute\", top: sk.top, left: sk.left }}\n >\n {sk.type === \"primitive\" ? (\n <SkeletonPrimitive\n width={sk.width}\n height={sk.height}\n borderRadius={sk.borderRadius}\n color={effectiveColors.color}\n highlightColor={effectiveColors.highlightColor}\n />\n ) : (\n /* Container Clone: Renders static style */\n <div\n style={{\n width: sk.width,\n height: sk.height,\n borderRadius: sk.borderRadius,\n ...sk.style, // Apply captured container styles\n // Ensure it doesn't hide children if z-index issues arise,\n // but since we render in DOM order, children come later and sit on top.\n boxSizing: \"border-box\",\n }}\n />\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n};\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { HTMLMotionProps } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
interface AutoSkeletonProps {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
config?: {
|
|
8
|
+
color?: string;
|
|
9
|
+
highlightColor?: string;
|
|
10
|
+
borderRadius?: string | number;
|
|
11
|
+
theme?: "light" | "dark" | "auto";
|
|
12
|
+
lightColor?: string;
|
|
13
|
+
lightHighlightColor?: string;
|
|
14
|
+
darkColor?: string;
|
|
15
|
+
darkHighlightColor?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
declare const AutoSkeleton: React.FC<AutoSkeletonProps>;
|
|
19
|
+
|
|
20
|
+
interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
|
|
21
|
+
width?: string | number;
|
|
22
|
+
height?: string | number;
|
|
23
|
+
borderRadius?: string | number;
|
|
24
|
+
color?: string;
|
|
25
|
+
highlightColor?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps>;
|
|
28
|
+
|
|
29
|
+
export { AutoSkeleton, type AutoSkeletonProps, SkeletonPrimitive, type SkeletonPrimitiveProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { HTMLMotionProps } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
interface AutoSkeletonProps {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
config?: {
|
|
8
|
+
color?: string;
|
|
9
|
+
highlightColor?: string;
|
|
10
|
+
borderRadius?: string | number;
|
|
11
|
+
theme?: "light" | "dark" | "auto";
|
|
12
|
+
lightColor?: string;
|
|
13
|
+
lightHighlightColor?: string;
|
|
14
|
+
darkColor?: string;
|
|
15
|
+
darkHighlightColor?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
declare const AutoSkeleton: React.FC<AutoSkeletonProps>;
|
|
19
|
+
|
|
20
|
+
interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
|
|
21
|
+
width?: string | number;
|
|
22
|
+
height?: string | number;
|
|
23
|
+
borderRadius?: string | number;
|
|
24
|
+
color?: string;
|
|
25
|
+
highlightColor?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps>;
|
|
28
|
+
|
|
29
|
+
export { AutoSkeleton, type AutoSkeletonProps, SkeletonPrimitive, type SkeletonPrimitiveProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/AutoSkeleton.tsx
|
|
6
|
+
var SkeletonPrimitive = ({
|
|
7
|
+
width,
|
|
8
|
+
height,
|
|
9
|
+
borderRadius = "4px",
|
|
10
|
+
color = "var(--auto-skeleton-color, #eee)",
|
|
11
|
+
highlightColor = "var(--auto-skeleton-highlight, #f5f5f5)",
|
|
12
|
+
style,
|
|
13
|
+
...props
|
|
14
|
+
}) => {
|
|
15
|
+
return /* @__PURE__ */ jsx(
|
|
16
|
+
motion.div,
|
|
17
|
+
{
|
|
18
|
+
style: {
|
|
19
|
+
width,
|
|
20
|
+
height,
|
|
21
|
+
borderRadius,
|
|
22
|
+
backgroundColor: color,
|
|
23
|
+
...style
|
|
24
|
+
},
|
|
25
|
+
animate: {
|
|
26
|
+
backgroundColor: [color, highlightColor, color]
|
|
27
|
+
},
|
|
28
|
+
transition: {
|
|
29
|
+
duration: 1.5,
|
|
30
|
+
repeat: Infinity,
|
|
31
|
+
ease: "easeInOut"
|
|
32
|
+
},
|
|
33
|
+
"data-testid": "skeleton",
|
|
34
|
+
...props
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
var AutoSkeleton = ({
|
|
39
|
+
loading,
|
|
40
|
+
children,
|
|
41
|
+
config
|
|
42
|
+
}) => {
|
|
43
|
+
const theme = config?.theme || "auto";
|
|
44
|
+
const containerRef = useRef(null);
|
|
45
|
+
const [skeletons, setSkeletons] = useState([]);
|
|
46
|
+
const updateSkeletons = () => {
|
|
47
|
+
if (!containerRef.current) return;
|
|
48
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
49
|
+
const newSkeletons = [];
|
|
50
|
+
const getSkeletonType = (el) => {
|
|
51
|
+
const tagName = el.tagName.toLowerCase();
|
|
52
|
+
const style = window.getComputedStyle(el);
|
|
53
|
+
if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
|
|
54
|
+
return { type: "primitive" };
|
|
55
|
+
}
|
|
56
|
+
const hasText = Array.from(el.childNodes).some(
|
|
57
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
|
|
58
|
+
);
|
|
59
|
+
if (hasText) return { type: "primitive" };
|
|
60
|
+
const hasBackground = style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent";
|
|
61
|
+
const hasBorder = style.borderWidth !== "0px" && style.borderStyle !== "none" && style.borderColor !== "rgba(0, 0, 0, 0)" && style.borderColor !== "transparent";
|
|
62
|
+
const hasShadow = style.boxShadow !== "none";
|
|
63
|
+
if (hasBackground || hasBorder || hasShadow) {
|
|
64
|
+
return {
|
|
65
|
+
type: "container",
|
|
66
|
+
styles: {
|
|
67
|
+
backgroundColor: style.backgroundColor,
|
|
68
|
+
border: style.border,
|
|
69
|
+
borderRadius: style.borderRadius,
|
|
70
|
+
boxShadow: style.boxShadow
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
const walker = document.createTreeWalker(
|
|
77
|
+
containerRef.current,
|
|
78
|
+
NodeFilter.SHOW_ELEMENT,
|
|
79
|
+
{
|
|
80
|
+
acceptNode: (node2) => {
|
|
81
|
+
const el = node2;
|
|
82
|
+
const style = window.getComputedStyle(el);
|
|
83
|
+
if (style.display === "none") return NodeFilter.FILTER_REJECT;
|
|
84
|
+
if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
|
|
85
|
+
return NodeFilter.FILTER_SKIP;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
let node = walker.nextNode();
|
|
90
|
+
let idx = 0;
|
|
91
|
+
while (node) {
|
|
92
|
+
const el = node;
|
|
93
|
+
const rect = el.getBoundingClientRect();
|
|
94
|
+
const typeData = getSkeletonType(el);
|
|
95
|
+
if (typeData) {
|
|
96
|
+
let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
|
|
97
|
+
if (typeData.type === "primitive") {
|
|
98
|
+
if (borderRadius === "0px" || !borderRadius) {
|
|
99
|
+
if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
|
|
100
|
+
borderRadius = "4px";
|
|
101
|
+
if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
|
|
102
|
+
if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
newSkeletons.push({
|
|
106
|
+
key: `${idx++}`,
|
|
107
|
+
type: typeData.type,
|
|
108
|
+
width: rect.width,
|
|
109
|
+
height: rect.height,
|
|
110
|
+
top: rect.top - containerRect.top,
|
|
111
|
+
left: rect.left - containerRect.left,
|
|
112
|
+
borderRadius,
|
|
113
|
+
style: typeData.styles
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
node = walker.nextNode();
|
|
117
|
+
}
|
|
118
|
+
if (newSkeletons.length === 0 && (containerRect.width > 0 || containerRect.height > 0)) {
|
|
119
|
+
newSkeletons.push({
|
|
120
|
+
key: "fallback",
|
|
121
|
+
type: "primitive",
|
|
122
|
+
width: containerRect.width,
|
|
123
|
+
height: containerRect.height,
|
|
124
|
+
top: 0,
|
|
125
|
+
left: 0,
|
|
126
|
+
borderRadius: config?.borderRadius || "4px"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
setSkeletons(newSkeletons);
|
|
130
|
+
};
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!containerRef.current) return;
|
|
133
|
+
updateSkeletons();
|
|
134
|
+
const observer = new ResizeObserver(() => {
|
|
135
|
+
updateSkeletons();
|
|
136
|
+
});
|
|
137
|
+
observer.observe(containerRef.current);
|
|
138
|
+
const mutationObserver = new MutationObserver(() => updateSkeletons());
|
|
139
|
+
mutationObserver.observe(containerRef.current, {
|
|
140
|
+
childList: true,
|
|
141
|
+
subtree: true,
|
|
142
|
+
characterData: true
|
|
143
|
+
});
|
|
144
|
+
return () => {
|
|
145
|
+
observer.disconnect();
|
|
146
|
+
mutationObserver.disconnect();
|
|
147
|
+
};
|
|
148
|
+
}, [loading, children]);
|
|
149
|
+
const getEffectiveColors = () => {
|
|
150
|
+
if (config?.color || config?.highlightColor) {
|
|
151
|
+
return {
|
|
152
|
+
color: config.color,
|
|
153
|
+
highlightColor: config.highlightColor
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (theme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
|
|
157
|
+
return {
|
|
158
|
+
color: config.darkColor,
|
|
159
|
+
highlightColor: config.darkHighlightColor
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (theme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
|
|
163
|
+
return {
|
|
164
|
+
color: config.lightColor,
|
|
165
|
+
highlightColor: config.lightHighlightColor
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { color: void 0, highlightColor: void 0 };
|
|
169
|
+
};
|
|
170
|
+
const effectiveColors = getEffectiveColors();
|
|
171
|
+
return /* @__PURE__ */ jsxs(
|
|
172
|
+
"div",
|
|
173
|
+
{
|
|
174
|
+
style: { position: "relative", display: "inline-block", width: "auto" },
|
|
175
|
+
...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ jsx(
|
|
178
|
+
"div",
|
|
179
|
+
{
|
|
180
|
+
ref: containerRef,
|
|
181
|
+
style: {
|
|
182
|
+
opacity: loading ? 0 : 1,
|
|
183
|
+
transition: "opacity 0.2s ease"
|
|
184
|
+
// Smooth transition when loading finishes
|
|
185
|
+
},
|
|
186
|
+
"aria-hidden": loading,
|
|
187
|
+
children
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
loading && /* @__PURE__ */ jsx(
|
|
191
|
+
"div",
|
|
192
|
+
{
|
|
193
|
+
style: {
|
|
194
|
+
position: "absolute",
|
|
195
|
+
top: 0,
|
|
196
|
+
left: 0,
|
|
197
|
+
width: "100%",
|
|
198
|
+
height: "100%",
|
|
199
|
+
pointerEvents: "none"
|
|
200
|
+
},
|
|
201
|
+
children: skeletons.map((sk) => /* @__PURE__ */ jsx(
|
|
202
|
+
"div",
|
|
203
|
+
{
|
|
204
|
+
style: { position: "absolute", top: sk.top, left: sk.left },
|
|
205
|
+
children: sk.type === "primitive" ? /* @__PURE__ */ jsx(
|
|
206
|
+
SkeletonPrimitive,
|
|
207
|
+
{
|
|
208
|
+
width: sk.width,
|
|
209
|
+
height: sk.height,
|
|
210
|
+
borderRadius: sk.borderRadius,
|
|
211
|
+
color: effectiveColors.color,
|
|
212
|
+
highlightColor: effectiveColors.highlightColor
|
|
213
|
+
}
|
|
214
|
+
) : (
|
|
215
|
+
/* Container Clone: Renders static style */
|
|
216
|
+
/* @__PURE__ */ jsx(
|
|
217
|
+
"div",
|
|
218
|
+
{
|
|
219
|
+
style: {
|
|
220
|
+
width: sk.width,
|
|
221
|
+
height: sk.height,
|
|
222
|
+
borderRadius: sk.borderRadius,
|
|
223
|
+
...sk.style,
|
|
224
|
+
// Apply captured container styles
|
|
225
|
+
// Ensure it doesn't hide children if z-index issues arise,
|
|
226
|
+
// but since we render in DOM order, children come later and sit on top.
|
|
227
|
+
boxSizing: "border-box"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
},
|
|
233
|
+
sk.key
|
|
234
|
+
))
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export { AutoSkeleton, SkeletonPrimitive };
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
244
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/SkeletonPrimitive.tsx","../src/AutoSkeleton.tsx"],"names":["node","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import React from \"react\";\nimport { motion, HTMLMotionProps } from \"framer-motion\";\n\nexport interface SkeletonPrimitiveProps extends HTMLMotionProps<\"div\"> {\n width?: string | number;\n height?: string | number;\n borderRadius?: string | number;\n color?: string;\n highlightColor?: string;\n}\n\nexport const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps> = ({\n width,\n height,\n borderRadius = \"4px\",\n color = \"var(--auto-skeleton-color, #eee)\",\n highlightColor = \"var(--auto-skeleton-highlight, #f5f5f5)\",\n style,\n ...props\n}) => {\n return (\n <motion.div\n style={{\n width,\n height,\n borderRadius,\n backgroundColor: color,\n ...style,\n }}\n animate={{\n backgroundColor: [color, highlightColor, color],\n }}\n transition={{\n duration: 1.5,\n repeat: Infinity,\n ease: \"easeInOut\",\n }}\n data-testid=\"skeleton\"\n {...props}\n />\n );\n};\n","import React, { useRef, useEffect, useState, ReactNode } from \"react\";\nimport { SkeletonPrimitive } from \"./SkeletonPrimitive\";\n\nexport interface AutoSkeletonProps {\n loading: boolean;\n children: ReactNode;\n config?: {\n color?: string;\n highlightColor?: string;\n borderRadius?: string | number;\n theme?: \"light\" | \"dark\" | \"auto\";\n // Theme-specific colors (override CSS variables)\n lightColor?: string;\n lightHighlightColor?: string;\n darkColor?: string;\n darkHighlightColor?: string;\n };\n}\n\nexport const AutoSkeleton: React.FC<AutoSkeletonProps> = ({\n loading,\n children,\n config,\n}) => {\n const theme = config?.theme || \"auto\";\n const containerRef = useRef<HTMLDivElement>(null);\n const [skeletons, setSkeletons] = useState<\n Array<{\n key: string;\n type: \"primitive\" | \"container\";\n width: number;\n height: number;\n top: number;\n left: number;\n borderRadius: string;\n style?: React.CSSProperties; // For container styles\n }>\n >([]);\n\n const updateSkeletons = () => {\n if (!containerRef.current) return;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const newSkeletons: typeof skeletons = [];\n\n // Helper to categorize node\n const getSkeletonType = (\n el: Element,\n ): {\n type: \"primitive\" | \"container\";\n styles?: React.CSSProperties;\n } | null => {\n const tagName = el.tagName.toLowerCase();\n const style = window.getComputedStyle(el);\n\n // 1. Atomic Content (always pulse)\n if ([\"img\", \"input\", \"button\", \"textarea\", \"select\"].includes(tagName)) {\n return { type: \"primitive\" };\n }\n\n // 2. Text Content (always pulse)\n const hasText = Array.from(el.childNodes).some(\n (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim(),\n );\n if (hasText) return { type: \"primitive\" };\n\n // 3. Structural Container (static clone)\n // If it has visible styles but no direct text => it's a container\n const hasBackground =\n style.backgroundColor !== \"rgba(0, 0, 0, 0)\" &&\n style.backgroundColor !== \"transparent\";\n const hasBorder =\n style.borderWidth !== \"0px\" &&\n style.borderStyle !== \"none\" &&\n style.borderColor !== \"rgba(0, 0, 0, 0)\" &&\n style.borderColor !== \"transparent\";\n const hasShadow = style.boxShadow !== \"none\";\n\n if (hasBackground || hasBorder || hasShadow) {\n return {\n type: \"container\",\n styles: {\n backgroundColor: style.backgroundColor,\n border: style.border,\n borderRadius: style.borderRadius,\n boxShadow: style.boxShadow,\n },\n };\n }\n\n return null;\n };\n\n // Traverse\n const walker = document.createTreeWalker(\n containerRef.current,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (node) => {\n const el = node as Element;\n const style = window.getComputedStyle(el);\n if (style.display === \"none\") return NodeFilter.FILTER_REJECT;\n\n // If it matches a type, accept it.\n // Note: We want to accept containers AND traverse their children.\n // FILTER_ACCEPT does both.\n if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;\n\n return NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n let node = walker.nextNode();\n let idx = 0;\n while (node) {\n const el = node as Element;\n const rect = el.getBoundingClientRect();\n const typeData = getSkeletonType(el);\n\n if (typeData) {\n // Determine border radius logic\n let borderRadius =\n config?.borderRadius ||\n typeData.styles?.borderRadius ||\n window.getComputedStyle(el).borderRadius;\n\n if (typeData.type === \"primitive\") {\n // Auto-detect circle for rounded primitives\n if (borderRadius === \"0px\" || !borderRadius) {\n if (\n window.getComputedStyle(el).borderRadius === \"50%\" ||\n (rect.width === rect.height && rect.width < 50)\n ) {\n // Keep heuristic or default\n }\n borderRadius = \"4px\";\n if (el.tagName.toLowerCase() === \"button\") borderRadius = \"6px\";\n if (el.tagName.toLowerCase() === \"img\") borderRadius = \"4px\";\n }\n }\n\n newSkeletons.push({\n key: `${idx++}`,\n type: typeData.type,\n width: rect.width,\n height: rect.height,\n top: rect.top - containerRect.top,\n left: rect.left - containerRect.left,\n borderRadius: borderRadius as string,\n style: typeData.styles,\n });\n }\n\n node = walker.nextNode();\n }\n\n // If no content nodes found, but container has size (maybe a canvas or simple div w/ background),\n // Fallback? Or maybe user wants the single block behavior.\n // Let's add that fallback if newSkeletons is empty but container has dimension.\n if (\n newSkeletons.length === 0 &&\n (containerRect.width > 0 || containerRect.height > 0)\n ) {\n newSkeletons.push({\n key: \"fallback\",\n type: \"primitive\",\n width: containerRect.width,\n height: containerRect.height,\n top: 0,\n left: 0,\n borderRadius: (config?.borderRadius as string) || \"4px\",\n });\n }\n\n setSkeletons(newSkeletons);\n };\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n // Initial measure\n updateSkeletons();\n\n const observer = new ResizeObserver(() => {\n updateSkeletons();\n });\n\n observer.observe(containerRef.current);\n // Also observe children mutations if possible, but ResizeObserver on container catches layout changes often.\n // For robust child changes:\n const mutationObserver = new MutationObserver(() => updateSkeletons());\n mutationObserver.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n mutationObserver.disconnect();\n };\n }, [loading, children]); // Re-scan when children change\n\n // Determine effective colors based on theme\n const getEffectiveColors = () => {\n // If user passed explicit color/highlightColor, use those\n if (config?.color || config?.highlightColor) {\n return {\n color: config.color,\n highlightColor: config.highlightColor,\n };\n }\n // If theme-specific colors are provided, use them\n if (theme === \"dark\" && (config?.darkColor || config?.darkHighlightColor)) {\n return {\n color: config.darkColor,\n highlightColor: config.darkHighlightColor,\n };\n }\n if (\n theme === \"light\" &&\n (config?.lightColor || config?.lightHighlightColor)\n ) {\n return {\n color: config.lightColor,\n highlightColor: config.lightHighlightColor,\n };\n }\n // Otherwise, let CSS variables handle it (undefined = use CSS var defaults)\n return { color: undefined, highlightColor: undefined };\n };\n\n const effectiveColors = getEffectiveColors();\n\n return (\n <div\n style={{ position: \"relative\", display: \"inline-block\", width: \"auto\" }}\n {...(theme !== \"auto\" ? { \"data-skeleton-theme\": theme } : {})}\n >\n <div\n ref={containerRef}\n style={{\n opacity: loading ? 0 : 1,\n transition: \"opacity 0.2s ease\", // Smooth transition when loading finishes\n }}\n aria-hidden={loading}\n >\n {children}\n </div>\n\n {loading && (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n }}\n >\n {skeletons.map((sk) => (\n <div\n key={sk.key}\n style={{ position: \"absolute\", top: sk.top, left: sk.left }}\n >\n {sk.type === \"primitive\" ? (\n <SkeletonPrimitive\n width={sk.width}\n height={sk.height}\n borderRadius={sk.borderRadius}\n color={effectiveColors.color}\n highlightColor={effectiveColors.highlightColor}\n />\n ) : (\n /* Container Clone: Renders static style */\n <div\n style={{\n width: sk.width,\n height: sk.height,\n borderRadius: sk.borderRadius,\n ...sk.style, // Apply captured container styles\n // Ensure it doesn't hide children if z-index issues arise,\n // but since we render in DOM order, children come later and sit on top.\n boxSizing: \"border-box\",\n }}\n />\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n};\n"]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* src/styles.css */
|
|
2
|
+
:root {
|
|
3
|
+
--auto-skeleton-color: #eee;
|
|
4
|
+
--auto-skeleton-highlight: #f5f5f5;
|
|
5
|
+
}
|
|
6
|
+
@media (prefers-color-scheme: dark) {
|
|
7
|
+
:root {
|
|
8
|
+
--auto-skeleton-color: #2a2a2a;
|
|
9
|
+
--auto-skeleton-highlight: #333;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
[data-skeleton-theme=light] {
|
|
13
|
+
--auto-skeleton-color: #eee;
|
|
14
|
+
--auto-skeleton-highlight: #f5f5f5;
|
|
15
|
+
}
|
|
16
|
+
[data-skeleton-theme=dark] {
|
|
17
|
+
--auto-skeleton-color: #2a2a2a;
|
|
18
|
+
--auto-skeleton-highlight: #333;
|
|
19
|
+
}
|
|
20
|
+
:root .dark {
|
|
21
|
+
--auto-skeleton-color: #2a2a2a;
|
|
22
|
+
--auto-skeleton-highlight: #333;
|
|
23
|
+
}
|
|
24
|
+
/*# sourceMappingURL=styles.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/styles.css"],"sourcesContent":[":root {\n --auto-skeleton-color: #eee;\n --auto-skeleton-highlight: #f5f5f5;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --auto-skeleton-color: #2a2a2a;\n --auto-skeleton-highlight: #333;\n }\n}\n\n[data-skeleton-theme=\"light\"] {\n --auto-skeleton-color: #eee;\n --auto-skeleton-highlight: #f5f5f5;\n}\n\n[data-skeleton-theme=\"dark\"] {\n --auto-skeleton-color: #2a2a2a;\n --auto-skeleton-highlight: #333;\n}\n\n/* Optional generic support if user uses .dark class on a parent */\n/* .dark [data-skeleton-theme] { } */\n\n/* To keep it simple, we let the data-skeleton-theme control itself, \n and the media query handles the auto case at :root level. \n If a user puts .dark on body, they might expect us to change.\n We can support that by hooking into standard dark selectors if we want.\n But typically libs just rely on CSS vars inherited. \n Since we define variables at :root, if the user redefines them in .dark, it works.\n \n Here, we forcefully set them for our defaults.\n*/\n\n:root .dark {\n --auto-skeleton-color: #2a2a2a;\n --auto-skeleton-highlight: #333;\n}\n"],"mappings":";AAAA;AACE,yBAAuB;AACvB,6BAA2B;AAC7B;AAEA,QAAO,sBAAuB;AAC5B;AACE,2BAAuB;AACvB,+BAA2B;AAC7B;AACF;AAEA,CAAC;AACC,yBAAuB;AACvB,6BAA2B;AAC7B;AAEA,CAAC;AACC,yBAAuB;AACvB,6BAA2B;AAC7B;AAeA,MAAM,CAAC;AACL,yBAAuB;AACvB,6BAA2B;AAC7B;","names":[]}
|
package/dist/styles.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-skeleton-loader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/DashnyamB/smart-skeleton-loader.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/DashnyamB/smart-skeleton-loader/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/DashnyamB/smart-skeleton-loader#readme",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.mjs",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"style": "./dist/styles.css",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.mjs",
|
|
24
|
+
"require": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./styles.css": "./dist/styles.css"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"test": "vitest",
|
|
34
|
+
"test:unit": "vitest --run",
|
|
35
|
+
"test:browser": "vitest --run --config vitest.storybook.config.ts",
|
|
36
|
+
"storybook": "storybook dev -p 6006",
|
|
37
|
+
"build-storybook": "storybook build"
|
|
38
|
+
},
|
|
39
|
+
"author": "dashnyamb",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"description": "",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"clsx": "^2.1.1",
|
|
44
|
+
"framer-motion": "^12.29.2",
|
|
45
|
+
"react": "^19.2.4",
|
|
46
|
+
"react-dom": "^19.2.4",
|
|
47
|
+
"tailwind-merge": "^3.4.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@chromatic-com/storybook": "^5.0.0",
|
|
51
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
52
|
+
"@semantic-release/git": "^10.0.1",
|
|
53
|
+
"@storybook/addon-a11y": "^10.2.1",
|
|
54
|
+
"@storybook/addon-docs": "^10.2.1",
|
|
55
|
+
"@storybook/addon-onboarding": "^10.2.1",
|
|
56
|
+
"@storybook/addon-vitest": "^10.2.1",
|
|
57
|
+
"@storybook/react-vite": "^10.2.1",
|
|
58
|
+
"@testing-library/dom": "^10.4.1",
|
|
59
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
60
|
+
"@testing-library/react": "^16.3.2",
|
|
61
|
+
"@types/node": "^25.2.0",
|
|
62
|
+
"@types/react": "^19.2.10",
|
|
63
|
+
"@types/react-dom": "^19.2.3",
|
|
64
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
65
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
66
|
+
"autoprefixer": "^10.4.23",
|
|
67
|
+
"jsdom": "^27.4.0",
|
|
68
|
+
"playwright": "^1.58.0",
|
|
69
|
+
"postcss": "^8.5.6",
|
|
70
|
+
"semantic-release": "^24.2.9",
|
|
71
|
+
"storybook": "^10.2.1",
|
|
72
|
+
"tailwindcss": "^4.1.18",
|
|
73
|
+
"tsup": "^8.5.1",
|
|
74
|
+
"typescript": "^5.9.3",
|
|
75
|
+
"vitest": "^4.0.18"
|
|
76
|
+
}
|
|
77
|
+
}
|