auto-skeleton-react 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 auto-skeleton-react contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # auto-skeleton-react
2
+
3
+ Auto-generate skeleton loading screens from your existing React DOM structure. Zero manual skeleton creation for 70-80% of use cases.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install auto-skeleton-react
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { AutoSkeleton } from 'auto-skeleton-react';
15
+
16
+ function MyComponent() {
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ return (
20
+ <AutoSkeleton loading={loading}>
21
+ <UserProfile />
22
+ </AutoSkeleton>
23
+ );
24
+ }
25
+ ```
26
+
27
+ ## How It Works
28
+
29
+ 1. When `loading={true}`, the component renders children invisibly
30
+ 2. DOM is traversed and measured (dimensions, styles, attributes)
31
+ 3. Heuristics classify elements as text, image, icon, button, input, or container
32
+ 4. Skeleton blocks are rendered matching the original layout
33
+ 5. When `loading={false}`, the real content fades in
34
+
35
+ ## Features
36
+
37
+ - **Zero manual work** — wraps any React component, no separate skeleton needed
38
+ - **Layout preservation** — maintains flexbox, grid, margins, padding, gaps
39
+ - **Multi-line text** — auto-detects line count from element height
40
+ - **Table support** — preserves table structure (thead, tbody, tr, td)
41
+ - **Opt-out mechanism** — `data-no-skeleton` or `.no-skeleton` keeps elements visible during loading
42
+ - **Smooth transitions** — crossfade between skeleton and content
43
+ - **Configurable** — animation, colors, border radius, depth limits
44
+
45
+ ## Configuration
46
+
47
+ ```tsx
48
+ <AutoSkeleton
49
+ loading={loading}
50
+ config={{
51
+ animation: 'pulse', // 'pulse' | 'shimmer' | 'none'
52
+ baseColor: '#e0e0e0',
53
+ borderRadius: 4,
54
+ minTextHeight: 12,
55
+ maxDepth: 10,
56
+ }}
57
+ >
58
+ <MyComponent />
59
+ </AutoSkeleton>
60
+ ```
61
+
62
+ ## Opt-Out (No Skeleton)
63
+
64
+ Keep specific elements visible during loading:
65
+
66
+ ```tsx
67
+ {/* Using data attribute */}
68
+ <div data-no-skeleton>
69
+ <span>Always visible during loading</span>
70
+ </div>
71
+
72
+ {/* Using class name */}
73
+ <button className="no-skeleton">Cancel</button>
74
+ ```
75
+
76
+ ## Escape Hatches
77
+
78
+ ```tsx
79
+ {/* Force a specific skeleton type */}
80
+ <img data-skeleton-role="image" />
81
+ <span data-skeleton-role="text" />
82
+
83
+ {/* Ignore specific elements */}
84
+ <div data-skeleton-ignore>
85
+ <SensitiveComponent />
86
+ </div>
87
+ ```
88
+
89
+ ## Architecture
90
+
91
+ - **Leaf-only replacement** — preserves container structure, replaces only visual leaves
92
+ - **Wrapper + content pattern** — wrapper keeps spacing, inner div gets skeleton styling
93
+ - **Score-based inference** — extensible heuristics avoid brittle if/else chains
94
+ - **requestAnimationFrame** — ensures DOM is painted before measurement
95
+
96
+ ## Project Structure
97
+
98
+ ```
99
+ src/
100
+ ├── AutoSkeleton.tsx # Main component
101
+ ├── dom-traverser.ts # DOM measurement logic
102
+ ├── role-inferencer.ts # Heuristic-based classification
103
+ ├── skeleton-renderer.tsx # Skeleton block renderer
104
+ ├── types.ts # TypeScript interfaces
105
+ └── index.ts # Public API exports
106
+ ```
107
+
108
+ ## Known Limitations
109
+
110
+ - Client-side measurement only (expects hydration flash in SSR)
111
+ - Heuristics may misclassify custom components
112
+ - Not suitable for virtualized lists (>500 nodes)
113
+ - Performance cost on every loading transition
114
+ - Cannot predict dynamic text length
115
+
116
+ ## When to Use
117
+
118
+ - Standard CRUD forms, dashboards, profile pages
119
+ - Prototyping and MVPs
120
+ - Reducing 70-80% of manual skeleton work
121
+
122
+ ## When Not to Use
123
+
124
+ - Highly custom, pixel-perfect designs
125
+ - Virtualized tables or infinite scrollers
126
+ - Performance-critical views with frequent loading
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { SkeletonConfig } from './types';
3
+ interface AutoSkeletonProps {
4
+ loading: boolean;
5
+ children: React.ReactNode;
6
+ config?: Partial<SkeletonConfig>;
7
+ }
8
+ export declare function AutoSkeleton({ loading, children, config: userConfig, }: AutoSkeletonProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,578 @@
1
+ import { jsx as p, jsxs as ue } from "react/jsx-runtime";
2
+ import { useMemo as fe, useRef as ye, useState as Se, useLayoutEffect as de } from "react";
3
+ function ce(e) {
4
+ var r;
5
+ return e ? (r = e.hasAttribute) != null && r.call(e, "data-no-skeleton") ? !0 : ce(e.parentElement) : !1;
6
+ }
7
+ function he(e, r, i = 0) {
8
+ if (i > r.maxDepth) return null;
9
+ if (ce(e)) {
10
+ const c = e.getBoundingClientRect(), l = window.getComputedStyle(e), o = {};
11
+ for (const g of e.attributes)
12
+ o[g.name] = g.value;
13
+ return {
14
+ rect: c,
15
+ style: l,
16
+ tagName: e.tagName,
17
+ textContent: e.textContent,
18
+ attributes: o,
19
+ children: [],
20
+ passthrough: !0,
21
+ passthroughHtml: e.outerHTML
22
+ };
23
+ }
24
+ for (const c of r.ignoreSelectors)
25
+ if (e.matches(c)) return null;
26
+ const t = e.getBoundingClientRect(), s = window.getComputedStyle(e), y = e.tagName === "IMG", n = y ? e : null, S = n == null ? void 0 : n.getAttribute("src"), m = (n == null ? void 0 : n.src) || "", f = (n == null ? void 0 : n.naturalWidth) === 0 || (n == null ? void 0 : n.naturalHeight) === 0, a = !S || S === "", w = m === window.location.href || m.endsWith("/"), R = y && (a || w || f);
27
+ let v = t;
28
+ if (R && n) {
29
+ let c = 0, l = 0;
30
+ const o = e.parentElement, g = o ? window.getComputedStyle(o) : null;
31
+ let x = 0;
32
+ if (o) {
33
+ const d = o.getBoundingClientRect(), H = parseFloat((g == null ? void 0 : g.paddingLeft) || "0"), L = parseFloat((g == null ? void 0 : g.paddingRight) || "0");
34
+ x = d.width - H - L;
35
+ }
36
+ const b = n.style.width, C = n.style.height;
37
+ if (C && !C.includes("%")) {
38
+ const d = parseFloat(C);
39
+ !isNaN(d) && d > 0 && (l = d);
40
+ }
41
+ if (l === 0) {
42
+ const d = parseFloat(s.height);
43
+ !isNaN(d) && d > 50 && (l = d);
44
+ }
45
+ if (b)
46
+ if (b.includes("%")) {
47
+ const d = parseFloat(b) / 100;
48
+ !isNaN(d) && x > 0 && (c = x * d);
49
+ } else {
50
+ const d = parseFloat(b);
51
+ !isNaN(d) && d > 0 && (c = d);
52
+ }
53
+ if (c === 0 && x > 0 && (c = x), c === 0) {
54
+ const d = n.getAttribute("width");
55
+ d && (c = parseFloat(d));
56
+ }
57
+ if (l === 0) {
58
+ const d = n.getAttribute("height");
59
+ d && (l = parseFloat(d));
60
+ }
61
+ c > 0 && l > 0 && (v = {
62
+ x: t.x,
63
+ y: t.y,
64
+ width: c,
65
+ height: l,
66
+ top: t.top,
67
+ right: t.right,
68
+ bottom: t.bottom,
69
+ left: t.left,
70
+ toJSON: t.toJSON.bind(t)
71
+ });
72
+ }
73
+ if (v.width === 0 || v.height === 0 || s.display === "none" || s.visibility === "hidden")
74
+ return null;
75
+ const k = {};
76
+ for (const c of e.attributes)
77
+ k[c.name] = c.value;
78
+ const A = [];
79
+ for (const c of e.children)
80
+ if (c instanceof HTMLElement) {
81
+ const l = he(c, r, i + 1);
82
+ l && A.push(l);
83
+ }
84
+ return {
85
+ rect: v,
86
+ style: s,
87
+ tagName: e.tagName,
88
+ textContent: e.textContent,
89
+ attributes: k,
90
+ children: A
91
+ };
92
+ }
93
+ const pe = ["TABLE", "THEAD", "TBODY", "TFOOT", "TR"], ge = ["TH", "TD"];
94
+ function me(e) {
95
+ var S, m;
96
+ const { tagName: r, textContent: i, children: t, style: s, rect: y } = e;
97
+ if (y.width * y.height < 100 || pe.includes(r) || ge.includes(r)) return !1;
98
+ if (r.match(/^(IMG|BUTTON|INPUT|TEXTAREA|SELECT|SVG)$/) || i != null && i.trim() && t.length === 0) return !0;
99
+ if (t.length === 1 && (i != null && i.trim())) {
100
+ const f = t[0];
101
+ if ((S = f.textContent) != null && S.trim() && f.children.length === 0) return !0;
102
+ }
103
+ return t.length > 1 || (m = s.display) != null && m.match(/flex|grid/) ? !1 : t.length === 0;
104
+ }
105
+ function B(e, r) {
106
+ var c;
107
+ if (e.passthrough)
108
+ return {
109
+ type: "passthrough",
110
+ rect: {
111
+ x: e.rect.x,
112
+ y: e.rect.y,
113
+ width: e.rect.width,
114
+ height: e.rect.height
115
+ },
116
+ passthrough: !0,
117
+ passthroughHtml: e.passthroughHtml
118
+ };
119
+ const { rect: i, style: t, tagName: s, textContent: y, attributes: n, children: S } = e, m = n["data-skeleton-role"];
120
+ if (m) {
121
+ const l = m;
122
+ let o = 1;
123
+ if (l === "text") {
124
+ const g = parseFloat(t.lineHeight), x = parseFloat(t.fontSize), b = g || x * 1.2;
125
+ b > 0 && i.height > b && (o = Math.ceil(i.height / b));
126
+ }
127
+ return {
128
+ type: l,
129
+ rect: {
130
+ x: i.x,
131
+ y: i.y,
132
+ width: i.width,
133
+ height: i.height
134
+ },
135
+ borderRadius: parseFloat(t.borderRadius) || r.borderRadius,
136
+ lines: l === "text" ? o : void 0,
137
+ preservedStyles: {
138
+ display: t.display,
139
+ margin: t.margin,
140
+ padding: t.padding,
141
+ lineHeight: t.lineHeight,
142
+ flex: t.flex,
143
+ flexGrow: t.flexGrow,
144
+ flexShrink: t.flexShrink,
145
+ flexBasis: t.flexBasis,
146
+ alignSelf: t.alignSelf,
147
+ justifySelf: t.justifySelf,
148
+ gridColumn: t.gridColumn,
149
+ gridRow: t.gridRow,
150
+ gridArea: t.gridArea,
151
+ verticalAlign: t.verticalAlign
152
+ }
153
+ };
154
+ }
155
+ if (pe.includes(s)) {
156
+ const l = S.map((o) => B(o, r)).filter((o) => o.type !== "skip");
157
+ return {
158
+ type: s.toLowerCase(),
159
+ tagName: s,
160
+ rect: {
161
+ x: i.x,
162
+ y: i.y,
163
+ width: i.width,
164
+ height: i.height
165
+ },
166
+ children: l,
167
+ preservedStyles: {
168
+ display: t.display,
169
+ margin: t.margin,
170
+ padding: t.padding,
171
+ border: t.border,
172
+ borderBottom: t.borderBottom,
173
+ backgroundColor: t.backgroundColor,
174
+ width: t.width,
175
+ borderCollapse: t.borderCollapse,
176
+ tableLayout: t.tableLayout
177
+ }
178
+ };
179
+ }
180
+ if (ge.includes(s)) {
181
+ const l = S.map((o) => B(o, r)).filter((o) => o.type !== "skip");
182
+ return {
183
+ type: s.toLowerCase(),
184
+ tagName: s,
185
+ rect: {
186
+ x: i.x,
187
+ y: i.y,
188
+ width: i.width,
189
+ height: i.height
190
+ },
191
+ children: l,
192
+ preservedStyles: {
193
+ display: t.display,
194
+ margin: t.margin,
195
+ padding: t.padding,
196
+ border: t.border,
197
+ borderBottom: t.borderBottom,
198
+ backgroundColor: t.backgroundColor,
199
+ textAlign: t.textAlign,
200
+ verticalAlign: t.verticalAlign,
201
+ width: t.width
202
+ }
203
+ };
204
+ }
205
+ if (!me(e)) {
206
+ const l = S.map((o) => B(o, r)).filter((o) => o.type !== "skip");
207
+ return {
208
+ type: "container",
209
+ rect: {
210
+ x: i.x,
211
+ y: i.y,
212
+ width: i.width,
213
+ height: i.height
214
+ },
215
+ display: t.display,
216
+ gap: t.gap,
217
+ borderRadius: parseFloat(t.borderRadius) || 0,
218
+ children: l,
219
+ preservedStyles: {
220
+ display: t.display,
221
+ margin: t.margin,
222
+ padding: t.padding,
223
+ border: t.border,
224
+ boxSizing: t.boxSizing,
225
+ justifyContent: t.justifyContent,
226
+ alignItems: t.alignItems,
227
+ flexDirection: t.flexDirection,
228
+ minHeight: t.minHeight,
229
+ gridTemplateColumns: t.gridTemplateColumns,
230
+ gridTemplateRows: t.gridTemplateRows
231
+ }
232
+ };
233
+ }
234
+ const f = i.width * i.height, a = {
235
+ text: 0,
236
+ image: 0,
237
+ icon: 0,
238
+ button: 0,
239
+ input: 0,
240
+ container: 0,
241
+ skip: 0
242
+ };
243
+ y && y.trim().length > 0 && (a.text += 40), i.height < r.minTextHeight * 3 && i.height >= r.minTextHeight && (a.text += 30), s.match(/^(P|SPAN|H[1-6]|LABEL|A|DIV)$/) && (a.text += 20);
244
+ const w = parseFloat(t.fontSize);
245
+ w > 0 && w < 100 && (a.text += 20), s === "IMG" && (a.image += 100), n.role === "img" && (a.image += 60), t.backgroundImage !== "none" && (a.image += 50), f > r.minImageSize ** 2 && (a.image += 30), s === "SVG" && (a.icon += 70);
246
+ const R = i.width / i.height;
247
+ f < r.iconMaxSize ** 2 && R > 0.8 && R < 1.2 && (a.icon += 40), s === "BUTTON" && (a.button += 80), n.role === "button" && (a.button += 60), t.cursor === "pointer" && f < 2e4 && (a.button += 30), s.match(/^(INPUT|TEXTAREA|SELECT)$/) && (a.input += 80), n.contenteditable && (a.input += 50), S.length > 1 && (a.container += 50), (c = t.display) != null && c.match(/flex|grid/) && (a.container += 30), f < 100 && (a.skip += 50), (i.height < 5 || i.width < 5) && (a.skip += 40);
248
+ const v = Object.entries(a).filter(
249
+ ([l]) => l !== "container"
250
+ ).reduce((l, o) => o[1] > l[1] ? o : l), k = v[1] > 30 ? v[0] : "text";
251
+ let A = 1;
252
+ if (k === "text") {
253
+ const l = parseFloat(t.lineHeight), o = parseFloat(t.fontSize), g = l || o * 1.2;
254
+ g > 0 && i.height > g && (A = Math.ceil(i.height / g));
255
+ }
256
+ return {
257
+ type: k,
258
+ rect: {
259
+ x: i.x,
260
+ y: i.y,
261
+ width: i.width,
262
+ height: i.height
263
+ },
264
+ borderRadius: parseFloat(t.borderRadius) || r.borderRadius,
265
+ lines: k === "text" ? A : void 0,
266
+ preservedStyles: {
267
+ display: t.display,
268
+ margin: t.margin,
269
+ padding: t.padding,
270
+ lineHeight: t.lineHeight,
271
+ flex: t.flex,
272
+ flexGrow: t.flexGrow,
273
+ flexShrink: t.flexShrink,
274
+ flexBasis: t.flexBasis,
275
+ alignSelf: t.alignSelf,
276
+ justifySelf: t.justifySelf,
277
+ gridColumn: t.gridColumn,
278
+ gridRow: t.gridRow,
279
+ gridArea: t.gridArea,
280
+ verticalAlign: t.verticalAlign
281
+ }
282
+ };
283
+ }
284
+ function be({
285
+ blueprint: e,
286
+ baseColor: r,
287
+ highlightColor: i,
288
+ animation: t
289
+ }) {
290
+ return T(e, r, i, t);
291
+ }
292
+ function T(e, r, i, t, s) {
293
+ var a, w, R, v, k, A, c, l, o, g, x, b, C, d, H, L, G, z, E, j, D, M, O, W, _, P, U, $, V, J, X, q, Y, K, Q, Z, ee, te, ie, re, se, le, ne, ae, oe;
294
+ if (e.type === "passthrough")
295
+ return e.passthroughHtml ? /* @__PURE__ */ p(
296
+ "div",
297
+ {
298
+ style: { display: "contents" },
299
+ dangerouslySetInnerHTML: { __html: e.passthroughHtml }
300
+ },
301
+ s
302
+ ) : null;
303
+ if (e.type === "skip") return null;
304
+ if (e.type === "table")
305
+ return /* @__PURE__ */ p(
306
+ "table",
307
+ {
308
+ style: {
309
+ width: ((a = e.preservedStyles) == null ? void 0 : a.width) || "100%",
310
+ borderCollapse: (w = e.preservedStyles) == null ? void 0 : w.borderCollapse,
311
+ tableLayout: (R = e.preservedStyles) == null ? void 0 : R.tableLayout
312
+ },
313
+ children: (v = e.children) == null ? void 0 : v.map(
314
+ (h, u) => T(h, r, i, t, u)
315
+ )
316
+ },
317
+ s
318
+ );
319
+ if (e.type === "thead")
320
+ return /* @__PURE__ */ p("thead", { children: (k = e.children) == null ? void 0 : k.map(
321
+ (h, u) => T(h, r, i, t, u)
322
+ ) }, s);
323
+ if (e.type === "tbody")
324
+ return /* @__PURE__ */ p("tbody", { children: (A = e.children) == null ? void 0 : A.map(
325
+ (h, u) => T(h, r, i, t, u)
326
+ ) }, s);
327
+ if (e.type === "tr")
328
+ return /* @__PURE__ */ p("tr", { children: (c = e.children) == null ? void 0 : c.map(
329
+ (h, u) => T(h, r, i, t, u)
330
+ ) }, s);
331
+ if (e.type === "th" || e.type === "td") {
332
+ const h = e.type, u = {
333
+ padding: (l = e.preservedStyles) == null ? void 0 : l.padding,
334
+ borderBottom: (o = e.preservedStyles) == null ? void 0 : o.borderBottom,
335
+ backgroundColor: (g = e.preservedStyles) == null ? void 0 : g.backgroundColor,
336
+ textAlign: (x = e.preservedStyles) == null ? void 0 : x.textAlign,
337
+ verticalAlign: (b = e.preservedStyles) == null ? void 0 : b.verticalAlign,
338
+ width: (C = e.preservedStyles) == null ? void 0 : C.width
339
+ };
340
+ return e.children && e.children.length > 0 ? /* @__PURE__ */ p(h, { style: u, children: e.children.map(
341
+ (N, I) => T(N, r, i, t, I)
342
+ ) }, s) : /* @__PURE__ */ p(h, { style: u, children: /* @__PURE__ */ p("div", { style: {
343
+ width: "80%",
344
+ height: 16,
345
+ borderRadius: 4,
346
+ backgroundColor: r,
347
+ animation: t === "pulse" ? "skeleton-pulse 2s ease-in-out infinite" : void 0
348
+ } }) }, s);
349
+ }
350
+ if (e.type === "container" && e.children && e.children.length > 0) {
351
+ const h = ((d = e.preservedStyles) == null ? void 0 : d.border) || "", u = h && !h.includes("0px") && !h.startsWith("none"), F = e.display === "grid";
352
+ return /* @__PURE__ */ p(
353
+ "div",
354
+ {
355
+ style: {
356
+ display: e.display || "block",
357
+ gap: e.gap,
358
+ width: e.rect.width,
359
+ // Don't set fixed height - let container size naturally from children
360
+ // This avoids issues with incorrect height measurement from alt text
361
+ boxSizing: "border-box",
362
+ padding: (H = e.preservedStyles) == null ? void 0 : H.padding,
363
+ border: u ? (L = e.preservedStyles) == null ? void 0 : L.border : void 0,
364
+ borderRadius: e.borderRadius,
365
+ justifyContent: (G = e.preservedStyles) == null ? void 0 : G.justifyContent,
366
+ alignItems: (z = e.preservedStyles) == null ? void 0 : z.alignItems,
367
+ flexDirection: (E = e.preservedStyles) == null ? void 0 : E.flexDirection,
368
+ minHeight: (j = e.preservedStyles) == null ? void 0 : j.minHeight,
369
+ // For grid: use auto columns to let children size naturally
370
+ // For non-grid: use measured template
371
+ gridTemplateColumns: F ? `repeat(${e.children.length}, 1fr)` : (D = e.preservedStyles) == null ? void 0 : D.gridTemplateColumns,
372
+ gridTemplateRows: (M = e.preservedStyles) == null ? void 0 : M.gridTemplateRows
373
+ },
374
+ children: e.children.map(
375
+ (N, I) => T(N, r, i, t, I)
376
+ )
377
+ },
378
+ s
379
+ );
380
+ }
381
+ if (e.type === "image") {
382
+ const h = {
383
+ width: e.rect.width,
384
+ height: e.rect.height,
385
+ borderRadius: e.borderRadius,
386
+ backgroundColor: r,
387
+ display: "block",
388
+ boxSizing: "border-box",
389
+ margin: (O = e.preservedStyles) == null ? void 0 : O.margin
390
+ };
391
+ return t === "pulse" && (h.animation = "skeleton-pulse 2s ease-in-out infinite"), /* @__PURE__ */ p("div", { style: h, className: "skeleton-image" }, s);
392
+ }
393
+ if (e.type === "button" || e.type === "input") {
394
+ const h = {
395
+ // Lock exact dimensions
396
+ width: e.rect.width,
397
+ height: e.rect.height,
398
+ minWidth: e.rect.width,
399
+ minHeight: e.rect.height,
400
+ boxSizing: "border-box",
401
+ // Preserve flex-item properties
402
+ margin: (W = e.preservedStyles) == null ? void 0 : W.margin,
403
+ flex: (_ = e.preservedStyles) == null ? void 0 : _.flex,
404
+ flexGrow: (P = e.preservedStyles) != null && P.flexGrow ? Number(e.preservedStyles.flexGrow) : void 0,
405
+ flexShrink: 0,
406
+ // Prevent flex compression
407
+ flexBasis: (U = e.preservedStyles) == null ? void 0 : U.flexBasis,
408
+ alignSelf: ($ = e.preservedStyles) == null ? void 0 : $.alignSelf,
409
+ justifySelf: (V = e.preservedStyles) == null ? void 0 : V.justifySelf,
410
+ // Grid properties
411
+ gridColumn: (J = e.preservedStyles) == null ? void 0 : J.gridColumn,
412
+ gridRow: (X = e.preservedStyles) == null ? void 0 : X.gridRow,
413
+ gridArea: (q = e.preservedStyles) == null ? void 0 : q.gridArea,
414
+ // Visual
415
+ borderRadius: e.borderRadius,
416
+ backgroundColor: r,
417
+ // Preserve inline nature
418
+ display: "inline-block",
419
+ verticalAlign: ((Y = e.preservedStyles) == null ? void 0 : Y.verticalAlign) || "middle"
420
+ };
421
+ return t === "pulse" && (h.animation = "skeleton-pulse 2s ease-in-out infinite"), /* @__PURE__ */ p("div", { style: h, className: `skeleton-${e.type}` }, s);
422
+ }
423
+ const n = {
424
+ // Only margin for spacing between siblings
425
+ margin: (K = e.preservedStyles) == null ? void 0 : K.margin,
426
+ // Flex-item properties
427
+ flex: (Q = e.preservedStyles) == null ? void 0 : Q.flex,
428
+ flexGrow: (Z = e.preservedStyles) != null && Z.flexGrow ? Number(e.preservedStyles.flexGrow) : void 0,
429
+ flexShrink: (ee = e.preservedStyles) != null && ee.flexShrink ? Number(e.preservedStyles.flexShrink) : void 0,
430
+ flexBasis: (te = e.preservedStyles) == null ? void 0 : te.flexBasis,
431
+ alignSelf: (ie = e.preservedStyles) == null ? void 0 : ie.alignSelf,
432
+ // Grid-item properties
433
+ gridColumn: (re = e.preservedStyles) == null ? void 0 : re.gridColumn,
434
+ gridRow: (se = e.preservedStyles) == null ? void 0 : se.gridRow,
435
+ gridArea: (le = e.preservedStyles) == null ? void 0 : le.gridArea
436
+ }, S = ((ne = e.preservedStyles) == null ? void 0 : ne.display) === "inline" ? "inline-block" : "block", m = {
437
+ width: e.rect.width,
438
+ height: e.rect.height,
439
+ borderRadius: e.borderRadius,
440
+ backgroundColor: r,
441
+ display: S,
442
+ verticalAlign: ((ae = e.preservedStyles) == null ? void 0 : ae.verticalAlign) || "middle"
443
+ };
444
+ if (t === "pulse" && (m.animation = "skeleton-pulse 2s ease-in-out infinite"), e.type === "text" && e.lines && e.lines > 1) {
445
+ const h = parseFloat(((oe = e.preservedStyles) == null ? void 0 : oe.lineHeight) || "1.2"), u = e.rect.height / e.lines;
446
+ return /* @__PURE__ */ p(
447
+ "div",
448
+ {
449
+ style: {
450
+ ...n,
451
+ display: "flex",
452
+ flexDirection: "column",
453
+ gap: h > u ? h - u : 4
454
+ },
455
+ children: Array.from({ length: e.lines }).map((F, N) => /* @__PURE__ */ p(
456
+ "div",
457
+ {
458
+ style: {
459
+ ...m,
460
+ height: u,
461
+ width: N === e.lines - 1 ? e.rect.width * 0.7 : e.rect.width
462
+ // Last line shorter
463
+ },
464
+ className: `skeleton-${e.type}`
465
+ },
466
+ N
467
+ ))
468
+ },
469
+ s
470
+ );
471
+ }
472
+ const f = `skeleton-${e.type}`;
473
+ return /* @__PURE__ */ p("div", { style: n, children: /* @__PURE__ */ p("div", { style: m, className: f }) }, s);
474
+ }
475
+ const ve = {
476
+ animation: "pulse",
477
+ baseColor: "#e0e0e0",
478
+ highlightColor: "#f5f5f5",
479
+ borderRadius: 4,
480
+ minTextHeight: 12,
481
+ minImageSize: 32,
482
+ iconMaxSize: 48,
483
+ maxDepth: 10,
484
+ ignoreSelectors: [".no-skeleton", "[data-skeleton-ignore]"]
485
+ };
486
+ function ke({
487
+ loading: e,
488
+ children: r,
489
+ config: i
490
+ }) {
491
+ const t = fe(
492
+ () => ({ ...ve, ...i }),
493
+ [i]
494
+ ), s = ye(null), [y, n] = Se(null);
495
+ return de(() => {
496
+ if (!e || !s.current)
497
+ return;
498
+ const f = requestAnimationFrame(() => {
499
+ if (s.current)
500
+ try {
501
+ const a = he(s.current, t);
502
+ if (a) {
503
+ const w = B(a, t);
504
+ n(w);
505
+ }
506
+ } catch (a) {
507
+ console.error("AutoSkeleton measurement failed:", a), n(null);
508
+ }
509
+ });
510
+ return () => cancelAnimationFrame(f);
511
+ }, [e, t]), de(() => {
512
+ if (e) return;
513
+ const f = setTimeout(() => {
514
+ n(null);
515
+ }, 300);
516
+ return () => clearTimeout(f);
517
+ }, [e]), /* @__PURE__ */ ue("div", { style: { position: "relative" }, children: [
518
+ /* @__PURE__ */ p("style", { children: `
519
+ @keyframes skeleton-pulse {
520
+ 0%, 100% { opacity: 1; }
521
+ 50% { opacity: 0.5; }
522
+ }
523
+ .auto-skeleton-fade {
524
+ transition: opacity 0.3s ease-out;
525
+ }
526
+ ` }),
527
+ /* @__PURE__ */ p(
528
+ "div",
529
+ {
530
+ className: "auto-skeleton-fade",
531
+ style: { opacity: e && y !== null ? 0 : 1 },
532
+ children: r
533
+ }
534
+ ),
535
+ (e || y !== null) && y && /* @__PURE__ */ p(
536
+ "div",
537
+ {
538
+ className: "auto-skeleton-fade",
539
+ style: {
540
+ position: "absolute",
541
+ top: 0,
542
+ left: 0,
543
+ right: 0,
544
+ opacity: e ? 1 : 0,
545
+ pointerEvents: e ? "auto" : "none"
546
+ },
547
+ children: /* @__PURE__ */ p(
548
+ be,
549
+ {
550
+ blueprint: y,
551
+ baseColor: t.baseColor,
552
+ highlightColor: t.highlightColor,
553
+ animation: t.animation
554
+ }
555
+ )
556
+ }
557
+ ),
558
+ e && /* @__PURE__ */ p(
559
+ "div",
560
+ {
561
+ ref: s,
562
+ style: {
563
+ opacity: 0,
564
+ position: "absolute",
565
+ top: 0,
566
+ left: 0,
567
+ right: 0,
568
+ pointerEvents: "none",
569
+ zIndex: -1
570
+ },
571
+ children: r
572
+ }
573
+ )
574
+ ] });
575
+ }
576
+ export {
577
+ ke as AutoSkeleton
578
+ };
@@ -0,0 +1,2 @@
1
+ import type { ElementMeasurement, SkeletonConfig } from './types';
2
+ export declare function traverseAndMeasure(element: HTMLElement, config: SkeletonConfig, depth?: number): ElementMeasurement | null;
@@ -0,0 +1,2 @@
1
+ export { AutoSkeleton } from './AutoSkeleton';
2
+ export type { SkeletonConfig, SkeletonNode } from './types';
@@ -0,0 +1,2 @@
1
+ import type { ElementMeasurement, SkeletonConfig, SkeletonNode } from './types';
2
+ export declare function inferSkeletonNode(measurement: ElementMeasurement, config: SkeletonConfig): SkeletonNode;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { SkeletonNode } from './types';
3
+ interface SkeletonRendererProps {
4
+ blueprint: SkeletonNode;
5
+ baseColor: string;
6
+ highlightColor: string;
7
+ animation: 'pulse' | 'shimmer' | 'none';
8
+ }
9
+ export declare function SkeletonRenderer({ blueprint, baseColor, highlightColor, animation, }: SkeletonRendererProps): React.ReactNode;
10
+ export {};
@@ -0,0 +1,68 @@
1
+ export interface SkeletonConfig {
2
+ animation: 'pulse' | 'shimmer' | 'none';
3
+ baseColor: string;
4
+ highlightColor: string;
5
+ borderRadius: number;
6
+ minTextHeight: number;
7
+ minImageSize: number;
8
+ iconMaxSize: number;
9
+ maxDepth: number;
10
+ ignoreSelectors: string[];
11
+ }
12
+ export interface ElementMeasurement {
13
+ rect: DOMRect;
14
+ style: CSSStyleDeclaration;
15
+ tagName: string;
16
+ textContent: string | null;
17
+ attributes: Record<string, string>;
18
+ children: ElementMeasurement[];
19
+ passthrough?: boolean;
20
+ passthroughHtml?: string;
21
+ }
22
+ export interface SkeletonNode {
23
+ type: 'text' | 'image' | 'icon' | 'button' | 'input' | 'container' | 'skip' | 'passthrough' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td';
24
+ rect: {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ };
30
+ borderRadius?: number;
31
+ display?: string;
32
+ gap?: string;
33
+ children?: SkeletonNode[];
34
+ passthrough?: boolean;
35
+ passthroughHtml?: string;
36
+ preservedStyles?: {
37
+ display: string;
38
+ margin: string;
39
+ padding: string;
40
+ border?: string;
41
+ boxSizing?: string;
42
+ lineHeight?: string;
43
+ flex?: string;
44
+ flexGrow?: string;
45
+ flexShrink?: string;
46
+ flexBasis?: string;
47
+ alignSelf?: string;
48
+ justifySelf?: string;
49
+ justifyContent?: string;
50
+ alignItems?: string;
51
+ flexDirection?: string;
52
+ minHeight?: string;
53
+ gridTemplateColumns?: string;
54
+ gridTemplateRows?: string;
55
+ gridColumn?: string;
56
+ gridRow?: string;
57
+ gridArea?: string;
58
+ verticalAlign?: string;
59
+ width?: string;
60
+ textAlign?: string;
61
+ backgroundColor?: string;
62
+ borderBottom?: string;
63
+ borderCollapse?: string;
64
+ tableLayout?: string;
65
+ };
66
+ lines?: number;
67
+ tagName?: string;
68
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "auto-skeleton-react",
3
+ "version": "1.0.0",
4
+ "description": "Auto-generate skeleton loading screens from your existing React DOM structure",
5
+ "type": "module",
6
+ "main": "dist/auto-skeleton-react.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/auto-skeleton-react.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "vite build && tsc -p tsconfig.lib.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "react",
23
+ "skeleton",
24
+ "loading",
25
+ "auto-skeleton",
26
+ "placeholder",
27
+ "skeleton-screen",
28
+ "loading-state"
29
+ ],
30
+ "license": "MIT",
31
+ "peerDependencies": {
32
+ "react": ">=18.0.0",
33
+ "react-dom": ">=18.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.2.3",
37
+ "@types/react": "^18.2.43",
38
+ "@types/react-dom": "^18.2.17",
39
+ "react": "^18.2.0",
40
+ "react-dom": "^18.2.0",
41
+ "typescript": "^5.3.3",
42
+ "vite": "^5.0.8"
43
+ }
44
+ }