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 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
+ [![npm version](https://img.shields.io/npm/v/react-auto-skeleton.svg)](https://www.npmjs.com/package/react-auto-skeleton)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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":"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","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"]}
@@ -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 };
@@ -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"]}
@@ -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":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
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
+ }