react-ez-skeleton 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 +211 -0
- package/dist/index.cjs +165 -0
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +137 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# react-ez-skeleton
|
|
2
|
+
|
|
3
|
+
**Simple. Accessible. Zero-config React skeletons.**
|
|
4
|
+
|
|
5
|
+
`react-ez-skeleton` is a lightweight React skeleton loader library focused on excellent developer experience (DX). It works out of the box, is SSR-safe, respects accessibility and motion preferences, and is easy to theme using CSS variables.
|
|
6
|
+
|
|
7
|
+
No CSS imports. No heavy dependencies. No surprises.
|
|
8
|
+
|
|
9
|
+
## Why react-ez-skeleton?
|
|
10
|
+
|
|
11
|
+
- **Zero configuration**: works immediately
|
|
12
|
+
- **SSR & hydration safe**: Next.js, Remix, etc.
|
|
13
|
+
- **Accessible by default**: `aria-hidden`
|
|
14
|
+
- **Reduced-motion friendly**: respects `prefers-reduced-motion`
|
|
15
|
+
- **Themeable**: via CSS variables
|
|
16
|
+
- **Test-friendly**: `dataTestId` -> `data-testid`
|
|
17
|
+
- **Tiny API**: no lock-in
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install react-ez-skeleton
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
or
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add react-ez-skeleton
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Skeleton } from "react-ez-skeleton";
|
|
35
|
+
|
|
36
|
+
export function CardLoading() {
|
|
37
|
+
return <Skeleton width={200} height={20} />;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That’s it.
|
|
42
|
+
|
|
43
|
+
- No CSS imports
|
|
44
|
+
- No providers
|
|
45
|
+
- No setup
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
### `Skeleton`
|
|
50
|
+
|
|
51
|
+
The base building block.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<Skeleton width={120} height={16} />
|
|
55
|
+
<Skeleton width="100%" height={24} radius={8} />
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Props
|
|
59
|
+
|
|
60
|
+
| Prop | Type | Default | Description |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `width` | `number \| string` | `"100%"` | Width (px, %, etc.) |
|
|
63
|
+
| `height` | `number \| string` | `16` | Height |
|
|
64
|
+
| `radius` | `number \| string` | `4` | Border radius |
|
|
65
|
+
| `animate` | `boolean` | `true` | Enable shimmer animation |
|
|
66
|
+
| `respectReducedMotion` | `boolean` | `true` | Respects OS motion settings |
|
|
67
|
+
| `ariaHidden` | `boolean` | `true` | Decorative by default |
|
|
68
|
+
| `injectStyles` | `boolean` | `true` | Auto-inject required styles |
|
|
69
|
+
| `dataTestId` | `string` | `—` | Adds `data-testid` |
|
|
70
|
+
| `className` | `string` | `—` | Custom class |
|
|
71
|
+
| `style` | `React.CSSProperties` | `—` | Inline styles |
|
|
72
|
+
|
|
73
|
+
### `SkeletonText`
|
|
74
|
+
|
|
75
|
+
Perfect for paragraphs and content blocks.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<SkeletonText lines={3} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Advanced usage:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<SkeletonText
|
|
85
|
+
lines={4}
|
|
86
|
+
randomizeLineWidths
|
|
87
|
+
randomizeMin={60}
|
|
88
|
+
randomizeMax={95}
|
|
89
|
+
dataTestId="post-skeleton"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Props (in addition to `SkeletonProps`)
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Default |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `lines` | `number` | `3` |
|
|
98
|
+
| `lineHeight` | `number \| string` | `16` |
|
|
99
|
+
| `gap` | `number \| string` | `8` |
|
|
100
|
+
| `lineWidths` | `(number \| string)[]` | `—` |
|
|
101
|
+
| `randomizeLineWidths` | `boolean` | `false` |
|
|
102
|
+
| `randomizeMin` | `number` | `60` |
|
|
103
|
+
| `randomizeMax` | `number` | `100` |
|
|
104
|
+
| `randomizeSeed` | `number` | `—` |
|
|
105
|
+
|
|
106
|
+
Seeded randomness means deterministic layouts (nice for SSR and tests).
|
|
107
|
+
|
|
108
|
+
### `SkeletonCircle`
|
|
109
|
+
|
|
110
|
+
Ideal for avatars and icons.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<SkeletonCircle size={40} />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Theming (DX Highlight)
|
|
117
|
+
|
|
118
|
+
No theme providers. No props explosion.
|
|
119
|
+
|
|
120
|
+
Just CSS variables:
|
|
121
|
+
|
|
122
|
+
```css
|
|
123
|
+
:root {
|
|
124
|
+
--ez-skeleton-color-start: #2a2a2a;
|
|
125
|
+
--ez-skeleton-color-middle: #3a3a3a;
|
|
126
|
+
--ez-skeleton-color-end: #2a2a2a;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Works automatically across your app.
|
|
131
|
+
Perfect for dark mode and design systems.
|
|
132
|
+
|
|
133
|
+
## Accessibility & Motion
|
|
134
|
+
|
|
135
|
+
- Skeletons are decorative by default
|
|
136
|
+
- Automatically hidden from screen readers
|
|
137
|
+
- Honors `prefers-reduced-motion`
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Skeleton animate={false} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Testing Support
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<Skeleton dataTestId="profile-loading" />
|
|
147
|
+
|
|
148
|
+
expect(screen.getByTestId("profile-loading")).toBeInTheDocument();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`SkeletonText` automatically appends line indexes:
|
|
152
|
+
|
|
153
|
+
- `profile-loading-0`
|
|
154
|
+
- `profile-loading-1`
|
|
155
|
+
|
|
156
|
+
## SSR & Framework Support
|
|
157
|
+
|
|
158
|
+
Fully safe for:
|
|
159
|
+
|
|
160
|
+
- Next.js (App Router & Pages)
|
|
161
|
+
- Remix
|
|
162
|
+
- Vite SSR
|
|
163
|
+
- Astro
|
|
164
|
+
- CRA
|
|
165
|
+
|
|
166
|
+
Style injection is:
|
|
167
|
+
|
|
168
|
+
- Singleton-based
|
|
169
|
+
- Guarded
|
|
170
|
+
- Client-only
|
|
171
|
+
- No hydration mismatches
|
|
172
|
+
|
|
173
|
+
## Opt-out Style Injection
|
|
174
|
+
|
|
175
|
+
If you want full control:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<Skeleton injectStyles={false} />
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Then provide your own CSS:
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
@keyframes react-ez-skeleton-pulse {
|
|
185
|
+
from {
|
|
186
|
+
background-position: 100% 50%;
|
|
187
|
+
}
|
|
188
|
+
to {
|
|
189
|
+
background-position: 0 50%;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Bundle Philosophy
|
|
195
|
+
|
|
196
|
+
- No runtime dependencies
|
|
197
|
+
- Tree-shakable
|
|
198
|
+
- Side-effect safe
|
|
199
|
+
- Dual ESM + CJS builds
|
|
200
|
+
- TypeScript first
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
PRs and issues are welcome.
|
|
205
|
+
Please keep changes DX-focused and minimal.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT © Md. Shafiul Alam
|
|
210
|
+
|
|
211
|
+
Final note: `react-ez-skeleton` is designed for developers who care about correctness, accessibility, and simplicity — without sacrificing control.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Skeleton: () => Skeleton,
|
|
24
|
+
SkeletonCircle: () => SkeletonCircle,
|
|
25
|
+
SkeletonText: () => SkeletonText,
|
|
26
|
+
default: () => index_default,
|
|
27
|
+
injectSkeletonStyles: () => injectSkeletonStyles
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
var stylesInjected = false;
|
|
32
|
+
var toCssSize = (value) => {
|
|
33
|
+
if (value === void 0) return void 0;
|
|
34
|
+
if (typeof value === "number") return `${value}px`;
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
var Skeleton = ({
|
|
38
|
+
width = "100%",
|
|
39
|
+
height = 16,
|
|
40
|
+
radius = 4,
|
|
41
|
+
className = "",
|
|
42
|
+
style,
|
|
43
|
+
animate = true,
|
|
44
|
+
respectReducedMotion = true,
|
|
45
|
+
ariaHidden = true,
|
|
46
|
+
injectStyles = true,
|
|
47
|
+
dataTestId
|
|
48
|
+
}) => {
|
|
49
|
+
if (injectStyles) injectSkeletonStyles();
|
|
50
|
+
const prefersReducedMotion = respectReducedMotion && typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
51
|
+
const inlineStyle = {
|
|
52
|
+
display: "inline-block",
|
|
53
|
+
background: "linear-gradient(90deg, var(--ez-skeleton-color-start, #f2f2f2) 25%, var(--ez-skeleton-color-middle, #e6e6e6) 37%, var(--ez-skeleton-color-end, #f2f2f2) 63%)",
|
|
54
|
+
backgroundSize: "400% 100%",
|
|
55
|
+
animation: animate && !prefersReducedMotion ? "var(--ez-skeleton-animation, react-ez-skeleton-pulse 1.4s ease-in-out infinite)" : "none",
|
|
56
|
+
borderRadius: toCssSize(radius),
|
|
57
|
+
width: toCssSize(width),
|
|
58
|
+
height: toCssSize(height),
|
|
59
|
+
...style
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
62
|
+
"span",
|
|
63
|
+
{
|
|
64
|
+
className,
|
|
65
|
+
style: inlineStyle,
|
|
66
|
+
"aria-hidden": ariaHidden,
|
|
67
|
+
"data-testid": dataTestId
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
var injectSkeletonStyles = () => {
|
|
72
|
+
if (stylesInjected) return;
|
|
73
|
+
if (typeof document === "undefined") return;
|
|
74
|
+
const id = "react-ez-skeleton-keyframes";
|
|
75
|
+
if (document.getElementById(id)) {
|
|
76
|
+
stylesInjected = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const style = document.createElement("style");
|
|
80
|
+
style.id = id;
|
|
81
|
+
style.innerHTML = `:root{--ez-skeleton-color-start:#f2f2f2;--ez-skeleton-color-middle:#e6e6e6;--ez-skeleton-color-end:#f2f2f2;--ez-skeleton-animation:react-ez-skeleton-pulse 1.4s ease-in-out infinite;}@media (prefers-reduced-motion: reduce){:root{--ez-skeleton-animation:none;}}@keyframes react-ez-skeleton-pulse{0%{background-position:100% 50%;}100%{background-position:0 50%;}}`;
|
|
82
|
+
document.head.appendChild(style);
|
|
83
|
+
stylesInjected = true;
|
|
84
|
+
};
|
|
85
|
+
var SkeletonText = ({
|
|
86
|
+
lines = 3,
|
|
87
|
+
lineHeight = 16,
|
|
88
|
+
gap = 8,
|
|
89
|
+
width = "100%",
|
|
90
|
+
radius = 4,
|
|
91
|
+
className = "",
|
|
92
|
+
style,
|
|
93
|
+
lineWidths,
|
|
94
|
+
randomizeLineWidths = false,
|
|
95
|
+
randomizeMin = 60,
|
|
96
|
+
randomizeMax = 100,
|
|
97
|
+
randomizeSeed,
|
|
98
|
+
animate,
|
|
99
|
+
respectReducedMotion,
|
|
100
|
+
ariaHidden,
|
|
101
|
+
injectStyles,
|
|
102
|
+
dataTestId
|
|
103
|
+
}) => {
|
|
104
|
+
const clamp = (n, min2, max2) => Math.min(max2, Math.max(min2, n));
|
|
105
|
+
const createRng = (seed) => {
|
|
106
|
+
let s = seed >>> 0;
|
|
107
|
+
return () => {
|
|
108
|
+
s = 1664525 * s + 1013904223 >>> 0;
|
|
109
|
+
return s / 4294967296;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const rng = randomizeLineWidths ? createRng(randomizeSeed != null ? randomizeSeed : lines) : null;
|
|
113
|
+
const min = clamp(randomizeMin, 0, 100);
|
|
114
|
+
const max = clamp(randomizeMax, min, 100);
|
|
115
|
+
const getLineWidth = (index) => {
|
|
116
|
+
if (lineWidths && lineWidths[index] !== void 0) return lineWidths[index];
|
|
117
|
+
if (randomizeLineWidths && rng) {
|
|
118
|
+
const p = min + (max - min) * rng();
|
|
119
|
+
return `${Math.round(p)}%`;
|
|
120
|
+
}
|
|
121
|
+
if (index === lines - 1) return width;
|
|
122
|
+
return "100%";
|
|
123
|
+
};
|
|
124
|
+
const items = Array.from({ length: lines });
|
|
125
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style, children: items.map((_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
126
|
+
Skeleton,
|
|
127
|
+
{
|
|
128
|
+
width: getLineWidth(index),
|
|
129
|
+
height: lineHeight,
|
|
130
|
+
radius,
|
|
131
|
+
animate,
|
|
132
|
+
respectReducedMotion,
|
|
133
|
+
ariaHidden,
|
|
134
|
+
injectStyles,
|
|
135
|
+
dataTestId: dataTestId ? `${dataTestId}-${index}` : void 0,
|
|
136
|
+
style: { marginBottom: index === lines - 1 ? 0 : toCssSize(gap) }
|
|
137
|
+
},
|
|
138
|
+
index
|
|
139
|
+
)) });
|
|
140
|
+
};
|
|
141
|
+
var SkeletonCircle = ({ size = 40, className = "", style, animate, respectReducedMotion, ariaHidden, injectStyles, dataTestId }) => {
|
|
142
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
143
|
+
Skeleton,
|
|
144
|
+
{
|
|
145
|
+
width: size,
|
|
146
|
+
height: size,
|
|
147
|
+
radius: "50%",
|
|
148
|
+
className,
|
|
149
|
+
style,
|
|
150
|
+
animate,
|
|
151
|
+
respectReducedMotion,
|
|
152
|
+
ariaHidden,
|
|
153
|
+
injectStyles,
|
|
154
|
+
dataTestId
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
var index_default = Skeleton;
|
|
159
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
160
|
+
0 && (module.exports = {
|
|
161
|
+
Skeleton,
|
|
162
|
+
SkeletonCircle,
|
|
163
|
+
SkeletonText,
|
|
164
|
+
injectSkeletonStyles
|
|
165
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type SkeletonProps = {
|
|
4
|
+
width?: number | string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
radius?: number | string;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
animate?: boolean;
|
|
10
|
+
respectReducedMotion?: boolean;
|
|
11
|
+
ariaHidden?: boolean;
|
|
12
|
+
injectStyles?: boolean;
|
|
13
|
+
dataTestId?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const Skeleton: React.FC<SkeletonProps>;
|
|
16
|
+
declare const injectSkeletonStyles: () => void;
|
|
17
|
+
declare const SkeletonText: React.FC<Omit<SkeletonProps, "height"> & {
|
|
18
|
+
lines?: number;
|
|
19
|
+
lineHeight?: number | string;
|
|
20
|
+
gap?: number | string;
|
|
21
|
+
lineWidths?: Array<number | string>;
|
|
22
|
+
randomizeLineWidths?: boolean;
|
|
23
|
+
randomizeMin?: number;
|
|
24
|
+
randomizeMax?: number;
|
|
25
|
+
randomizeSeed?: number;
|
|
26
|
+
}>;
|
|
27
|
+
declare const SkeletonCircle: React.FC<Omit<SkeletonProps, "radius" | "height" | "width"> & {
|
|
28
|
+
size?: number | string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export { Skeleton, SkeletonCircle, type SkeletonProps, SkeletonText, Skeleton as default, injectSkeletonStyles };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type SkeletonProps = {
|
|
4
|
+
width?: number | string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
radius?: number | string;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
animate?: boolean;
|
|
10
|
+
respectReducedMotion?: boolean;
|
|
11
|
+
ariaHidden?: boolean;
|
|
12
|
+
injectStyles?: boolean;
|
|
13
|
+
dataTestId?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const Skeleton: React.FC<SkeletonProps>;
|
|
16
|
+
declare const injectSkeletonStyles: () => void;
|
|
17
|
+
declare const SkeletonText: React.FC<Omit<SkeletonProps, "height"> & {
|
|
18
|
+
lines?: number;
|
|
19
|
+
lineHeight?: number | string;
|
|
20
|
+
gap?: number | string;
|
|
21
|
+
lineWidths?: Array<number | string>;
|
|
22
|
+
randomizeLineWidths?: boolean;
|
|
23
|
+
randomizeMin?: number;
|
|
24
|
+
randomizeMax?: number;
|
|
25
|
+
randomizeSeed?: number;
|
|
26
|
+
}>;
|
|
27
|
+
declare const SkeletonCircle: React.FC<Omit<SkeletonProps, "radius" | "height" | "width"> & {
|
|
28
|
+
size?: number | string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export { Skeleton, SkeletonCircle, type SkeletonProps, SkeletonText, Skeleton as default, injectSkeletonStyles };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/index.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
var stylesInjected = false;
|
|
4
|
+
var toCssSize = (value) => {
|
|
5
|
+
if (value === void 0) return void 0;
|
|
6
|
+
if (typeof value === "number") return `${value}px`;
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
var Skeleton = ({
|
|
10
|
+
width = "100%",
|
|
11
|
+
height = 16,
|
|
12
|
+
radius = 4,
|
|
13
|
+
className = "",
|
|
14
|
+
style,
|
|
15
|
+
animate = true,
|
|
16
|
+
respectReducedMotion = true,
|
|
17
|
+
ariaHidden = true,
|
|
18
|
+
injectStyles = true,
|
|
19
|
+
dataTestId
|
|
20
|
+
}) => {
|
|
21
|
+
if (injectStyles) injectSkeletonStyles();
|
|
22
|
+
const prefersReducedMotion = respectReducedMotion && typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
23
|
+
const inlineStyle = {
|
|
24
|
+
display: "inline-block",
|
|
25
|
+
background: "linear-gradient(90deg, var(--ez-skeleton-color-start, #f2f2f2) 25%, var(--ez-skeleton-color-middle, #e6e6e6) 37%, var(--ez-skeleton-color-end, #f2f2f2) 63%)",
|
|
26
|
+
backgroundSize: "400% 100%",
|
|
27
|
+
animation: animate && !prefersReducedMotion ? "var(--ez-skeleton-animation, react-ez-skeleton-pulse 1.4s ease-in-out infinite)" : "none",
|
|
28
|
+
borderRadius: toCssSize(radius),
|
|
29
|
+
width: toCssSize(width),
|
|
30
|
+
height: toCssSize(height),
|
|
31
|
+
...style
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsx(
|
|
34
|
+
"span",
|
|
35
|
+
{
|
|
36
|
+
className,
|
|
37
|
+
style: inlineStyle,
|
|
38
|
+
"aria-hidden": ariaHidden,
|
|
39
|
+
"data-testid": dataTestId
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
var injectSkeletonStyles = () => {
|
|
44
|
+
if (stylesInjected) return;
|
|
45
|
+
if (typeof document === "undefined") return;
|
|
46
|
+
const id = "react-ez-skeleton-keyframes";
|
|
47
|
+
if (document.getElementById(id)) {
|
|
48
|
+
stylesInjected = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const style = document.createElement("style");
|
|
52
|
+
style.id = id;
|
|
53
|
+
style.innerHTML = `:root{--ez-skeleton-color-start:#f2f2f2;--ez-skeleton-color-middle:#e6e6e6;--ez-skeleton-color-end:#f2f2f2;--ez-skeleton-animation:react-ez-skeleton-pulse 1.4s ease-in-out infinite;}@media (prefers-reduced-motion: reduce){:root{--ez-skeleton-animation:none;}}@keyframes react-ez-skeleton-pulse{0%{background-position:100% 50%;}100%{background-position:0 50%;}}`;
|
|
54
|
+
document.head.appendChild(style);
|
|
55
|
+
stylesInjected = true;
|
|
56
|
+
};
|
|
57
|
+
var SkeletonText = ({
|
|
58
|
+
lines = 3,
|
|
59
|
+
lineHeight = 16,
|
|
60
|
+
gap = 8,
|
|
61
|
+
width = "100%",
|
|
62
|
+
radius = 4,
|
|
63
|
+
className = "",
|
|
64
|
+
style,
|
|
65
|
+
lineWidths,
|
|
66
|
+
randomizeLineWidths = false,
|
|
67
|
+
randomizeMin = 60,
|
|
68
|
+
randomizeMax = 100,
|
|
69
|
+
randomizeSeed,
|
|
70
|
+
animate,
|
|
71
|
+
respectReducedMotion,
|
|
72
|
+
ariaHidden,
|
|
73
|
+
injectStyles,
|
|
74
|
+
dataTestId
|
|
75
|
+
}) => {
|
|
76
|
+
const clamp = (n, min2, max2) => Math.min(max2, Math.max(min2, n));
|
|
77
|
+
const createRng = (seed) => {
|
|
78
|
+
let s = seed >>> 0;
|
|
79
|
+
return () => {
|
|
80
|
+
s = 1664525 * s + 1013904223 >>> 0;
|
|
81
|
+
return s / 4294967296;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const rng = randomizeLineWidths ? createRng(randomizeSeed != null ? randomizeSeed : lines) : null;
|
|
85
|
+
const min = clamp(randomizeMin, 0, 100);
|
|
86
|
+
const max = clamp(randomizeMax, min, 100);
|
|
87
|
+
const getLineWidth = (index) => {
|
|
88
|
+
if (lineWidths && lineWidths[index] !== void 0) return lineWidths[index];
|
|
89
|
+
if (randomizeLineWidths && rng) {
|
|
90
|
+
const p = min + (max - min) * rng();
|
|
91
|
+
return `${Math.round(p)}%`;
|
|
92
|
+
}
|
|
93
|
+
if (index === lines - 1) return width;
|
|
94
|
+
return "100%";
|
|
95
|
+
};
|
|
96
|
+
const items = Array.from({ length: lines });
|
|
97
|
+
return /* @__PURE__ */ jsx("div", { className, style, children: items.map((_, index) => /* @__PURE__ */ jsx(
|
|
98
|
+
Skeleton,
|
|
99
|
+
{
|
|
100
|
+
width: getLineWidth(index),
|
|
101
|
+
height: lineHeight,
|
|
102
|
+
radius,
|
|
103
|
+
animate,
|
|
104
|
+
respectReducedMotion,
|
|
105
|
+
ariaHidden,
|
|
106
|
+
injectStyles,
|
|
107
|
+
dataTestId: dataTestId ? `${dataTestId}-${index}` : void 0,
|
|
108
|
+
style: { marginBottom: index === lines - 1 ? 0 : toCssSize(gap) }
|
|
109
|
+
},
|
|
110
|
+
index
|
|
111
|
+
)) });
|
|
112
|
+
};
|
|
113
|
+
var SkeletonCircle = ({ size = 40, className = "", style, animate, respectReducedMotion, ariaHidden, injectStyles, dataTestId }) => {
|
|
114
|
+
return /* @__PURE__ */ jsx(
|
|
115
|
+
Skeleton,
|
|
116
|
+
{
|
|
117
|
+
width: size,
|
|
118
|
+
height: size,
|
|
119
|
+
radius: "50%",
|
|
120
|
+
className,
|
|
121
|
+
style,
|
|
122
|
+
animate,
|
|
123
|
+
respectReducedMotion,
|
|
124
|
+
ariaHidden,
|
|
125
|
+
injectStyles,
|
|
126
|
+
dataTestId
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
var index_default = Skeleton;
|
|
131
|
+
export {
|
|
132
|
+
Skeleton,
|
|
133
|
+
SkeletonCircle,
|
|
134
|
+
SkeletonText,
|
|
135
|
+
index_default as default,
|
|
136
|
+
injectSkeletonStyles
|
|
137
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-ez-skeleton",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "react-ez-skeleton provides simple, flexible skeleton components for React applications, enabling fast setup and smooth loading states without unnecessary complexity.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Md. Shafiul Alam",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.tsx --format cjs,esm --dts --clean",
|
|
24
|
+
"dev": "tsup src/index.tsx --format esm --dts --watch",
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
29
|
+
"react-dom": "^17.0.0 || ^18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.0.0",
|
|
33
|
+
"@types/react-dom": "^18.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|