instaskeleton 0.1.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 +298 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# instaskeleton
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/instaskeleton)
|
|
4
|
+
[](https://bundlephobia.com/package/instaskeleton)
|
|
5
|
+
[](https://github.com/LittleBoy9/instaskeleton/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Ultra-light React skeleton loader with **zero DOM scanning**, **zero layout measurement**, and **zero lag**.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
~1.2 KB gzipped (JS) + ~0.45 KB (CSS) = ~1.65 KB total
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why instaskeleton?
|
|
14
|
+
|
|
15
|
+
Most skeleton libraries either:
|
|
16
|
+
- Require separate skeleton components for every UI element
|
|
17
|
+
- Scan the DOM at runtime to generate placeholders (slow, causes layout shifts)
|
|
18
|
+
|
|
19
|
+
**instaskeleton** takes a different approach:
|
|
20
|
+
- **Zero DOM scanning** — no runtime layout measurement
|
|
21
|
+
- **Zero work when not loading** — early exit skips all computation
|
|
22
|
+
- **Infer from JSX** — automatic skeleton generation from your React tree
|
|
23
|
+
- **Manual schema** — pixel-perfect control when you need it
|
|
24
|
+
- **LRU cache** — repeated renders are instant (100-entry limit prevents memory leaks)
|
|
25
|
+
- **Reduced motion support** — respects `prefers-reduced-motion`
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install instaskeleton
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Import the bundled styles once:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import 'instaskeleton/styles.css';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Use inference when you want the fastest setup:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { InstaSkeleton } from 'instaskeleton';
|
|
45
|
+
import 'instaskeleton/styles.css';
|
|
46
|
+
|
|
47
|
+
type Product = {
|
|
48
|
+
title: string;
|
|
49
|
+
price: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ProductCard({
|
|
53
|
+
loading,
|
|
54
|
+
product
|
|
55
|
+
}: {
|
|
56
|
+
loading: boolean;
|
|
57
|
+
product: Product;
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<InstaSkeleton loading={loading} infer cacheKey="product-card-v1">
|
|
61
|
+
<article>
|
|
62
|
+
<img src="/cover.png" alt="" />
|
|
63
|
+
<h3>{product.title}</h3>
|
|
64
|
+
<p>{product.price}</p>
|
|
65
|
+
<button>Add to cart</button>
|
|
66
|
+
</article>
|
|
67
|
+
</InstaSkeleton>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Manual Schema
|
|
73
|
+
|
|
74
|
+
Use manual schema when you want the skeleton to resemble the inner component structure more closely.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { InstaSkeleton, type SkeletonNode } from 'instaskeleton';
|
|
78
|
+
|
|
79
|
+
const articleCardSchema: SkeletonNode[] = [
|
|
80
|
+
{ type: 'rect', height: '12rem', radius: '1rem' },
|
|
81
|
+
{ type: 'line', width: '68%' },
|
|
82
|
+
{ type: 'line', width: '92%' },
|
|
83
|
+
{ type: 'line', width: '40%' }
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function ArticleCard({ loading }: { loading: boolean }) {
|
|
87
|
+
return (
|
|
88
|
+
<InstaSkeleton loading={loading} schema={articleCardSchema} infer={false}>
|
|
89
|
+
<article>
|
|
90
|
+
<img src="/hero.png" alt="" />
|
|
91
|
+
<h3>How we removed DOM scanning from loading states</h3>
|
|
92
|
+
<p>Manual schema mirrors the actual card anatomy.</p>
|
|
93
|
+
<small>5 min read</small>
|
|
94
|
+
</article>
|
|
95
|
+
</InstaSkeleton>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## HOC Pattern
|
|
101
|
+
|
|
102
|
+
Use `withInstaSkeleton` when you want to preconfigure loading behavior for a reusable component.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { withInstaSkeleton } from 'instaskeleton';
|
|
106
|
+
|
|
107
|
+
function StatCard({ label, value }: { label: string; value: string }) {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<span>{label}</span>
|
|
111
|
+
<strong>{value}</strong>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const StatCardWithSkeleton = withInstaSkeleton(StatCard, {
|
|
117
|
+
skeleton: [
|
|
118
|
+
{ type: 'line', width: '35%', height: '0.75rem' },
|
|
119
|
+
{ type: 'rect', height: '3rem', radius: '0.75rem' }
|
|
120
|
+
],
|
|
121
|
+
infer: false,
|
|
122
|
+
cacheKey: 'stat-card'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
<StatCardWithSkeleton loading={isLoading} label="Downloads" value="12.4k" />;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API Reference
|
|
129
|
+
|
|
130
|
+
### `<InstaSkeleton>`
|
|
131
|
+
|
|
132
|
+
The main component for wrapping content with skeleton loading states.
|
|
133
|
+
|
|
134
|
+
| Prop | Type | Default | Description |
|
|
135
|
+
|------|------|---------|-------------|
|
|
136
|
+
| `loading` | `boolean` | required | Show skeleton when `true`, children when `false` |
|
|
137
|
+
| `children` | `ReactNode` | required | Content to display when not loading |
|
|
138
|
+
| `schema` | `SkeletonNode \| SkeletonNode[]` | `undefined` | Manual skeleton definition |
|
|
139
|
+
| `infer` | `boolean` | `true` | Auto-generate skeleton from children |
|
|
140
|
+
| `cacheKey` | `string` | `undefined` | Cache inferred schema for reuse |
|
|
141
|
+
| `className` | `string` | `undefined` | Additional CSS class for the skeleton root |
|
|
142
|
+
| `animation` | `'shimmer' \| 'pulse' \| 'none'` | `'shimmer'` | Animation style |
|
|
143
|
+
| `inferOptions` | `InferOptions` | `{}` | Control inference behavior |
|
|
144
|
+
|
|
145
|
+
### `InferOptions`
|
|
146
|
+
|
|
147
|
+
Fine-tune the inference algorithm:
|
|
148
|
+
|
|
149
|
+
| Option | Type | Default | Description |
|
|
150
|
+
|--------|------|---------|-------------|
|
|
151
|
+
| `maxDepth` | `number` | `6` | Maximum JSX tree depth to traverse |
|
|
152
|
+
| `maxNodes` | `number` | `120` | Maximum nodes to process |
|
|
153
|
+
| `textLineHeight` | `string \| number` | `'0.95rem'` | Height for text line skeletons |
|
|
154
|
+
|
|
155
|
+
### `withInstaSkeleton(Component, options)`
|
|
156
|
+
|
|
157
|
+
HOC to create a skeleton-wrapped version of any component.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
const WrappedComponent = withInstaSkeleton(MyComponent, {
|
|
161
|
+
skeleton: [...], // Manual schema
|
|
162
|
+
infer: false, // Disable inference
|
|
163
|
+
cacheKey: 'my-comp', // Cache key
|
|
164
|
+
className: 'custom', // Root class
|
|
165
|
+
animation: 'pulse', // Animation style
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Adds `loading` prop to the component
|
|
169
|
+
<WrappedComponent loading={isLoading} {...props} />
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `clearInstaSkeletonCache(key?)`
|
|
173
|
+
|
|
174
|
+
Clear cached schemas:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { clearInstaSkeletonCache } from 'instaskeleton';
|
|
178
|
+
|
|
179
|
+
// Clear specific cache entry
|
|
180
|
+
clearInstaSkeletonCache('product-card');
|
|
181
|
+
|
|
182
|
+
// Clear all cached schemas
|
|
183
|
+
clearInstaSkeletonCache();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `SkeletonNode`
|
|
187
|
+
|
|
188
|
+
Supported node types:
|
|
189
|
+
|
|
190
|
+
#### `line` — Text placeholder
|
|
191
|
+
```tsx
|
|
192
|
+
{ type: 'line', width: '80%', height: '1rem' }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `rect` — Block placeholder (images, buttons, cards)
|
|
196
|
+
```tsx
|
|
197
|
+
{ type: 'rect', width: '100%', height: '8rem', radius: '1rem' }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `circle` — Avatar or icon placeholder
|
|
201
|
+
```tsx
|
|
202
|
+
{ type: 'circle', width: '3rem', height: '3rem' }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `group` — Container for nested nodes
|
|
206
|
+
```tsx
|
|
207
|
+
{
|
|
208
|
+
type: 'group',
|
|
209
|
+
gap: '0.75rem',
|
|
210
|
+
children: [
|
|
211
|
+
{ type: 'circle', width: '3rem', height: '3rem' },
|
|
212
|
+
{ type: 'line', width: '60%' },
|
|
213
|
+
{ type: 'line', width: '40%' },
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Performance
|
|
219
|
+
|
|
220
|
+
### Zero Work When Not Loading
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
// When loading=false, InstaSkeleton returns children immediately
|
|
224
|
+
// No inference, no schema processing, no DOM overhead
|
|
225
|
+
if (!loading) return <>{children}</>;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### LRU Cache with Size Limit
|
|
229
|
+
|
|
230
|
+
Schemas are cached with a 100-entry limit to prevent memory leaks in long-running SPAs.
|
|
231
|
+
|
|
232
|
+
### GPU-Accelerated Animations
|
|
233
|
+
|
|
234
|
+
Shimmer animation uses `will-change: transform` for 60fps performance.
|
|
235
|
+
|
|
236
|
+
### Reduced Motion Support
|
|
237
|
+
|
|
238
|
+
Animations are disabled automatically when `prefers-reduced-motion: reduce` is set.
|
|
239
|
+
|
|
240
|
+
## Choosing The Right Mode
|
|
241
|
+
|
|
242
|
+
| Use Case | Mode | Why |
|
|
243
|
+
|----------|------|-----|
|
|
244
|
+
| Quick prototyping | `infer` | Zero config, good approximation |
|
|
245
|
+
| Production cards with specific layout | `schema` | Pixel-perfect control |
|
|
246
|
+
| Reusable components | `withInstaSkeleton` | Consistent API, one config |
|
|
247
|
+
| Lists with repeated items | `infer` + `cacheKey` | Cached after first render |
|
|
248
|
+
| Complex nested structures | `infer` + `inferOptions` | Tune depth/node limits |
|
|
249
|
+
|
|
250
|
+
## Example App
|
|
251
|
+
|
|
252
|
+
The local example app is a comprehensive visual reference covering:
|
|
253
|
+
|
|
254
|
+
- Profile cards, product grids, social feeds
|
|
255
|
+
- Tables, file lists, pricing cards
|
|
256
|
+
- Settings forms with mixed controls
|
|
257
|
+
- Chat threads, deeply nested comment threads
|
|
258
|
+
- Dashboard layouts with nested cards
|
|
259
|
+
- E-commerce product detail pages
|
|
260
|
+
- Multi-level navigation structures
|
|
261
|
+
- Mixed media galleries
|
|
262
|
+
- Complex forms with validation states
|
|
263
|
+
- Manual schema mirrors showing exact structure
|
|
264
|
+
- HOC usage patterns
|
|
265
|
+
- All primitive node types
|
|
266
|
+
- Animation comparison (shimmer, pulse, none)
|
|
267
|
+
|
|
268
|
+
Run it from the repo root:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm run example:install
|
|
272
|
+
npm run example:dev
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npm run build # Build the library
|
|
279
|
+
npm run typecheck # Run TypeScript checks
|
|
280
|
+
npm run dev # Watch mode
|
|
281
|
+
npm run example:dev # Run example app
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Browser Support
|
|
285
|
+
|
|
286
|
+
- Chrome/Edge 88+
|
|
287
|
+
- Firefox 78+
|
|
288
|
+
- Safari 14+
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT © [LittleBoy9](https://github.com/LittleBoy9)
|
|
293
|
+
|
|
294
|
+
## Notes
|
|
295
|
+
|
|
296
|
+
- Inference walks the React element tree, not the rendered DOM.
|
|
297
|
+
- Function components may be unwrapped, but hook-heavy components can fall back to child inference.
|
|
298
|
+
- If you need strict visual matching, use manual schema.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var g=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var v=(e,t)=>{for(var n in t)g(e,n,{get:t[n],enumerable:!0})},P=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of b(t))!w.call(e,r)&&r!==n&&g(e,r,{get:()=>t[r],enumerable:!(o=x(t,r))||o.enumerable});return e};var C=e=>P(g({},"__esModule",{value:!0}),e);var D={};v(D,{InstaSkeleton:()=>y,clearInstaSkeletonCache:()=>I,withInstaSkeleton:()=>N});module.exports=C(D);var l=require("react"),a=require("react/jsx-runtime"),R={maxDepth:6,maxNodes:120,textLineHeight:"0.95rem"},A=100,m=new Map;function E(e,t){if(m.size>=A){let n=m.keys().next().value;n&&m.delete(n)}m.set(e,t)}function L(e){return e?Array.isArray(e)?e:[e]:[]}function h(e,t){return e??t}function W(e,t){let n={...R,...t},o=0,r=(i,d)=>{if(o>=n.maxNodes||d>n.maxDepth)return[];if(i==null||typeof i=="boolean")return[];if(typeof i=="string"||typeof i=="number")return o+=1,[{type:"line",height:n.textLineHeight}];if(Array.isArray(i))return i.flatMap(p=>r(p,d+1));if(!(0,l.isValidElement)(i))return[];o+=1;let s=typeof i.type=="string"?i.type:"component";if(s==="img"||s==="video"||s==="svg"||s==="canvas")return[{type:"rect",height:"8rem"}];if(s==="button"||s==="input"||s==="textarea"||s==="select")return[{type:"rect",height:"2.5rem",radius:"0.5rem"}];let u=r(i.props.children,d+1);return u.length===0?[{type:"line",width:"70%",height:n.textLineHeight}]:u.length===1?u:[{type:"group",children:u}]};return r(l.Children.toArray(e),0)}function k(e){let t={};return e.width!==void 0&&(t.width=h(e.width,"")),e.height!==void 0&&(t.height=h(e.height,"")),e.radius!==void 0&&(t.borderRadius=h(e.radius,"")),e.gap!==void 0&&(t.gap=h(e.gap,"")),t}var S=(0,l.memo)(function e({node:t,animation:n}){if(t.type==="group")return(0,a.jsx)("div",{className:"is-group",style:k(t),children:(t.children??[]).map((r,i)=>(0,a.jsx)(e,{node:r,animation:n},i))});let o=n==="none"?"":` is-anim-${n}`;return(0,a.jsx)("div",{className:`is-node is-${t.type}${o}`,style:k(t),"aria-hidden":"true"})}),y=(0,l.memo)(function({loading:t,children:n,schema:o,infer:r=!0,cacheKey:i,className:d,animation:s="shimmer",inferOptions:u}){if(!t)return(0,a.jsx)(a.Fragment,{children:n});let p=(0,l.useMemo)(()=>{let f=L(o);if(f.length>0)return f;if(!r)return[];if(i&&m.has(i))return m.get(i)??[];let c=W(n,u);return i&&E(i,c),c},[o,r,n,i,u]);return(0,a.jsx)("div",{className:["is-root",d].filter(Boolean).join(" "),role:"status","aria-live":"polite","aria-busy":"true",children:p.length>0?p.map((f,c)=>(0,a.jsx)(S,{node:f,animation:s},c)):(0,a.jsx)(S,{node:{type:"line"},animation:s})})});function N(e,t){let n=o=>{let{loading:r,...i}=o;return(0,a.jsx)(y,{loading:r,schema:t?.skeleton,infer:t?.infer,cacheKey:t?.cacheKey,className:t?.className,animation:t?.animation,children:(0,a.jsx)(e,{...i})})};return n.displayName=`withInstaSkeleton(${e.displayName||e.name||"Component"})`,(0,l.memo)(n)}function I(e){if(e){m.delete(e);return}m.clear()}0&&(module.exports={InstaSkeleton,clearInstaSkeletonCache,withInstaSkeleton});
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core.tsx"],"sourcesContent":["import './styles.css';\n\nexport { InstaSkeleton, clearInstaSkeletonCache, withInstaSkeleton } from './core';\nexport type { InferOptions, InstaSkeletonProps, SkeletonNode, SkeletonNodeType, WithInstaSkeletonProps } from './types';\n","import React, { Children, isValidElement, memo, useMemo } from 'react';\nimport type {\n InferOptions,\n InstaSkeletonProps,\n SkeletonNode,\n WithInstaSkeletonProps\n} from './types';\n\nconst DEFAULT_INFER: Required<InferOptions> = {\n maxDepth: 6,\n maxNodes: 120,\n textLineHeight: '0.95rem'\n};\n\nconst CACHE_MAX_SIZE = 100;\nconst schemaCache = new Map<string, SkeletonNode[]>();\n\nfunction setCacheWithLimit(key: string, value: SkeletonNode[]): void {\n if (schemaCache.size >= CACHE_MAX_SIZE) {\n const firstKey = schemaCache.keys().next().value;\n if (firstKey) schemaCache.delete(firstKey);\n }\n schemaCache.set(key, value);\n}\n\nfunction normalizeSchema(schema?: SkeletonNode | SkeletonNode[]): SkeletonNode[] {\n if (!schema) return [];\n return Array.isArray(schema) ? schema : [schema];\n}\n\nfunction resolveDimension(value: string | number | undefined, fallback: string | number): string | number {\n return value ?? fallback;\n}\n\nfunction inferFromChildren(children: React.ReactNode, options?: InferOptions): SkeletonNode[] {\n const merged = { ...DEFAULT_INFER, ...options };\n let nodeCount = 0;\n\n const walk = (node: React.ReactNode, depth: number): SkeletonNode[] => {\n if (nodeCount >= merged.maxNodes || depth > merged.maxDepth) return [];\n if (node == null || typeof node === 'boolean') return [];\n\n if (typeof node === 'string' || typeof node === 'number') {\n nodeCount += 1;\n return [{ type: 'line', height: merged.textLineHeight }];\n }\n\n if (Array.isArray(node)) {\n return node.flatMap((item) => walk(item, depth + 1));\n }\n\n if (!isValidElement(node)) {\n return [];\n }\n\n nodeCount += 1;\n\n const elType = typeof node.type === 'string' ? node.type : 'component';\n\n if (elType === 'img' || elType === 'video' || elType === 'svg' || elType === 'canvas') {\n return [{ type: 'rect', height: '8rem' }];\n }\n\n if (elType === 'button' || elType === 'input' || elType === 'textarea' || elType === 'select') {\n return [{ type: 'rect', height: '2.5rem', radius: '0.5rem' }];\n }\n\n const childNodes = walk(node.props.children, depth + 1);\n\n if (childNodes.length === 0) {\n return [{ type: 'line', width: '70%', height: merged.textLineHeight }];\n }\n\n if (childNodes.length === 1) {\n return childNodes;\n }\n\n return [{ type: 'group', children: childNodes }];\n };\n\n return walk(Children.toArray(children), 0);\n}\n\nfunction nodeStyle(node: SkeletonNode): React.CSSProperties {\n const style: React.CSSProperties = {};\n\n if (node.width !== undefined) style.width = resolveDimension(node.width, '');\n if (node.height !== undefined) style.height = resolveDimension(node.height, '');\n if (node.radius !== undefined) style.borderRadius = resolveDimension(node.radius, '');\n if (node.gap !== undefined) style.gap = resolveDimension(node.gap, '');\n\n return style;\n}\n\nconst NodeView = memo(function NodeView({ node, animation }: { node: SkeletonNode; animation: InstaSkeletonProps['animation'] }) {\n if (node.type === 'group') {\n return (\n <div className=\"is-group\" style={nodeStyle(node)}>\n {(node.children ?? []).map((child, index) => (\n <NodeView key={index} node={child} animation={animation} />\n ))}\n </div>\n );\n }\n\n const animClass = animation === 'none' ? '' : ` is-anim-${animation}`;\n return <div className={`is-node is-${node.type}${animClass}`} style={nodeStyle(node)} aria-hidden=\"true\" />;\n});\n\nexport const InstaSkeleton = memo(function InstaSkeleton({\n loading,\n children,\n schema,\n infer = true,\n cacheKey,\n className,\n animation = 'shimmer',\n inferOptions\n}: InstaSkeletonProps) {\n // Early exit: skip all inference work when not loading\n if (!loading) return <>{children}</>;\n\n const skeletonSchema = useMemo(() => {\n const normalized = normalizeSchema(schema);\n if (normalized.length > 0) return normalized;\n if (!infer) return [];\n\n if (cacheKey && schemaCache.has(cacheKey)) {\n return schemaCache.get(cacheKey) ?? [];\n }\n\n const inferred = inferFromChildren(children, inferOptions);\n if (cacheKey) setCacheWithLimit(cacheKey, inferred);\n return inferred;\n }, [schema, infer, children, cacheKey, inferOptions]);\n\n return (\n <div className={['is-root', className].filter(Boolean).join(' ')} role=\"status\" aria-live=\"polite\" aria-busy=\"true\">\n {skeletonSchema.length > 0 ? (\n skeletonSchema.map((node, index) => <NodeView key={index} node={node} animation={animation} />)\n ) : (\n <NodeView node={{ type: 'line' }} animation={animation} />\n )}\n </div>\n );\n});\n\nexport function withInstaSkeleton<P extends object>(\n Component: React.ComponentType<P>,\n options?: Omit<WithInstaSkeletonProps, 'loading'>\n) {\n const Wrapped = (props: P & { loading: boolean }) => {\n const { loading, ...rest } = props;\n\n return (\n <InstaSkeleton\n loading={loading}\n schema={options?.skeleton}\n infer={options?.infer}\n cacheKey={options?.cacheKey}\n className={options?.className}\n animation={options?.animation}\n >\n <Component {...(rest as P)} />\n </InstaSkeleton>\n );\n };\n\n Wrapped.displayName = `withInstaSkeleton(${Component.displayName || Component.name || 'Component'})`;\n\n return memo(Wrapped);\n}\n\nexport function clearInstaSkeletonCache(key?: string): void {\n if (key) {\n schemaCache.delete(key);\n return;\n }\n\n schemaCache.clear();\n}\n"],"mappings":"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","names":["index_exports","__export","InstaSkeleton","clearInstaSkeletonCache","withInstaSkeleton","__toCommonJS","import_react","import_jsx_runtime","DEFAULT_INFER","CACHE_MAX_SIZE","schemaCache","setCacheWithLimit","key","value","firstKey","normalizeSchema","schema","resolveDimension","fallback","inferFromChildren","children","options","merged","nodeCount","walk","node","depth","item","elType","childNodes","nodeStyle","style","NodeView","animation","child","index","animClass","InstaSkeleton","loading","infer","cacheKey","className","inferOptions","skeletonSchema","normalized","inferred","withInstaSkeleton","Component","Wrapped","props","rest","clearInstaSkeletonCache"]}
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.is-root{display:flex;flex-direction:column;gap:.75rem;width:100%}.is-node{position:relative;overflow:hidden;background:#e6e6e6;border-radius:.5rem}.is-line{height:.95rem;width:100%;border-radius:999px}.is-rect{width:100%;height:5rem}.is-circle{width:2.5rem;height:2.5rem;border-radius:999px}.is-group{display:flex;flex-direction:column;gap:.5rem}.is-anim-shimmer:after{content:"";position:absolute;inset:0;transform:translate(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.6),transparent);animation:is-shimmer 1.2s linear infinite;will-change:transform}.is-anim-pulse{animation:is-pulse 1.1s ease-in-out infinite}@keyframes is-shimmer{to{transform:translate(100%)}}@keyframes is-pulse{0%{opacity:1}50%{opacity:.6}to{opacity:1}}@media(prefers-reduced-motion:reduce){.is-anim-shimmer:after,.is-anim-pulse{animation:none}}
|
|
2
|
+
/*# sourceMappingURL=index.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/styles.css"],"sourcesContent":[".is-root {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n width: 100%;\n}\n\n.is-node {\n position: relative;\n overflow: hidden;\n background: #e6e6e6;\n border-radius: 0.5rem;\n}\n\n.is-line {\n height: 0.95rem;\n width: 100%;\n border-radius: 999px;\n}\n\n.is-rect {\n width: 100%;\n height: 5rem;\n}\n\n.is-circle {\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 999px;\n}\n\n.is-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.is-anim-shimmer::after {\n content: '';\n position: absolute;\n inset: 0;\n transform: translateX(-100%);\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);\n animation: is-shimmer 1.2s linear infinite;\n will-change: transform;\n}\n\n.is-anim-pulse {\n animation: is-pulse 1.1s ease-in-out infinite;\n}\n\n@keyframes is-shimmer {\n 100% {\n transform: translateX(100%);\n }\n}\n\n@keyframes is-pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .is-anim-shimmer::after,\n .is-anim-pulse {\n animation: none;\n }\n}\n"],"mappings":"AAAA,CAAC,QACC,QAAS,KACT,eAAgB,OAChB,IAAK,OACL,MAAO,IACT,CAEA,CAAC,QACC,SAAU,SACV,SAAU,OACV,WAAY,QAVd,cAWiB,KACjB,CAEA,CAAC,QACC,OAAQ,OACR,MAAO,KAhBT,cAiBiB,KACjB,CAEA,CAAC,QACC,MAAO,KACP,OAAQ,IACV,CAEA,CAAC,UACC,MAAO,OACP,OAAQ,OA3BV,cA4BiB,KACjB,CAEA,CAAC,SACC,QAAS,KACT,eAAgB,OAChB,IAAK,KACP,CAEA,CAAC,eAAe,OACd,QAAS,GACT,SAAU,SAvCZ,MAwCS,EACP,UAAW,UAAW,OACtB,WAAY,gBAAgB,KAAK,CAAE,WAAW,CAAE,KAAK,GAAG,CAAE,GAAG,CAAE,GAAG,CAAE,GAAI,CAAE,aAC1E,UAAW,WAAW,KAAK,OAAO,SAClC,YAAa,SACf,CAEA,CAAC,cACC,UAAW,SAAS,KAAK,YAAY,QACvC,CAEA,WARa,WASX,GACE,UAAW,UAAW,KACxB,CACF,CAEA,WATa,SAUX,GACE,QAAS,CACX,CACA,IACE,QAAS,EACX,CACA,GACE,QAAS,CACX,CACF,CAEA,OAAO,uBAAyB,QAC9B,CAjCD,eAiCiB,OAChB,CAxBD,cAyBG,UAAW,IACb,CACF","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React, { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type SkeletonNodeType = 'line' | 'rect' | 'circle' | 'group';
|
|
5
|
+
interface SkeletonNode {
|
|
6
|
+
type: SkeletonNodeType;
|
|
7
|
+
width?: string | number;
|
|
8
|
+
height?: string | number;
|
|
9
|
+
radius?: string | number;
|
|
10
|
+
gap?: string | number;
|
|
11
|
+
children?: SkeletonNode[];
|
|
12
|
+
}
|
|
13
|
+
interface InferOptions {
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxNodes?: number;
|
|
16
|
+
textLineHeight?: string | number;
|
|
17
|
+
}
|
|
18
|
+
interface InstaSkeletonProps {
|
|
19
|
+
loading: boolean;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
schema?: SkeletonNode | SkeletonNode[];
|
|
22
|
+
infer?: boolean;
|
|
23
|
+
cacheKey?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
animation?: 'shimmer' | 'pulse' | 'none';
|
|
26
|
+
inferOptions?: InferOptions;
|
|
27
|
+
}
|
|
28
|
+
interface WithInstaSkeletonProps {
|
|
29
|
+
loading: boolean;
|
|
30
|
+
skeleton?: SkeletonNode | SkeletonNode[];
|
|
31
|
+
infer?: boolean;
|
|
32
|
+
cacheKey?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
animation?: 'shimmer' | 'pulse' | 'none';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare const InstaSkeleton: React.NamedExoticComponent<InstaSkeletonProps>;
|
|
38
|
+
declare function withInstaSkeleton<P extends object>(Component: React.ComponentType<P>, options?: Omit<WithInstaSkeletonProps, 'loading'>): React.MemoExoticComponent<{
|
|
39
|
+
(props: P & {
|
|
40
|
+
loading: boolean;
|
|
41
|
+
}): react_jsx_runtime.JSX.Element;
|
|
42
|
+
displayName: string;
|
|
43
|
+
}>;
|
|
44
|
+
declare function clearInstaSkeletonCache(key?: string): void;
|
|
45
|
+
|
|
46
|
+
export { type InferOptions, InstaSkeleton, type InstaSkeletonProps, type SkeletonNode, type SkeletonNodeType, type WithInstaSkeletonProps, clearInstaSkeletonCache, withInstaSkeleton };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React, { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type SkeletonNodeType = 'line' | 'rect' | 'circle' | 'group';
|
|
5
|
+
interface SkeletonNode {
|
|
6
|
+
type: SkeletonNodeType;
|
|
7
|
+
width?: string | number;
|
|
8
|
+
height?: string | number;
|
|
9
|
+
radius?: string | number;
|
|
10
|
+
gap?: string | number;
|
|
11
|
+
children?: SkeletonNode[];
|
|
12
|
+
}
|
|
13
|
+
interface InferOptions {
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxNodes?: number;
|
|
16
|
+
textLineHeight?: string | number;
|
|
17
|
+
}
|
|
18
|
+
interface InstaSkeletonProps {
|
|
19
|
+
loading: boolean;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
schema?: SkeletonNode | SkeletonNode[];
|
|
22
|
+
infer?: boolean;
|
|
23
|
+
cacheKey?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
animation?: 'shimmer' | 'pulse' | 'none';
|
|
26
|
+
inferOptions?: InferOptions;
|
|
27
|
+
}
|
|
28
|
+
interface WithInstaSkeletonProps {
|
|
29
|
+
loading: boolean;
|
|
30
|
+
skeleton?: SkeletonNode | SkeletonNode[];
|
|
31
|
+
infer?: boolean;
|
|
32
|
+
cacheKey?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
animation?: 'shimmer' | 'pulse' | 'none';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare const InstaSkeleton: React.NamedExoticComponent<InstaSkeletonProps>;
|
|
38
|
+
declare function withInstaSkeleton<P extends object>(Component: React.ComponentType<P>, options?: Omit<WithInstaSkeletonProps, 'loading'>): React.MemoExoticComponent<{
|
|
39
|
+
(props: P & {
|
|
40
|
+
loading: boolean;
|
|
41
|
+
}): react_jsx_runtime.JSX.Element;
|
|
42
|
+
displayName: string;
|
|
43
|
+
}>;
|
|
44
|
+
declare function clearInstaSkeletonCache(key?: string): void;
|
|
45
|
+
|
|
46
|
+
export { type InferOptions, InstaSkeleton, type InstaSkeletonProps, type SkeletonNode, type SkeletonNodeType, type WithInstaSkeletonProps, clearInstaSkeletonCache, withInstaSkeleton };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Children as S,isValidElement as N,memo as h,useMemo as I}from"react";import{Fragment as A,jsx as s}from"react/jsx-runtime";var x={maxDepth:6,maxNodes:120,textLineHeight:"0.95rem"},b=100,l=new Map;function w(e,t){if(l.size>=b){let i=l.keys().next().value;i&&l.delete(i)}l.set(e,t)}function v(e){return e?Array.isArray(e)?e:[e]:[]}function c(e,t){return e??t}function P(e,t){let i={...x,...t},o=0,a=(n,u)=>{if(o>=i.maxNodes||u>i.maxDepth)return[];if(n==null||typeof n=="boolean")return[];if(typeof n=="string"||typeof n=="number")return o+=1,[{type:"line",height:i.textLineHeight}];if(Array.isArray(n))return n.flatMap(d=>a(d,u+1));if(!N(n))return[];o+=1;let r=typeof n.type=="string"?n.type:"component";if(r==="img"||r==="video"||r==="svg"||r==="canvas")return[{type:"rect",height:"8rem"}];if(r==="button"||r==="input"||r==="textarea"||r==="select")return[{type:"rect",height:"2.5rem",radius:"0.5rem"}];let m=a(n.props.children,u+1);return m.length===0?[{type:"line",width:"70%",height:i.textLineHeight}]:m.length===1?m:[{type:"group",children:m}]};return a(S.toArray(e),0)}function g(e){let t={};return e.width!==void 0&&(t.width=c(e.width,"")),e.height!==void 0&&(t.height=c(e.height,"")),e.radius!==void 0&&(t.borderRadius=c(e.radius,"")),e.gap!==void 0&&(t.gap=c(e.gap,"")),t}var y=h(function e({node:t,animation:i}){if(t.type==="group")return s("div",{className:"is-group",style:g(t),children:(t.children??[]).map((a,n)=>s(e,{node:a,animation:i},n))});let o=i==="none"?"":` is-anim-${i}`;return s("div",{className:`is-node is-${t.type}${o}`,style:g(t),"aria-hidden":"true"})}),k=h(function({loading:t,children:i,schema:o,infer:a=!0,cacheKey:n,className:u,animation:r="shimmer",inferOptions:m}){if(!t)return s(A,{children:i});let d=I(()=>{let p=v(o);if(p.length>0)return p;if(!a)return[];if(n&&l.has(n))return l.get(n)??[];let f=P(i,m);return n&&w(n,f),f},[o,a,i,n,m]);return s("div",{className:["is-root",u].filter(Boolean).join(" "),role:"status","aria-live":"polite","aria-busy":"true",children:d.length>0?d.map((p,f)=>s(y,{node:p,animation:r},f)):s(y,{node:{type:"line"},animation:r})})});function C(e,t){let i=o=>{let{loading:a,...n}=o;return s(k,{loading:a,schema:t?.skeleton,infer:t?.infer,cacheKey:t?.cacheKey,className:t?.className,animation:t?.animation,children:s(e,{...n})})};return i.displayName=`withInstaSkeleton(${e.displayName||e.name||"Component"})`,h(i)}function R(e){if(e){l.delete(e);return}l.clear()}export{k as InstaSkeleton,R as clearInstaSkeletonCache,C as withInstaSkeleton};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.tsx"],"sourcesContent":["import React, { Children, isValidElement, memo, useMemo } from 'react';\nimport type {\n InferOptions,\n InstaSkeletonProps,\n SkeletonNode,\n WithInstaSkeletonProps\n} from './types';\n\nconst DEFAULT_INFER: Required<InferOptions> = {\n maxDepth: 6,\n maxNodes: 120,\n textLineHeight: '0.95rem'\n};\n\nconst CACHE_MAX_SIZE = 100;\nconst schemaCache = new Map<string, SkeletonNode[]>();\n\nfunction setCacheWithLimit(key: string, value: SkeletonNode[]): void {\n if (schemaCache.size >= CACHE_MAX_SIZE) {\n const firstKey = schemaCache.keys().next().value;\n if (firstKey) schemaCache.delete(firstKey);\n }\n schemaCache.set(key, value);\n}\n\nfunction normalizeSchema(schema?: SkeletonNode | SkeletonNode[]): SkeletonNode[] {\n if (!schema) return [];\n return Array.isArray(schema) ? schema : [schema];\n}\n\nfunction resolveDimension(value: string | number | undefined, fallback: string | number): string | number {\n return value ?? fallback;\n}\n\nfunction inferFromChildren(children: React.ReactNode, options?: InferOptions): SkeletonNode[] {\n const merged = { ...DEFAULT_INFER, ...options };\n let nodeCount = 0;\n\n const walk = (node: React.ReactNode, depth: number): SkeletonNode[] => {\n if (nodeCount >= merged.maxNodes || depth > merged.maxDepth) return [];\n if (node == null || typeof node === 'boolean') return [];\n\n if (typeof node === 'string' || typeof node === 'number') {\n nodeCount += 1;\n return [{ type: 'line', height: merged.textLineHeight }];\n }\n\n if (Array.isArray(node)) {\n return node.flatMap((item) => walk(item, depth + 1));\n }\n\n if (!isValidElement(node)) {\n return [];\n }\n\n nodeCount += 1;\n\n const elType = typeof node.type === 'string' ? node.type : 'component';\n\n if (elType === 'img' || elType === 'video' || elType === 'svg' || elType === 'canvas') {\n return [{ type: 'rect', height: '8rem' }];\n }\n\n if (elType === 'button' || elType === 'input' || elType === 'textarea' || elType === 'select') {\n return [{ type: 'rect', height: '2.5rem', radius: '0.5rem' }];\n }\n\n const childNodes = walk(node.props.children, depth + 1);\n\n if (childNodes.length === 0) {\n return [{ type: 'line', width: '70%', height: merged.textLineHeight }];\n }\n\n if (childNodes.length === 1) {\n return childNodes;\n }\n\n return [{ type: 'group', children: childNodes }];\n };\n\n return walk(Children.toArray(children), 0);\n}\n\nfunction nodeStyle(node: SkeletonNode): React.CSSProperties {\n const style: React.CSSProperties = {};\n\n if (node.width !== undefined) style.width = resolveDimension(node.width, '');\n if (node.height !== undefined) style.height = resolveDimension(node.height, '');\n if (node.radius !== undefined) style.borderRadius = resolveDimension(node.radius, '');\n if (node.gap !== undefined) style.gap = resolveDimension(node.gap, '');\n\n return style;\n}\n\nconst NodeView = memo(function NodeView({ node, animation }: { node: SkeletonNode; animation: InstaSkeletonProps['animation'] }) {\n if (node.type === 'group') {\n return (\n <div className=\"is-group\" style={nodeStyle(node)}>\n {(node.children ?? []).map((child, index) => (\n <NodeView key={index} node={child} animation={animation} />\n ))}\n </div>\n );\n }\n\n const animClass = animation === 'none' ? '' : ` is-anim-${animation}`;\n return <div className={`is-node is-${node.type}${animClass}`} style={nodeStyle(node)} aria-hidden=\"true\" />;\n});\n\nexport const InstaSkeleton = memo(function InstaSkeleton({\n loading,\n children,\n schema,\n infer = true,\n cacheKey,\n className,\n animation = 'shimmer',\n inferOptions\n}: InstaSkeletonProps) {\n // Early exit: skip all inference work when not loading\n if (!loading) return <>{children}</>;\n\n const skeletonSchema = useMemo(() => {\n const normalized = normalizeSchema(schema);\n if (normalized.length > 0) return normalized;\n if (!infer) return [];\n\n if (cacheKey && schemaCache.has(cacheKey)) {\n return schemaCache.get(cacheKey) ?? [];\n }\n\n const inferred = inferFromChildren(children, inferOptions);\n if (cacheKey) setCacheWithLimit(cacheKey, inferred);\n return inferred;\n }, [schema, infer, children, cacheKey, inferOptions]);\n\n return (\n <div className={['is-root', className].filter(Boolean).join(' ')} role=\"status\" aria-live=\"polite\" aria-busy=\"true\">\n {skeletonSchema.length > 0 ? (\n skeletonSchema.map((node, index) => <NodeView key={index} node={node} animation={animation} />)\n ) : (\n <NodeView node={{ type: 'line' }} animation={animation} />\n )}\n </div>\n );\n});\n\nexport function withInstaSkeleton<P extends object>(\n Component: React.ComponentType<P>,\n options?: Omit<WithInstaSkeletonProps, 'loading'>\n) {\n const Wrapped = (props: P & { loading: boolean }) => {\n const { loading, ...rest } = props;\n\n return (\n <InstaSkeleton\n loading={loading}\n schema={options?.skeleton}\n infer={options?.infer}\n cacheKey={options?.cacheKey}\n className={options?.className}\n animation={options?.animation}\n >\n <Component {...(rest as P)} />\n </InstaSkeleton>\n );\n };\n\n Wrapped.displayName = `withInstaSkeleton(${Component.displayName || Component.name || 'Component'})`;\n\n return memo(Wrapped);\n}\n\nexport function clearInstaSkeletonCache(key?: string): void {\n if (key) {\n schemaCache.delete(key);\n return;\n }\n\n schemaCache.clear();\n}\n"],"mappings":"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","names":["Children","isValidElement","memo","useMemo","Fragment","jsx","DEFAULT_INFER","CACHE_MAX_SIZE","schemaCache","setCacheWithLimit","key","value","firstKey","normalizeSchema","schema","resolveDimension","fallback","inferFromChildren","children","options","merged","nodeCount","walk","node","depth","item","elType","childNodes","nodeStyle","style","NodeView","animation","child","index","animClass","InstaSkeleton","loading","infer","cacheKey","className","inferOptions","skeletonSchema","normalized","inferred","withInstaSkeleton","Component","Wrapped","props","rest","clearInstaSkeletonCache"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "instaskeleton",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ultra-light React skeleton generator with zero DOM scanning",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sounak Das",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/LittleBoy9/instaskeleton.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/LittleBoy9/instaskeleton/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/LittleBoy9/instaskeleton#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./styles.css": "./dist/index.css"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"**/*.css"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"example:install": "npm --prefix example install",
|
|
38
|
+
"example:dev": "npm run build && npm --prefix example run dev",
|
|
39
|
+
"example:build": "npm run build && npm --prefix example run build"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"skeleton",
|
|
43
|
+
"react",
|
|
44
|
+
"loading",
|
|
45
|
+
"placeholder",
|
|
46
|
+
"ui"
|
|
47
|
+
],
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^18.3.0",
|
|
53
|
+
"tsup": "^8.1.0",
|
|
54
|
+
"typescript": "^5.6.0"
|
|
55
|
+
}
|
|
56
|
+
}
|