skeleton-ghost-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 +29 -0
- package/index.js +1 -0
- package/package.json +22 -0
- package/src/components/Skeleton.jsx +18 -0
- package/src/components/SkeletonItem.jsx +17 -0
- package/src/components/Skip.jsx +7 -0
- package/src/index.js +3 -0
- package/src/utils/replaceWithSkeleton.js +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# skeletonloader
|
|
2
|
+
|
|
3
|
+
A **Tailwind CSS–based, structure-preserving skeleton loader for React**.
|
|
4
|
+
|
|
5
|
+
`skeletonloader` automatically converts your existing JSX into skeleton placeholders **while keeping the exact layout and styling**, by reusing the same `className` (Tailwind utilities).
|
|
6
|
+
|
|
7
|
+
✅ No DOM measurement
|
|
8
|
+
✅ No layout shift
|
|
9
|
+
✅ SSR / Next.js safe
|
|
10
|
+
✅ Zero config
|
|
11
|
+
✅ Tailwind-native
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🧱 **Preserves structure** – DOM tree stays the same
|
|
18
|
+
- 🎨 **Uses Tailwind classes** – width, height, radius come from `className`
|
|
19
|
+
- 🌗 **Dark mode support** – works with `dark:` utilities
|
|
20
|
+
- ⏸ **Disable animation** – optional pulse control
|
|
21
|
+
- ⛔ **Skip skeleton** – exclude specific components
|
|
22
|
+
- ⚡ **Lightweight** – no extra dependencies
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install skeletonloader
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src";
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skeleton-ghost-loader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tailwind-based structure preserving skeleton loader for React",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react",
|
|
12
|
+
"skeleton",
|
|
13
|
+
"tailwind",
|
|
14
|
+
"loader",
|
|
15
|
+
"ui"
|
|
16
|
+
],
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=16.8",
|
|
20
|
+
"react-dom": ">=16.8"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { replaceWithSkeleton } from "../utils/replaceWithSkeleton";
|
|
3
|
+
import Skip from "./Skip";
|
|
4
|
+
|
|
5
|
+
const Skeleton = ({ loading, children, pulse = true }) => {
|
|
6
|
+
if (!loading) return children;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
{replaceWithSkeleton(children, { pulse })}
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Skeleton.Skip = Skip;
|
|
17
|
+
|
|
18
|
+
export default Skeleton;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const SkeletonItem = ({ className = "", pulse }) => {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
className={`
|
|
8
|
+
${className}
|
|
9
|
+
bg-gray-200 dark:bg-gray-700
|
|
10
|
+
rounded
|
|
11
|
+
${pulse ? "animate-pulse" : ""}
|
|
12
|
+
`}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default SkeletonItem;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import SkeletonItem from "../components/SkeletonItem";
|
|
3
|
+
|
|
4
|
+
export function replaceWithSkeleton(node, options) {
|
|
5
|
+
const { pulse } = options;
|
|
6
|
+
|
|
7
|
+
// Text node
|
|
8
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={`h-4 w-full rounded bg-gray-200 dark:bg-gray-700 ${
|
|
12
|
+
pulse ? "animate-pulse" : ""
|
|
13
|
+
}`}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!React.isValidElement(node)) return node;
|
|
19
|
+
|
|
20
|
+
// Skip skeleton
|
|
21
|
+
if (node.type?.displayName === "SkeletonSkip") {
|
|
22
|
+
return node.props.children;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { className = "", children } = node.props;
|
|
26
|
+
|
|
27
|
+
// Leaf node
|
|
28
|
+
if (!children) {
|
|
29
|
+
return (
|
|
30
|
+
<SkeletonItem
|
|
31
|
+
className={className}
|
|
32
|
+
pulse={pulse}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Container node
|
|
38
|
+
return React.cloneElement(node, {
|
|
39
|
+
className,
|
|
40
|
+
children: React.Children.map(children, (child) =>
|
|
41
|
+
replaceWithSkeleton(child, options)
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
}
|