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 +21 -0
- package/README.md +130 -0
- package/dist/AutoSkeleton.d.ts +9 -0
- package/dist/auto-skeleton-react.js +578 -0
- package/dist/dom-traverser.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/role-inferencer.d.ts +2 -0
- package/dist/skeleton-renderer.d.ts +10 -0
- package/dist/types.d.ts +68 -0
- package/package.json +44 -0
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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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 {};
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|