@tavosud/sky-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 ADDED
@@ -0,0 +1,357 @@
1
+ # @tavosud/sky-skeleton
2
+
3
+ Lightweight, zero-dependency React skeleton loader with a GPU-accelerated CSS shimmer. Built for performance — under **1.3 KB** (ESM, minified), zero repaints.
4
+
5
+ [![Tests](https://img.shields.io/badge/tests-33%20passed-brightgreen)](#testing)
6
+ [![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20coffee-Ko--fi-FF5E5B?logo=ko-fi&logoColor=white)](https://ko-fi.com/tavosud)
7
+
8
+ ## Features
9
+
10
+ - **Zero runtime dependencies** — React is a peer dependency
11
+ - **GPU-accelerated shimmer** — uses `transform: translateX()` on a pseudo-element; zero CPU repaints
12
+ - **Pulse effect** — alternative `effect="pulse"` fades the skeleton in and out
13
+ - **CSS custom properties** — colors, speed, and border-radius are fully themeable without JavaScript
14
+ - **Dark mode** — automatic via `prefers-color-scheme` CSS media query
15
+ - **`SkeletonTheme`** — set defaults once for an entire subtree with `display: contents` (layout-safe)
16
+ - **Three variants** — `rect`, `circle`, `text` with multi-line paragraph support via `count`
17
+ - **Staggered animation** — `stagger` prop creates a wave delay across `text` lines
18
+ - **Circle auto-square** — omit `height` on `variant="circle"` and it mirrors `width` automatically
19
+ - **Off-screen pause** — `IntersectionObserver` pauses the animation when the skeleton is not visible
20
+ - **`React.forwardRef`** — full ref forwarding to the underlying DOM element
21
+ - **Semantic HTML** — render any element with the `as` prop
22
+ - **Reduced-motion aware** — respects `prefers-reduced-motion` natively via CSS media query
23
+ - **Accessible** — `role="progressbar"`, `aria-busy`, and a customisable `aria-label` out of the box
24
+ - **Full TypeScript API** — every prop documented with JSDoc for rich IDE autocompletion
25
+ - **33 unit tests** — Vitest + Testing Library
26
+ - **Tree-shakeable** — ESM build with `sideEffects: false`
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @tavosud/sky-skeleton
32
+ # or
33
+ yarn add @tavosud/sky-skeleton
34
+ # or
35
+ pnpm add @tavosud/sky-skeleton
36
+ ```
37
+
38
+ React 17 or later is required as a peer dependency.
39
+
40
+ ## Usage
41
+
42
+ ```tsx
43
+ import { Skeleton } from '@tavosud/sky-skeleton';
44
+ ```
45
+
46
+ ### Rectangle (default)
47
+
48
+ Use to represent images, banners, or any block-level element.
49
+
50
+ ```tsx
51
+ <Skeleton loading={true} width={320} height={180} />
52
+ ```
53
+
54
+ ### Circle
55
+
56
+ Use to represent avatars or profile pictures. When you only provide `width`, the component automatically sets `height` to the same value so you don't have to repeat yourself.
57
+
58
+ ```tsx
59
+ // Auto-square: height mirrors width automatically
60
+ <Skeleton loading={true} variant="circle" width={48} />
61
+
62
+ // Explicit height overrides auto-square when needed
63
+ <Skeleton loading={true} variant="circle" width={64} height={32} />
64
+ ```
65
+
66
+ ### Text lines
67
+
68
+ Use to represent paragraphs. `count` controls how many lines are rendered — the last one is automatically narrowed to 80% width to mimic a real paragraph ending.
69
+
70
+ ```tsx
71
+ // Single line
72
+ <Skeleton loading={true} variant="text" />
73
+
74
+ // Full paragraph (4 lines)
75
+ <Skeleton loading={true} variant="text" count={4} />
76
+
77
+ // Staggered "wave" — each line starts 80 ms after the previous
78
+ <Skeleton loading={true} variant="text" count={4} stagger={0.08} />
79
+ ```
80
+
81
+ ### Conditional rendering with children
82
+
83
+ When `loading` is `false`, the component renders its `children` instead of the skeleton.
84
+
85
+ ```tsx
86
+ function UserCard({ user, isLoading }) {
87
+ return (
88
+ <div>
89
+ <Skeleton loading={isLoading} variant="circle" width={48} height={48}>
90
+ <img src={user.avatar} alt={user.name} />
91
+ </Skeleton>
92
+
93
+ <Skeleton loading={isLoading} variant="text" count={2}>
94
+ <p>{user.name}</p>
95
+ <p>{user.bio}</p>
96
+ </Skeleton>
97
+ </div>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Custom styles via `className`
103
+
104
+ ```tsx
105
+ <Skeleton loading={true} variant="rect" className="my-custom-skeleton" />
106
+ ```
107
+
108
+ ```css
109
+ /* your-styles.css */
110
+ .my-custom-skeleton {
111
+ border-radius: 12px;
112
+ width: 100%;
113
+ height: 200px;
114
+ }
115
+ ```
116
+
117
+ ### Inline styles via `style`
118
+
119
+ For one-off overrides without a CSS class:
120
+
121
+ ```tsx
122
+ <Skeleton loading={true} style={{ borderRadius: '12px', width: '100%', height: 200 }} />
123
+ ```
124
+
125
+ ### Effect — shimmer vs pulse
126
+
127
+ Choose between two built-in animation styles:
128
+
129
+ ```tsx
130
+ // Default: a highlight sweeps across the surface (GPU-accelerated)
131
+ <Skeleton effect="shimmer" width={300} height={20} />
132
+
133
+ // Alternative: the element gently fades in and out
134
+ <Skeleton effect="pulse" width={300} height={20} />
135
+ ```
136
+
137
+ You can also set a default effect for the entire tree via `SkeletonTheme`:
138
+
139
+ ```tsx
140
+ <SkeletonTheme effect="pulse">
141
+ <Skeleton />
142
+ </SkeletonTheme>
143
+ ```
144
+
145
+ ### Disable animation
146
+
147
+ Pass `animate={false}` to render a static placeholder — useful in tests or when you need to manage motion yourself:
148
+
149
+ ```tsx
150
+ <Skeleton loading={true} variant="rect" animate={false} />
151
+ ```
152
+
153
+ > **Note:** the component also reads the OS-level `prefers-reduced-motion` setting via a CSS media query and disables the animation automatically — no `animate` prop needed. Additionally, skeletons that scroll out of the viewport have their animation automatically paused via `IntersectionObserver` to save GPU cycles.
154
+
155
+ ### Semantic HTML via `as`
156
+
157
+ By default the skeleton renders a `<span>`. Use `as` to render the semantically correct element:
158
+
159
+ ```tsx
160
+ <Skeleton as="div" height={200} /> {/* block container */}
161
+ <Skeleton as="p" variant="text" /> {/* paragraph */}
162
+ <Skeleton as="li" variant="text" count={3} /> {/* list items */}
163
+ ```
164
+
165
+ ### Control animation speed
166
+
167
+ ```tsx
168
+ <Skeleton speed={2.5} /> {/* slower */}
169
+ <Skeleton speed={0.8} /> {/* faster */}
170
+ ```
171
+
172
+ ### borderRadius per instance
173
+
174
+ ```tsx
175
+ <Skeleton borderRadius={12} height={180} /> {/* number → px */}
176
+ <Skeleton borderRadius="1rem" height={180} /> {/* or string */}
177
+ ```
178
+
179
+ ### Custom accessible label
180
+
181
+ ```tsx
182
+ <Skeleton loading={true} variant="circle" width={48} aria-label="Loading user avatar" />
183
+ ```
184
+
185
+ ### Access the DOM element via ref
186
+
187
+ `Skeleton` is a `forwardRef` component — you can attach a `ref` to reach the underlying DOM node:
188
+
189
+ ```tsx
190
+ const ref = useRef<HTMLElement>(null);
191
+
192
+ <Skeleton ref={ref} width={300} height={20} />
193
+ ```
194
+
195
+ When `variant="text"` with multiple lines, the ref is forwarded to the **first** line.
196
+
197
+ ## SkeletonTheme
198
+
199
+ Wrap your app or a section in `<SkeletonTheme>` to set defaults for all `<Skeleton>` children — no need to repeat props on every instance.
200
+
201
+ ```tsx
202
+ import { Skeleton, SkeletonTheme } from '@tavosud/sky-skeleton';
203
+
204
+ <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f0f0f0" speed={1.8} borderRadius={8}>
205
+ <Skeleton height={120} />
206
+ <Skeleton variant="circle" width={48} height={48} />
207
+ <Skeleton variant="text" count={3} />
208
+ </SkeletonTheme>
209
+ ```
210
+
211
+ `SkeletonTheme` injects CSS custom properties using a `display: contents` wrapper, so the theming has **zero JavaScript overhead per render** and **never breaks flex, grid, or any other layout context** — children see through it completely.
212
+
213
+ ### Dark mode with SkeletonTheme
214
+
215
+ You can provide separate light/dark themes:
216
+
217
+ ```tsx
218
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
219
+
220
+ <SkeletonTheme
221
+ baseColor={isDark ? '#2a2a2a' : '#eee'}
222
+ highlightColor={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.6)'}
223
+ >
224
+ <App />
225
+ </SkeletonTheme>
226
+ ```
227
+
228
+ Or rely on the **automatic CSS dark mode** built into the library (no JavaScript needed — just set a CSS variable in your dark theme):
229
+
230
+ ```css
231
+ @media (prefers-color-scheme: dark) {
232
+ :root {
233
+ --skeleton-base-color: #2a2a2a;
234
+ --skeleton-highlight-color: rgba(255, 255, 255, 0.08);
235
+ }
236
+ }
237
+ ```
238
+
239
+ ### SkeletonTheme Props
240
+
241
+ | Prop | Type | Default | Description |
242
+ |---|---|---|---|
243
+ | `baseColor` | `string` | `#eee` | Background color of all skeletons in the subtree. |
244
+ | `highlightColor` | `string` | `rgba(255,255,255,0.6)` | Shimmer highlight color. |
245
+ | `speed` | `number` | `1.4` | Animation duration in seconds. |
246
+ | `borderRadius` | `string \| number` | `4` | Border radius. Numbers treated as `px`. |
247
+ | `animate` | `boolean` | `true` | Disable animation for all children. |
248
+ | `effect` | `'shimmer' \| 'pulse'` | `'shimmer'` | Default animation effect for all children. |
249
+ | `variant` | `SkeletonVariant` | `'rect'` | Default variant for all children. |
250
+
251
+ ## API
252
+
253
+ | Prop | Type | Default | Description |
254
+ | ------------- | ------------------------------ | -------------- | --------------------------------------------------------------------------- |
255
+ | `loading` | `boolean` | `true` | When `true`, renders the skeleton; `false` renders children. |
256
+ | `variant` | `'rect' \| 'circle' \| 'text'` | `'rect'` | Shape of the skeleton element. |
257
+ | `as` | `ElementType` | `'span'` | HTML element or React component to render as the skeleton root. |
258
+ | `width` | `string \| number` | — | Custom width. Numbers are treated as `px`. |
259
+ | `height` | `string \| number` | — | Custom height. For `circle`, omitting this mirrors `width` automatically. |
260
+ | `borderRadius`| `string \| number` | `4` | Border radius. Numbers treated as `px`. Overrides CSS variable. |
261
+ | `count` | `number` | `1` | Number of lines to render. Only applies to `variant="text"`. |
262
+ | `speed` | `number` | `1.4` | Duration of one animation cycle in seconds. Overrides CSS variable. |
263
+ | `effect` | `'shimmer' \| 'pulse'` | `'shimmer'` | Animation style. `shimmer` sweeps a highlight; `pulse` fades the element. |
264
+ | `stagger` | `number` | `0` | Delay in seconds between each line in `variant="text"`. Creates a wave. |
265
+ | `animate` | `boolean` | `true` | Enables or disables the animation. |
266
+ | `style` | `React.CSSProperties` | — | Inline styles merged onto the element. Takes precedence over `width`/`height` props. |
267
+ | `className` | `string` | — | Extra CSS class appended to the skeleton element. |
268
+ | `aria-label` | `string` | `"Loading..."` | Screen reader label. Override when you know the content being loaded. |
269
+ | `children` | `React.ReactNode` | — | Rendered when `loading` is `false`. |
270
+ | `ref` | `React.Ref<HTMLElement>` | — | Forwarded to the underlying DOM element (`forwardRef`). |
271
+
272
+ ## Accessibility
273
+
274
+ When in loading state each skeleton element renders with:
275
+
276
+ ```html
277
+ <span role="progressbar" aria-busy="true" aria-label="Loading..."></span>
278
+ ```
279
+
280
+ Screen readers announce the loading state correctly with no extra configuration. Override `aria-label` when you know what content is loading:
281
+
282
+ ```tsx
283
+ <Skeleton loading={true} aria-label="Loading article content" />
284
+ ```
285
+
286
+ The shimmer animation is automatically disabled for users who have enabled **Reduce Motion** in their OS accessibility settings, via the native `prefers-reduced-motion` CSS media query.
287
+
288
+ ## Mobile support
289
+
290
+ The component is mobile-first by default:
291
+
292
+ - `max-width: 100%` prevents horizontal overflow on narrow viewports
293
+ - `box-sizing: border-box` ensures padding/border never break the layout
294
+ - `rect` and `text` variants use `display: block` so they naturally fill their container without extra CSS
295
+ - The shimmer runs on the GPU compositor thread (`transform: translateX()`), which is equally efficient on low-end mobile hardware
296
+ - `prefers-reduced-motion` is respected automatically — no JavaScript needed
297
+
298
+ For fluid layouts, omit `width` and let the skeleton fill its parent:
299
+
300
+ ```tsx
301
+ // Fills 100% of the parent — works on any screen size
302
+ <Skeleton loading={true} height={180} />
303
+ ```
304
+
305
+ ## CSS Custom Properties
306
+
307
+ You can override the skeleton’s visual tokens globally by setting CSS variables anywhere in your stylesheet:
308
+
309
+ ```css
310
+ :root {
311
+ --skeleton-base-color: #d0d0d0;
312
+ --skeleton-highlight-color: rgba(255, 255, 255, 0.7);
313
+ --skeleton-border-radius: 8px;
314
+ --skeleton-animation-duration: 1.8s;
315
+ }
316
+ ```
317
+
318
+ | Variable | Default | Description |
319
+ |---|---|---|
320
+ | `--skeleton-base-color` | `#eee` | Background fill color |
321
+ | `--skeleton-highlight-color` | `rgba(255,255,255,0.6)` | Shimmer sweep color |
322
+ | `--skeleton-border-radius` | `4px` | Corner radius for `rect` and `text` |
323
+ | `--skeleton-animation-duration` | `1.4s` | Duration of one animation cycle |
324
+ | `--skeleton-animation-delay` | `0s` | Per-element animation start delay (used internally by `stagger`) |
325
+
326
+ ## Testing
327
+
328
+ The library ships with **33 unit tests** covering all props and the `SkeletonTheme` context. Run them with:
329
+
330
+ ```bash
331
+ npm test # single run
332
+ npm run test:watch # watch mode
333
+ ```
334
+
335
+ Tests are written with [Vitest](https://vitest.dev/) and [@testing-library/react](https://testing-library.com/).
336
+
337
+
338
+ The shimmer is implemented as a `::after` pseudo-element animated exclusively with `transform: translateX()`. This property is handled entirely by the browser's compositor thread — it never triggers layout or paint, keeping the main thread free and CPU usage near zero regardless of how many skeletons are on screen.
339
+
340
+ `will-change: transform` is set on the pseudo-element so the browser promotes it to its own GPU layer before the animation starts.
341
+
342
+ ## Bundle size
343
+
344
+ | Format | Size |
345
+ | ------ | ------- |
346
+ | ESM | ~1.2 KB |
347
+ | CJS | ~1.3 KB |
348
+
349
+ Measured after minification. CSS is injected inline — no separate stylesheet to load.
350
+
351
+ ## Browser support
352
+
353
+ All modern browsers. Uses `@keyframes`, `transform`, `::after`, and `linear-gradient` — universally supported since IE 10+.
354
+
355
+ ## License
356
+
357
+ MIT © [tavosud](https://github.com/tavosud)
@@ -0,0 +1,123 @@
1
+ import React, { CSSProperties, ElementType } from 'react';
2
+ import './Skeleton.css';
3
+ /**
4
+ * Shape variant of the skeleton placeholder.
5
+ * - `'rect'` — rectangular block (cards, images, banners)
6
+ * - `'circle'` — circular shape (avatars, profile pictures)
7
+ * - `'text'` — narrow text line; combine with `count` for paragraphs
8
+ */
9
+ export type SkeletonVariant = 'rect' | 'circle' | 'text';
10
+ /**
11
+ * Visual animation style applied to the skeleton.
12
+ * - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)
13
+ * - `'pulse'` — the element gently fades in and out
14
+ */
15
+ export type SkeletonEffect = 'shimmer' | 'pulse';
16
+ export interface SkeletonProps {
17
+ /**
18
+ * Controls whether the skeleton or the actual content is displayed.
19
+ * When `false`, the component renders its `children` unchanged.
20
+ * @default true
21
+ */
22
+ loading?: boolean;
23
+ /**
24
+ * Shape of the skeleton placeholder.
25
+ * @default 'rect'
26
+ */
27
+ variant?: SkeletonVariant;
28
+ /**
29
+ * HTML element or React component to render as the skeleton root.
30
+ * Use this to keep the DOM semantically correct.
31
+ * @example `as="div"` | `as="li"` | `as="p"`
32
+ * @default 'span'
33
+ */
34
+ as?: ElementType;
35
+ /**
36
+ * Width of the skeleton element.
37
+ * - Numbers are converted to pixels: `200` → `"200px"`
38
+ * - Strings are used as-is: `"50%"`, `"12rem"`
39
+ */
40
+ width?: string | number;
41
+ /**
42
+ * Height of the skeleton element.
43
+ * - Numbers are converted to pixels: `20` → `"20px"`
44
+ * - Strings are used as-is: `"1.5rem"`, `"auto"`
45
+ * - For `variant="circle"`, omitting `height` automatically copies `width`.
46
+ */
47
+ height?: string | number;
48
+ /**
49
+ * Border radius of the skeleton element.
50
+ * - Numbers are converted to pixels: `8` → `"8px"`
51
+ * - Strings are used as-is: `"50%"`, `"1rem"`
52
+ * - Overrides the `--skeleton-border-radius` CSS variable for this instance.
53
+ */
54
+ borderRadius?: string | number;
55
+ /**
56
+ * Number of skeleton lines to render. Applies whenever `variant="text"`.
57
+ * - `count={1}` renders a single line (default)
58
+ * - `count={3}` renders three lines, with the last at 80% width to mimic
59
+ * a real paragraph ending
60
+ * @default 1
61
+ */
62
+ count?: number;
63
+ /**
64
+ * Duration of one animation cycle in seconds.
65
+ * Overrides the `--skeleton-animation-duration` CSS variable for this instance.
66
+ * @example `speed={2}` for a slower, subtler animation
67
+ * @default 1.4
68
+ */
69
+ speed?: number;
70
+ /**
71
+ * Visual effect for the skeleton animation.
72
+ * - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)
73
+ * - `'pulse'` — the element fades in and out
74
+ * @default 'shimmer'
75
+ */
76
+ effect?: SkeletonEffect;
77
+ /**
78
+ * Animation-delay increment (in seconds) between each line when
79
+ * `variant="text"` and `count > 1`. Creates a staggered wave effect.
80
+ * @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line
81
+ * @default 0
82
+ */
83
+ stagger?: number;
84
+ /**
85
+ * Enables or disables the skeleton animation.
86
+ * Set to `false` to render a static placeholder.
87
+ * @default true
88
+ */
89
+ animate?: boolean;
90
+ /**
91
+ * Additional CSS class name appended to the skeleton element.
92
+ */
93
+ className?: string;
94
+ /**
95
+ * Inline styles applied directly to the skeleton element.
96
+ * Values provided here take precedence over `width`/`height` props.
97
+ */
98
+ style?: CSSProperties;
99
+ /**
100
+ * Accessible label read by screen readers while the skeleton is visible.
101
+ * @default "Loading..."
102
+ */
103
+ 'aria-label'?: string;
104
+ /**
105
+ * Content rendered when `loading` is `false`.
106
+ */
107
+ children?: React.ReactNode;
108
+ }
109
+ /**
110
+ * Animated content placeholder shown while data is loading.
111
+ *
112
+ * @example
113
+ * // Single rect (default)
114
+ * <Skeleton width={300} height={20} />
115
+ *
116
+ * // Paragraph of 3 text lines
117
+ * <Skeleton variant="text" count={3} stagger={0.08} />
118
+ *
119
+ * // Avatar circle
120
+ * <Skeleton variant="circle" width={48} />
121
+ */
122
+ export declare const Skeleton: React.ForwardRefExoticComponent<SkeletonProps & React.RefAttributes<HTMLElement>>;
123
+ export default Skeleton;
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { SkeletonVariant, SkeletonEffect } from './Skeleton';
3
+ export interface SkeletonThemeContextValue {
4
+ /**
5
+ * Base background color of the skeleton.
6
+ * Overrides the `--skeleton-base-color` CSS variable.
7
+ */
8
+ baseColor?: string;
9
+ /**
10
+ * Color of the shimmer highlight.
11
+ * Overrides the `--skeleton-highlight-color` CSS variable.
12
+ */
13
+ highlightColor?: string;
14
+ /**
15
+ * Duration of one animation cycle in seconds.
16
+ * @default 1.4
17
+ */
18
+ speed?: number;
19
+ /**
20
+ * Border radius applied to `rect` and `text` variants.
21
+ * Numbers are treated as `px`.
22
+ * @default 4
23
+ */
24
+ borderRadius?: string | number;
25
+ /**
26
+ * When `false`, all skeletons inside the theme render without animation.
27
+ * @default true
28
+ */
29
+ animate?: boolean;
30
+ /**
31
+ * Default animation effect for all Skeleton children.
32
+ * @default 'shimmer'
33
+ */
34
+ effect?: SkeletonEffect;
35
+ /**
36
+ * Default variant for all Skeleton children.
37
+ * @default 'rect'
38
+ */
39
+ variant?: SkeletonVariant;
40
+ }
41
+ /** Read the nearest SkeletonTheme context. */
42
+ export declare function useSkeletonTheme(): SkeletonThemeContextValue;
43
+ export interface SkeletonThemeProps extends SkeletonThemeContextValue {
44
+ children: React.ReactNode;
45
+ }
46
+ /**
47
+ * Sets default values for all `<Skeleton>` components in its subtree.
48
+ * Also injects CSS custom properties so colors and speed are applied
49
+ * without any JavaScript per-instance overhead.
50
+ *
51
+ * Uses `display: contents` on the CSS-variable wrapper so it never
52
+ * breaks flex, grid, or any other layout context.
53
+ *
54
+ * @example
55
+ * <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
56
+ * <Skeleton />
57
+ * <Skeleton variant="circle" width={48} />
58
+ * </SkeletonTheme>
59
+ */
60
+ export declare function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }: SkeletonThemeProps): React.JSX.Element;
61
+ export default SkeletonTheme;
@@ -0,0 +1,4 @@
1
+ export { Skeleton, default } from './Skeleton';
2
+ export type { SkeletonProps, SkeletonVariant, SkeletonEffect } from './Skeleton';
3
+ export { SkeletonTheme } from './SkeletonTheme';
4
+ export type { SkeletonThemeProps, SkeletonThemeContextValue } from './SkeletonTheme';
@@ -0,0 +1,146 @@
1
+ import React, { createContext, useContext, forwardRef, useRef, useState, useEffect } from 'react';
2
+
3
+ const SkeletonThemeContext = createContext({});
4
+ /** Read the nearest SkeletonTheme context. */
5
+ function useSkeletonTheme() {
6
+ return useContext(SkeletonThemeContext);
7
+ }
8
+ /**
9
+ * Sets default values for all `<Skeleton>` components in its subtree.
10
+ * Also injects CSS custom properties so colors and speed are applied
11
+ * without any JavaScript per-instance overhead.
12
+ *
13
+ * Uses `display: contents` on the CSS-variable wrapper so it never
14
+ * breaks flex, grid, or any other layout context.
15
+ *
16
+ * @example
17
+ * <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
18
+ * <Skeleton />
19
+ * <Skeleton variant="circle" width={48} />
20
+ * </SkeletonTheme>
21
+ */
22
+ function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }) {
23
+ const cssVars = {};
24
+ if (baseColor !== undefined) {
25
+ cssVars['--skeleton-base-color'] = baseColor;
26
+ }
27
+ if (highlightColor !== undefined) {
28
+ cssVars['--skeleton-highlight-color'] = highlightColor;
29
+ }
30
+ if (speed !== undefined) {
31
+ cssVars['--skeleton-animation-duration'] = `${speed}s`;
32
+ }
33
+ if (borderRadius !== undefined) {
34
+ const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
35
+ cssVars['--skeleton-border-radius'] = r;
36
+ }
37
+ const hasCssVars = Object.keys(cssVars).length > 0;
38
+ return (React.createElement(SkeletonThemeContext.Provider, { value: { baseColor, highlightColor, speed, borderRadius, animate, effect, variant } }, hasCssVars ? (
39
+ // display:contents makes the wrapper invisible to layout (flex/grid),
40
+ // while still propagating inherited CSS custom properties.
41
+ React.createElement("div", { style: Object.assign(Object.assign({}, cssVars), { display: 'contents' }) }, children)) : (React.createElement(React.Fragment, null, children))));
42
+ }
43
+
44
+ /** Merges multiple refs into a single callback ref. */
45
+ function mergeRefs(...refs) {
46
+ return (value) => {
47
+ refs.forEach((ref) => {
48
+ if (typeof ref === 'function') {
49
+ ref(value);
50
+ }
51
+ else if (ref != null) {
52
+ ref.current = value;
53
+ }
54
+ });
55
+ };
56
+ }
57
+ /** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */
58
+ function useIsVisible(ref) {
59
+ const [visible, setVisible] = useState(true);
60
+ useEffect(() => {
61
+ const el = ref.current;
62
+ if (!el || typeof IntersectionObserver === 'undefined')
63
+ return;
64
+ const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), { threshold: 0 });
65
+ observer.observe(el);
66
+ return () => observer.disconnect();
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, []);
69
+ return visible;
70
+ }
71
+ function normalizeSize(value) {
72
+ if (value === undefined)
73
+ return undefined;
74
+ return typeof value === 'number' ? `${value}px` : value;
75
+ }
76
+ const SkeletonItem = forwardRef(function SkeletonItem({ variant = 'rect', as: Tag = 'span', width, height, borderRadius, speed, delay, effect = 'shimmer', className, style: styleProp, animate = true, 'aria-label': ariaLabel = 'Loading...', }, forwardedRef) {
77
+ const innerRef = useRef(null);
78
+ const isVisible = useIsVisible(innerRef);
79
+ // Auto-square circle: when only width is given, mirror it as height
80
+ const resolvedHeight = variant === 'circle' && height === undefined && width !== undefined ? width : height;
81
+ const cssVars = {};
82
+ if (speed !== undefined) {
83
+ cssVars['--skeleton-animation-duration'] = `${speed}s`;
84
+ }
85
+ if (borderRadius !== undefined) {
86
+ cssVars['--skeleton-border-radius'] =
87
+ typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
88
+ }
89
+ if (delay !== undefined && delay > 0) {
90
+ cssVars['--skeleton-animation-delay'] = `${delay}s`;
91
+ }
92
+ const style = Object.assign(Object.assign({ width: normalizeSize(width), height: normalizeSize(resolvedHeight) }, cssVars), styleProp);
93
+ const classes = [
94
+ 'skeleton',
95
+ `skeleton--${variant}`,
96
+ `skeleton--${effect}`,
97
+ !animate && 'skeleton--no-animate',
98
+ animate && !isVisible && 'skeleton--paused',
99
+ className,
100
+ ]
101
+ .filter(Boolean)
102
+ .join(' ');
103
+ const El = Tag;
104
+ return (React.createElement(El, { ref: mergeRefs(innerRef, forwardedRef), role: "progressbar", "aria-busy": "true", "aria-label": ariaLabel, className: classes, style: style }));
105
+ });
106
+ /**
107
+ * Animated content placeholder shown while data is loading.
108
+ *
109
+ * @example
110
+ * // Single rect (default)
111
+ * <Skeleton width={300} height={20} />
112
+ *
113
+ * // Paragraph of 3 text lines
114
+ * <Skeleton variant="text" count={3} stagger={0.08} />
115
+ *
116
+ * // Avatar circle
117
+ * <Skeleton variant="circle" width={48} />
118
+ */
119
+ const Skeleton = forwardRef(function Skeleton(props, ref) {
120
+ var _a, _b, _c;
121
+ const theme = useSkeletonTheme();
122
+ const { loading = true, variant = (_a = theme.variant) !== null && _a !== void 0 ? _a : 'rect', as, width, height, borderRadius = theme.borderRadius, count = 1, speed = theme.speed, effect = (_b = theme.effect) !== null && _b !== void 0 ? _b : 'shimmer', stagger = 0, animate = (_c = theme.animate) !== null && _c !== void 0 ? _c : true, className, style, 'aria-label': ariaLabel, children, } = props;
123
+ if (!loading) {
124
+ return React.createElement(React.Fragment, null, children);
125
+ }
126
+ const itemProps = {
127
+ variant,
128
+ as,
129
+ width,
130
+ height,
131
+ borderRadius,
132
+ speed,
133
+ effect,
134
+ animate,
135
+ className,
136
+ style,
137
+ 'aria-label': ariaLabel,
138
+ };
139
+ if (variant === 'text') {
140
+ return (React.createElement(React.Fragment, null, Array.from({ length: count }, (_, i) => (React.createElement(SkeletonItem, Object.assign({ key: i }, itemProps, { variant: "text", delay: stagger > 0 ? i * stagger : undefined, ref: i === 0 ? ref : undefined }))))));
141
+ }
142
+ return React.createElement(SkeletonItem, Object.assign({}, itemProps, { ref: ref }));
143
+ });
144
+
145
+ export { Skeleton, SkeletonTheme, Skeleton as default };
146
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../src/SkeletonTheme.tsx","../src/Skeleton.tsx"],"sourcesContent":["import React, { createContext, useContext, CSSProperties } from 'react';\r\nimport { SkeletonVariant, SkeletonEffect } from './Skeleton';\r\n\r\nexport interface SkeletonThemeContextValue {\r\n /**\r\n * Base background color of the skeleton.\r\n * Overrides the `--skeleton-base-color` CSS variable.\r\n */\r\n baseColor?: string;\r\n\r\n /**\r\n * Color of the shimmer highlight.\r\n * Overrides the `--skeleton-highlight-color` CSS variable.\r\n */\r\n highlightColor?: string;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Border radius applied to `rect` and `text` variants.\r\n * Numbers are treated as `px`.\r\n * @default 4\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * When `false`, all skeletons inside the theme render without animation.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Default animation effect for all Skeleton children.\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Default variant for all Skeleton children.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n}\r\n\r\nconst SkeletonThemeContext = createContext<SkeletonThemeContextValue>({});\r\n\r\n/** Read the nearest SkeletonTheme context. */\r\nexport function useSkeletonTheme(): SkeletonThemeContextValue {\r\n return useContext(SkeletonThemeContext);\r\n}\r\n\r\nexport interface SkeletonThemeProps extends SkeletonThemeContextValue {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * Sets default values for all `<Skeleton>` components in its subtree.\r\n * Also injects CSS custom properties so colors and speed are applied\r\n * without any JavaScript per-instance overhead.\r\n *\r\n * Uses `display: contents` on the CSS-variable wrapper so it never\r\n * breaks flex, grid, or any other layout context.\r\n *\r\n * @example\r\n * <SkeletonTheme baseColor=\"#e0e0e0\" highlightColor=\"#f5f5f5\" speed={1.8}>\r\n * <Skeleton />\r\n * <Skeleton variant=\"circle\" width={48} />\r\n * </SkeletonTheme>\r\n */\r\nexport function SkeletonTheme({\r\n children,\r\n baseColor,\r\n highlightColor,\r\n speed,\r\n borderRadius,\r\n animate,\r\n effect,\r\n variant,\r\n}: SkeletonThemeProps) {\r\n const cssVars: CSSProperties = {};\r\n\r\n if (baseColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-base-color'] = baseColor;\r\n }\r\n if (highlightColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-highlight-color'] = highlightColor;\r\n }\r\n if (speed !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n (cssVars as Record<string, string>)['--skeleton-border-radius'] = r;\r\n }\r\n\r\n const hasCssVars = Object.keys(cssVars).length > 0;\r\n\r\n return (\r\n <SkeletonThemeContext.Provider value={{ baseColor, highlightColor, speed, borderRadius, animate, effect, variant }}>\r\n {hasCssVars ? (\r\n // display:contents makes the wrapper invisible to layout (flex/grid),\r\n // while still propagating inherited CSS custom properties.\r\n <div style={{ ...cssVars, display: 'contents' }}>\r\n {children}\r\n </div>\r\n ) : (\r\n <>{children}</>\r\n )}\r\n </SkeletonThemeContext.Provider>\r\n );\r\n}\r\n\r\nexport default SkeletonTheme;\r\n","import React, { CSSProperties, ElementType, forwardRef, useEffect, useRef, useState } from 'react';\r\nimport './Skeleton.css';\r\nimport { useSkeletonTheme } from './SkeletonTheme';\r\n\r\n/**\r\n * Shape variant of the skeleton placeholder.\r\n * - `'rect'` — rectangular block (cards, images, banners)\r\n * - `'circle'` — circular shape (avatars, profile pictures)\r\n * - `'text'` — narrow text line; combine with `count` for paragraphs\r\n */\r\nexport type SkeletonVariant = 'rect' | 'circle' | 'text';\r\n\r\n/**\r\n * Visual animation style applied to the skeleton.\r\n * - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element gently fades in and out\r\n */\r\nexport type SkeletonEffect = 'shimmer' | 'pulse';\r\n\r\n/** Merges multiple refs into a single callback ref. */\r\nfunction mergeRefs<T>(...refs: (React.Ref<T> | null | undefined)[]): React.RefCallback<T> {\r\n return (value) => {\r\n refs.forEach((ref) => {\r\n if (typeof ref === 'function') {\r\n ref(value);\r\n } else if (ref != null) {\r\n (ref as { current: T | null }).current = value;\r\n }\r\n });\r\n };\r\n}\r\n\r\n/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */\r\nfunction useIsVisible(ref: React.RefObject<HTMLElement>): boolean {\r\n const [visible, setVisible] = useState(true);\r\n useEffect(() => {\r\n const el = ref.current;\r\n if (!el || typeof IntersectionObserver === 'undefined') return;\r\n const observer = new IntersectionObserver(\r\n ([entry]) => setVisible(entry.isIntersecting),\r\n { threshold: 0 }\r\n );\r\n observer.observe(el);\r\n return () => observer.disconnect();\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n return visible;\r\n}\r\n\r\nexport interface SkeletonProps {\r\n /**\r\n * Controls whether the skeleton or the actual content is displayed.\r\n * When `false`, the component renders its `children` unchanged.\r\n * @default true\r\n */\r\n loading?: boolean;\r\n\r\n /**\r\n * Shape of the skeleton placeholder.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n\r\n /**\r\n * HTML element or React component to render as the skeleton root.\r\n * Use this to keep the DOM semantically correct.\r\n * @example `as=\"div\"` | `as=\"li\"` | `as=\"p\"`\r\n * @default 'span'\r\n */\r\n as?: ElementType;\r\n\r\n /**\r\n * Width of the skeleton element.\r\n * - Numbers are converted to pixels: `200` → `\"200px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"12rem\"`\r\n */\r\n width?: string | number;\r\n\r\n /**\r\n * Height of the skeleton element.\r\n * - Numbers are converted to pixels: `20` → `\"20px\"`\r\n * - Strings are used as-is: `\"1.5rem\"`, `\"auto\"`\r\n * - For `variant=\"circle\"`, omitting `height` automatically copies `width`.\r\n */\r\n height?: string | number;\r\n\r\n /**\r\n * Border radius of the skeleton element.\r\n * - Numbers are converted to pixels: `8` → `\"8px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"1rem\"`\r\n * - Overrides the `--skeleton-border-radius` CSS variable for this instance.\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * Number of skeleton lines to render. Applies whenever `variant=\"text\"`.\r\n * - `count={1}` renders a single line (default)\r\n * - `count={3}` renders three lines, with the last at 80% width to mimic\r\n * a real paragraph ending\r\n * @default 1\r\n */\r\n count?: number;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * Overrides the `--skeleton-animation-duration` CSS variable for this instance.\r\n * @example `speed={2}` for a slower, subtler animation\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Visual effect for the skeleton animation.\r\n * - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element fades in and out\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Animation-delay increment (in seconds) between each line when\r\n * `variant=\"text\"` and `count > 1`. Creates a staggered wave effect.\r\n * @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line\r\n * @default 0\r\n */\r\n stagger?: number;\r\n\r\n /**\r\n * Enables or disables the skeleton animation.\r\n * Set to `false` to render a static placeholder.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Additional CSS class name appended to the skeleton element.\r\n */\r\n className?: string;\r\n\r\n /**\r\n * Inline styles applied directly to the skeleton element.\r\n * Values provided here take precedence over `width`/`height` props.\r\n */\r\n style?: CSSProperties;\r\n\r\n /**\r\n * Accessible label read by screen readers while the skeleton is visible.\r\n * @default \"Loading...\"\r\n */\r\n 'aria-label'?: string;\r\n\r\n /**\r\n * Content rendered when `loading` is `false`.\r\n */\r\n children?: React.ReactNode;\r\n}\r\n\r\nfunction normalizeSize(value: string | number | undefined): string | undefined {\r\n if (value === undefined) return undefined;\r\n return typeof value === 'number' ? `${value}px` : value;\r\n}\r\n\r\ntype SkeletonItemProps = Required<Pick<SkeletonProps, 'variant'>> &\r\n Pick<SkeletonProps, 'as' | 'width' | 'height' | 'borderRadius' | 'speed' | 'effect' | 'className' | 'style' | 'animate' | 'aria-label'> & {\r\n /** Per-item animation-delay in seconds (stagger effect). */\r\n delay?: number;\r\n };\r\n\r\nconst SkeletonItem = forwardRef<HTMLElement, SkeletonItemProps>(function SkeletonItem({\r\n variant = 'rect',\r\n as: Tag = 'span',\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n delay,\r\n effect = 'shimmer',\r\n className,\r\n style: styleProp,\r\n animate = true,\r\n 'aria-label': ariaLabel = 'Loading...',\r\n}, forwardedRef) {\r\n const innerRef = useRef<HTMLElement>(null);\r\n const isVisible = useIsVisible(innerRef);\r\n\r\n // Auto-square circle: when only width is given, mirror it as height\r\n const resolvedHeight =\r\n variant === 'circle' && height === undefined && width !== undefined ? width : height;\r\n\r\n const cssVars: Record<string, string> = {};\r\n if (speed !== undefined) {\r\n cssVars['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n cssVars['--skeleton-border-radius'] =\r\n typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n }\r\n if (delay !== undefined && delay > 0) {\r\n cssVars['--skeleton-animation-delay'] = `${delay}s`;\r\n }\r\n\r\n const style: CSSProperties = {\r\n width: normalizeSize(width),\r\n height: normalizeSize(resolvedHeight),\r\n ...cssVars,\r\n ...styleProp,\r\n };\r\n\r\n const classes = [\r\n 'skeleton',\r\n `skeleton--${variant}`,\r\n `skeleton--${effect}`,\r\n !animate && 'skeleton--no-animate',\r\n animate && !isVisible && 'skeleton--paused',\r\n className,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const El: any = Tag;\r\n return (\r\n <El\r\n ref={mergeRefs(innerRef, forwardedRef)}\r\n role=\"progressbar\"\r\n aria-busy=\"true\"\r\n aria-label={ariaLabel}\r\n className={classes}\r\n style={style}\r\n />\r\n );\r\n});\r\n\r\n/**\r\n * Animated content placeholder shown while data is loading.\r\n *\r\n * @example\r\n * // Single rect (default)\r\n * <Skeleton width={300} height={20} />\r\n *\r\n * // Paragraph of 3 text lines\r\n * <Skeleton variant=\"text\" count={3} stagger={0.08} />\r\n *\r\n * // Avatar circle\r\n * <Skeleton variant=\"circle\" width={48} />\r\n */\r\nexport const Skeleton = forwardRef<HTMLElement, SkeletonProps>(function Skeleton(props, ref) {\r\n const theme = useSkeletonTheme();\r\n\r\n const {\r\n loading = true,\r\n variant = theme.variant ?? 'rect',\r\n as,\r\n width,\r\n height,\r\n borderRadius = theme.borderRadius,\r\n count = 1,\r\n speed = theme.speed,\r\n effect = theme.effect ?? 'shimmer',\r\n stagger = 0,\r\n animate = theme.animate ?? true,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n children,\r\n } = props;\r\n\r\n if (!loading) {\r\n return <>{children}</>;\r\n }\r\n\r\n const itemProps: Omit<SkeletonItemProps, 'delay'> = {\r\n variant,\r\n as,\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n effect,\r\n animate,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n };\r\n\r\n if (variant === 'text') {\r\n return (\r\n <>\r\n {Array.from({ length: count }, (_, i) => (\r\n <SkeletonItem\r\n key={i}\r\n {...itemProps}\r\n variant=\"text\"\r\n delay={stagger > 0 ? i * stagger : undefined}\r\n ref={i === 0 ? ref : undefined}\r\n />\r\n ))}\r\n </>\r\n );\r\n }\r\n\r\n return <SkeletonItem {...itemProps} ref={ref} />;\r\n});\r\n\r\nexport default Skeleton;\r\n\r\n"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var React = require('react');
6
+
7
+ const SkeletonThemeContext = React.createContext({});
8
+ /** Read the nearest SkeletonTheme context. */
9
+ function useSkeletonTheme() {
10
+ return React.useContext(SkeletonThemeContext);
11
+ }
12
+ /**
13
+ * Sets default values for all `<Skeleton>` components in its subtree.
14
+ * Also injects CSS custom properties so colors and speed are applied
15
+ * without any JavaScript per-instance overhead.
16
+ *
17
+ * Uses `display: contents` on the CSS-variable wrapper so it never
18
+ * breaks flex, grid, or any other layout context.
19
+ *
20
+ * @example
21
+ * <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
22
+ * <Skeleton />
23
+ * <Skeleton variant="circle" width={48} />
24
+ * </SkeletonTheme>
25
+ */
26
+ function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }) {
27
+ const cssVars = {};
28
+ if (baseColor !== undefined) {
29
+ cssVars['--skeleton-base-color'] = baseColor;
30
+ }
31
+ if (highlightColor !== undefined) {
32
+ cssVars['--skeleton-highlight-color'] = highlightColor;
33
+ }
34
+ if (speed !== undefined) {
35
+ cssVars['--skeleton-animation-duration'] = `${speed}s`;
36
+ }
37
+ if (borderRadius !== undefined) {
38
+ const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
39
+ cssVars['--skeleton-border-radius'] = r;
40
+ }
41
+ const hasCssVars = Object.keys(cssVars).length > 0;
42
+ return (React.createElement(SkeletonThemeContext.Provider, { value: { baseColor, highlightColor, speed, borderRadius, animate, effect, variant } }, hasCssVars ? (
43
+ // display:contents makes the wrapper invisible to layout (flex/grid),
44
+ // while still propagating inherited CSS custom properties.
45
+ React.createElement("div", { style: Object.assign(Object.assign({}, cssVars), { display: 'contents' }) }, children)) : (React.createElement(React.Fragment, null, children))));
46
+ }
47
+
48
+ /** Merges multiple refs into a single callback ref. */
49
+ function mergeRefs(...refs) {
50
+ return (value) => {
51
+ refs.forEach((ref) => {
52
+ if (typeof ref === 'function') {
53
+ ref(value);
54
+ }
55
+ else if (ref != null) {
56
+ ref.current = value;
57
+ }
58
+ });
59
+ };
60
+ }
61
+ /** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */
62
+ function useIsVisible(ref) {
63
+ const [visible, setVisible] = React.useState(true);
64
+ React.useEffect(() => {
65
+ const el = ref.current;
66
+ if (!el || typeof IntersectionObserver === 'undefined')
67
+ return;
68
+ const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), { threshold: 0 });
69
+ observer.observe(el);
70
+ return () => observer.disconnect();
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, []);
73
+ return visible;
74
+ }
75
+ function normalizeSize(value) {
76
+ if (value === undefined)
77
+ return undefined;
78
+ return typeof value === 'number' ? `${value}px` : value;
79
+ }
80
+ const SkeletonItem = React.forwardRef(function SkeletonItem({ variant = 'rect', as: Tag = 'span', width, height, borderRadius, speed, delay, effect = 'shimmer', className, style: styleProp, animate = true, 'aria-label': ariaLabel = 'Loading...', }, forwardedRef) {
81
+ const innerRef = React.useRef(null);
82
+ const isVisible = useIsVisible(innerRef);
83
+ // Auto-square circle: when only width is given, mirror it as height
84
+ const resolvedHeight = variant === 'circle' && height === undefined && width !== undefined ? width : height;
85
+ const cssVars = {};
86
+ if (speed !== undefined) {
87
+ cssVars['--skeleton-animation-duration'] = `${speed}s`;
88
+ }
89
+ if (borderRadius !== undefined) {
90
+ cssVars['--skeleton-border-radius'] =
91
+ typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
92
+ }
93
+ if (delay !== undefined && delay > 0) {
94
+ cssVars['--skeleton-animation-delay'] = `${delay}s`;
95
+ }
96
+ const style = Object.assign(Object.assign({ width: normalizeSize(width), height: normalizeSize(resolvedHeight) }, cssVars), styleProp);
97
+ const classes = [
98
+ 'skeleton',
99
+ `skeleton--${variant}`,
100
+ `skeleton--${effect}`,
101
+ !animate && 'skeleton--no-animate',
102
+ animate && !isVisible && 'skeleton--paused',
103
+ className,
104
+ ]
105
+ .filter(Boolean)
106
+ .join(' ');
107
+ const El = Tag;
108
+ return (React.createElement(El, { ref: mergeRefs(innerRef, forwardedRef), role: "progressbar", "aria-busy": "true", "aria-label": ariaLabel, className: classes, style: style }));
109
+ });
110
+ /**
111
+ * Animated content placeholder shown while data is loading.
112
+ *
113
+ * @example
114
+ * // Single rect (default)
115
+ * <Skeleton width={300} height={20} />
116
+ *
117
+ * // Paragraph of 3 text lines
118
+ * <Skeleton variant="text" count={3} stagger={0.08} />
119
+ *
120
+ * // Avatar circle
121
+ * <Skeleton variant="circle" width={48} />
122
+ */
123
+ const Skeleton = React.forwardRef(function Skeleton(props, ref) {
124
+ var _a, _b, _c;
125
+ const theme = useSkeletonTheme();
126
+ const { loading = true, variant = (_a = theme.variant) !== null && _a !== void 0 ? _a : 'rect', as, width, height, borderRadius = theme.borderRadius, count = 1, speed = theme.speed, effect = (_b = theme.effect) !== null && _b !== void 0 ? _b : 'shimmer', stagger = 0, animate = (_c = theme.animate) !== null && _c !== void 0 ? _c : true, className, style, 'aria-label': ariaLabel, children, } = props;
127
+ if (!loading) {
128
+ return React.createElement(React.Fragment, null, children);
129
+ }
130
+ const itemProps = {
131
+ variant,
132
+ as,
133
+ width,
134
+ height,
135
+ borderRadius,
136
+ speed,
137
+ effect,
138
+ animate,
139
+ className,
140
+ style,
141
+ 'aria-label': ariaLabel,
142
+ };
143
+ if (variant === 'text') {
144
+ return (React.createElement(React.Fragment, null, Array.from({ length: count }, (_, i) => (React.createElement(SkeletonItem, Object.assign({ key: i }, itemProps, { variant: "text", delay: stagger > 0 ? i * stagger : undefined, ref: i === 0 ? ref : undefined }))))));
145
+ }
146
+ return React.createElement(SkeletonItem, Object.assign({}, itemProps, { ref: ref }));
147
+ });
148
+
149
+ exports.Skeleton = Skeleton;
150
+ exports.SkeletonTheme = SkeletonTheme;
151
+ exports.default = Skeleton;
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/SkeletonTheme.tsx","../src/Skeleton.tsx"],"sourcesContent":["import React, { createContext, useContext, CSSProperties } from 'react';\r\nimport { SkeletonVariant, SkeletonEffect } from './Skeleton';\r\n\r\nexport interface SkeletonThemeContextValue {\r\n /**\r\n * Base background color of the skeleton.\r\n * Overrides the `--skeleton-base-color` CSS variable.\r\n */\r\n baseColor?: string;\r\n\r\n /**\r\n * Color of the shimmer highlight.\r\n * Overrides the `--skeleton-highlight-color` CSS variable.\r\n */\r\n highlightColor?: string;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Border radius applied to `rect` and `text` variants.\r\n * Numbers are treated as `px`.\r\n * @default 4\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * When `false`, all skeletons inside the theme render without animation.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Default animation effect for all Skeleton children.\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Default variant for all Skeleton children.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n}\r\n\r\nconst SkeletonThemeContext = createContext<SkeletonThemeContextValue>({});\r\n\r\n/** Read the nearest SkeletonTheme context. */\r\nexport function useSkeletonTheme(): SkeletonThemeContextValue {\r\n return useContext(SkeletonThemeContext);\r\n}\r\n\r\nexport interface SkeletonThemeProps extends SkeletonThemeContextValue {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * Sets default values for all `<Skeleton>` components in its subtree.\r\n * Also injects CSS custom properties so colors and speed are applied\r\n * without any JavaScript per-instance overhead.\r\n *\r\n * Uses `display: contents` on the CSS-variable wrapper so it never\r\n * breaks flex, grid, or any other layout context.\r\n *\r\n * @example\r\n * <SkeletonTheme baseColor=\"#e0e0e0\" highlightColor=\"#f5f5f5\" speed={1.8}>\r\n * <Skeleton />\r\n * <Skeleton variant=\"circle\" width={48} />\r\n * </SkeletonTheme>\r\n */\r\nexport function SkeletonTheme({\r\n children,\r\n baseColor,\r\n highlightColor,\r\n speed,\r\n borderRadius,\r\n animate,\r\n effect,\r\n variant,\r\n}: SkeletonThemeProps) {\r\n const cssVars: CSSProperties = {};\r\n\r\n if (baseColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-base-color'] = baseColor;\r\n }\r\n if (highlightColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-highlight-color'] = highlightColor;\r\n }\r\n if (speed !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n (cssVars as Record<string, string>)['--skeleton-border-radius'] = r;\r\n }\r\n\r\n const hasCssVars = Object.keys(cssVars).length > 0;\r\n\r\n return (\r\n <SkeletonThemeContext.Provider value={{ baseColor, highlightColor, speed, borderRadius, animate, effect, variant }}>\r\n {hasCssVars ? (\r\n // display:contents makes the wrapper invisible to layout (flex/grid),\r\n // while still propagating inherited CSS custom properties.\r\n <div style={{ ...cssVars, display: 'contents' }}>\r\n {children}\r\n </div>\r\n ) : (\r\n <>{children}</>\r\n )}\r\n </SkeletonThemeContext.Provider>\r\n );\r\n}\r\n\r\nexport default SkeletonTheme;\r\n","import React, { CSSProperties, ElementType, forwardRef, useEffect, useRef, useState } from 'react';\r\nimport './Skeleton.css';\r\nimport { useSkeletonTheme } from './SkeletonTheme';\r\n\r\n/**\r\n * Shape variant of the skeleton placeholder.\r\n * - `'rect'` — rectangular block (cards, images, banners)\r\n * - `'circle'` — circular shape (avatars, profile pictures)\r\n * - `'text'` — narrow text line; combine with `count` for paragraphs\r\n */\r\nexport type SkeletonVariant = 'rect' | 'circle' | 'text';\r\n\r\n/**\r\n * Visual animation style applied to the skeleton.\r\n * - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element gently fades in and out\r\n */\r\nexport type SkeletonEffect = 'shimmer' | 'pulse';\r\n\r\n/** Merges multiple refs into a single callback ref. */\r\nfunction mergeRefs<T>(...refs: (React.Ref<T> | null | undefined)[]): React.RefCallback<T> {\r\n return (value) => {\r\n refs.forEach((ref) => {\r\n if (typeof ref === 'function') {\r\n ref(value);\r\n } else if (ref != null) {\r\n (ref as { current: T | null }).current = value;\r\n }\r\n });\r\n };\r\n}\r\n\r\n/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */\r\nfunction useIsVisible(ref: React.RefObject<HTMLElement>): boolean {\r\n const [visible, setVisible] = useState(true);\r\n useEffect(() => {\r\n const el = ref.current;\r\n if (!el || typeof IntersectionObserver === 'undefined') return;\r\n const observer = new IntersectionObserver(\r\n ([entry]) => setVisible(entry.isIntersecting),\r\n { threshold: 0 }\r\n );\r\n observer.observe(el);\r\n return () => observer.disconnect();\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n return visible;\r\n}\r\n\r\nexport interface SkeletonProps {\r\n /**\r\n * Controls whether the skeleton or the actual content is displayed.\r\n * When `false`, the component renders its `children` unchanged.\r\n * @default true\r\n */\r\n loading?: boolean;\r\n\r\n /**\r\n * Shape of the skeleton placeholder.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n\r\n /**\r\n * HTML element or React component to render as the skeleton root.\r\n * Use this to keep the DOM semantically correct.\r\n * @example `as=\"div\"` | `as=\"li\"` | `as=\"p\"`\r\n * @default 'span'\r\n */\r\n as?: ElementType;\r\n\r\n /**\r\n * Width of the skeleton element.\r\n * - Numbers are converted to pixels: `200` → `\"200px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"12rem\"`\r\n */\r\n width?: string | number;\r\n\r\n /**\r\n * Height of the skeleton element.\r\n * - Numbers are converted to pixels: `20` → `\"20px\"`\r\n * - Strings are used as-is: `\"1.5rem\"`, `\"auto\"`\r\n * - For `variant=\"circle\"`, omitting `height` automatically copies `width`.\r\n */\r\n height?: string | number;\r\n\r\n /**\r\n * Border radius of the skeleton element.\r\n * - Numbers are converted to pixels: `8` → `\"8px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"1rem\"`\r\n * - Overrides the `--skeleton-border-radius` CSS variable for this instance.\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * Number of skeleton lines to render. Applies whenever `variant=\"text\"`.\r\n * - `count={1}` renders a single line (default)\r\n * - `count={3}` renders three lines, with the last at 80% width to mimic\r\n * a real paragraph ending\r\n * @default 1\r\n */\r\n count?: number;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * Overrides the `--skeleton-animation-duration` CSS variable for this instance.\r\n * @example `speed={2}` for a slower, subtler animation\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Visual effect for the skeleton animation.\r\n * - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element fades in and out\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Animation-delay increment (in seconds) between each line when\r\n * `variant=\"text\"` and `count > 1`. Creates a staggered wave effect.\r\n * @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line\r\n * @default 0\r\n */\r\n stagger?: number;\r\n\r\n /**\r\n * Enables or disables the skeleton animation.\r\n * Set to `false` to render a static placeholder.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Additional CSS class name appended to the skeleton element.\r\n */\r\n className?: string;\r\n\r\n /**\r\n * Inline styles applied directly to the skeleton element.\r\n * Values provided here take precedence over `width`/`height` props.\r\n */\r\n style?: CSSProperties;\r\n\r\n /**\r\n * Accessible label read by screen readers while the skeleton is visible.\r\n * @default \"Loading...\"\r\n */\r\n 'aria-label'?: string;\r\n\r\n /**\r\n * Content rendered when `loading` is `false`.\r\n */\r\n children?: React.ReactNode;\r\n}\r\n\r\nfunction normalizeSize(value: string | number | undefined): string | undefined {\r\n if (value === undefined) return undefined;\r\n return typeof value === 'number' ? `${value}px` : value;\r\n}\r\n\r\ntype SkeletonItemProps = Required<Pick<SkeletonProps, 'variant'>> &\r\n Pick<SkeletonProps, 'as' | 'width' | 'height' | 'borderRadius' | 'speed' | 'effect' | 'className' | 'style' | 'animate' | 'aria-label'> & {\r\n /** Per-item animation-delay in seconds (stagger effect). */\r\n delay?: number;\r\n };\r\n\r\nconst SkeletonItem = forwardRef<HTMLElement, SkeletonItemProps>(function SkeletonItem({\r\n variant = 'rect',\r\n as: Tag = 'span',\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n delay,\r\n effect = 'shimmer',\r\n className,\r\n style: styleProp,\r\n animate = true,\r\n 'aria-label': ariaLabel = 'Loading...',\r\n}, forwardedRef) {\r\n const innerRef = useRef<HTMLElement>(null);\r\n const isVisible = useIsVisible(innerRef);\r\n\r\n // Auto-square circle: when only width is given, mirror it as height\r\n const resolvedHeight =\r\n variant === 'circle' && height === undefined && width !== undefined ? width : height;\r\n\r\n const cssVars: Record<string, string> = {};\r\n if (speed !== undefined) {\r\n cssVars['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n cssVars['--skeleton-border-radius'] =\r\n typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n }\r\n if (delay !== undefined && delay > 0) {\r\n cssVars['--skeleton-animation-delay'] = `${delay}s`;\r\n }\r\n\r\n const style: CSSProperties = {\r\n width: normalizeSize(width),\r\n height: normalizeSize(resolvedHeight),\r\n ...cssVars,\r\n ...styleProp,\r\n };\r\n\r\n const classes = [\r\n 'skeleton',\r\n `skeleton--${variant}`,\r\n `skeleton--${effect}`,\r\n !animate && 'skeleton--no-animate',\r\n animate && !isVisible && 'skeleton--paused',\r\n className,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const El: any = Tag;\r\n return (\r\n <El\r\n ref={mergeRefs(innerRef, forwardedRef)}\r\n role=\"progressbar\"\r\n aria-busy=\"true\"\r\n aria-label={ariaLabel}\r\n className={classes}\r\n style={style}\r\n />\r\n );\r\n});\r\n\r\n/**\r\n * Animated content placeholder shown while data is loading.\r\n *\r\n * @example\r\n * // Single rect (default)\r\n * <Skeleton width={300} height={20} />\r\n *\r\n * // Paragraph of 3 text lines\r\n * <Skeleton variant=\"text\" count={3} stagger={0.08} />\r\n *\r\n * // Avatar circle\r\n * <Skeleton variant=\"circle\" width={48} />\r\n */\r\nexport const Skeleton = forwardRef<HTMLElement, SkeletonProps>(function Skeleton(props, ref) {\r\n const theme = useSkeletonTheme();\r\n\r\n const {\r\n loading = true,\r\n variant = theme.variant ?? 'rect',\r\n as,\r\n width,\r\n height,\r\n borderRadius = theme.borderRadius,\r\n count = 1,\r\n speed = theme.speed,\r\n effect = theme.effect ?? 'shimmer',\r\n stagger = 0,\r\n animate = theme.animate ?? true,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n children,\r\n } = props;\r\n\r\n if (!loading) {\r\n return <>{children}</>;\r\n }\r\n\r\n const itemProps: Omit<SkeletonItemProps, 'delay'> = {\r\n variant,\r\n as,\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n effect,\r\n animate,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n };\r\n\r\n if (variant === 'text') {\r\n return (\r\n <>\r\n {Array.from({ length: count }, (_, i) => (\r\n <SkeletonItem\r\n key={i}\r\n {...itemProps}\r\n variant=\"text\"\r\n delay={stagger > 0 ? i * stagger : undefined}\r\n ref={i === 0 ? ref : undefined}\r\n />\r\n ))}\r\n </>\r\n );\r\n }\r\n\r\n return <SkeletonItem {...itemProps} ref={ref} />;\r\n});\r\n\r\nexport default Skeleton;\r\n\r\n"],"names":["createContext","useContext","useState","useEffect","forwardRef","useRef"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@tavosud/sky-skeleton",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight zero-dependency React skeleton loader with shimmer effect",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "rollup -c",
13
+ "dev": "rollup -c -w",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "react",
20
+ "skeleton",
21
+ "loader",
22
+ "shimmer",
23
+ "placeholder",
24
+ "zero-dependency"
25
+ ],
26
+ "author": "tavosud",
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "react": ">=17.0.0",
30
+ "react-dom": ">=17.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@rollup/plugin-commonjs": "^25.0.8",
34
+ "@rollup/plugin-node-resolve": "^15.3.1",
35
+ "@rollup/plugin-typescript": "^11.1.6",
36
+ "@testing-library/jest-dom": "^6.9.1",
37
+ "@testing-library/react": "^16.3.2",
38
+ "@types/react": "^18.3.28",
39
+ "@vitejs/plugin-react": "^6.0.1",
40
+ "jsdom": "^29.0.1",
41
+ "rollup": "^4.60.0",
42
+ "rollup-plugin-postcss": "^4.0.2",
43
+ "tslib": "^2.8.1",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.1.0"
46
+ },
47
+ "sideEffects": false
48
+ }