react-loaded 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vingt-douze
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,263 @@
1
+ # React Loaded
2
+
3
+ > Loading should feel loaded before it actually loads.
4
+
5
+ Smart skeleton screens that mirror your actual components. No layout shift. No visual jumps.
6
+
7
+
8
+ ## The Problem
9
+
10
+ Traditional skeleton screens create a disconnect between loading and loaded states:
11
+
12
+ - Generic gray boxes do not match your actual UI
13
+ - Lists show arbitrary counts (3 skeletons -> 47 items = jarring jump)
14
+ - Building custom skeletons for every component is tedious and fragile
15
+
16
+ ## The Solution
17
+
18
+ **React Loaded** renders your real components in "skeleton mode" using CSS masking. The skeleton is your component, just with content hidden. This guarantees:
19
+
20
+ - **Zero layout shift** between loading and loaded states
21
+ - **Pixel-perfect structure** that matches the final render
22
+ - **Persistent list counts** that remember how many items to show
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pnpm add react-loaded
28
+ ```
29
+
30
+ Required: import the stylesheet once in your app:
31
+
32
+ ```tsx
33
+ import "react-loaded/style.css";
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Single Component
39
+
40
+ ```tsx
41
+ import { SmartSkeleton } from "react-loaded";
42
+
43
+ function UserProfile({ userId }) {
44
+ const { data: user, isLoading } = useQuery(["user", userId], fetchUser);
45
+
46
+ return (
47
+ <SmartSkeleton
48
+ loading={isLoading}
49
+ element={<ProfileCard user={{ name: "Loading...", avatar: "" }} />}
50
+ >
51
+ <ProfileCard user={user} />
52
+ </SmartSkeleton>
53
+ );
54
+ }
55
+ ```
56
+
57
+ Or with conditional rendering:
58
+
59
+ ```tsx
60
+ if (isLoading) {
61
+ return (
62
+ <SmartSkeleton
63
+ loading
64
+ element={<ProfileCard user={{ name: "Loading...", avatar: "" }} />}
65
+ />
66
+ );
67
+ }
68
+
69
+ return <ProfileCard user={user} />;
70
+ ```
71
+
72
+ ### Lists with Persistence
73
+
74
+ ```tsx
75
+ import { SmartSkeletonList } from "react-loaded";
76
+
77
+ function ProductList() {
78
+ const { data: products, isLoading } = useQuery(["products"], fetchProducts);
79
+
80
+ return (
81
+ <SmartSkeletonList
82
+ loading={isLoading}
83
+ items={products}
84
+ storageKey="product-list"
85
+ defaultCount={6}
86
+ renderItem={(product) => <ProductCard product={product} />}
87
+ renderSkeleton={(index) => (
88
+ <ProductCard product={{ id: index, name: "Product", price: 0 }} />
89
+ )}
90
+ keyExtractor={(product) => product.id}
91
+ />
92
+ );
93
+ }
94
+ ```
95
+
96
+ **How persistence works:**
97
+ 1. First visit: shows `defaultCount` skeletons (6)
98
+ 2. Data loads: renders 42 products, saves count to localStorage
99
+ 3. Next visit: shows 42 skeletons -> loads 42 products -> no jump
100
+
101
+ ## API Reference
102
+
103
+ ### `<SmartSkeleton>`
104
+
105
+ Wraps a single component to display it in skeleton mode while loading.
106
+
107
+ | Prop | Type | Default | Description |
108
+ |------|------|---------|-------------|
109
+ | `element` | `ReactElement` | *required* | The skeleton version with mock or placeholder data |
110
+ | `children` | `ReactElement` | - | The real content when loaded. Returns `null` if omitted |
111
+ | `loading` | `boolean` | `false` | Whether to show the skeleton |
112
+ | `animate` | `boolean` | `true` | Enable shimmer animation |
113
+ | `className` | `string` | - | Additional CSS classes |
114
+ | `seed` | `string \| number` | - | Stable seed for text width randomness |
115
+ | `suppressRefWarning` | `boolean` | `false` | Suppress console warning when auto-wrapper is needed |
116
+
117
+ ### `<SmartSkeletonList>`
118
+
119
+ Renders a list with skeleton placeholders and optional count persistence.
120
+
121
+ | Prop | Type | Default | Description |
122
+ |------|------|---------|-------------|
123
+ | `items` | `T[] | undefined` | *required* | Array of items, `undefined` while loading |
124
+ | `renderItem` | `(item: T, index: number) => ReactElement` | *required* | Render function for loaded items |
125
+ | `renderSkeleton` | `(index: number) => ReactElement` | *required* | Render function for skeleton placeholders |
126
+ | `loading` | `boolean` | `false` | Whether to show skeletons |
127
+ | `storageKey` | `string` | - | localStorage key for count persistence |
128
+ | `defaultCount` | `number` | `3` | Initial skeleton count |
129
+ | `minCount` | `number` | `1` | Minimum skeletons to display |
130
+ | `maxCount` | `number` | - | Maximum skeletons to display |
131
+ | `animate` | `boolean` | `true` | Enable shimmer animation |
132
+ | `seed` | `string \| number` | - | Stable seed for text width randomness |
133
+ | `suppressRefWarning` | `boolean` | `false` | Suppress console warning when auto-wrapper is needed |
134
+ | `keyExtractor` | `(item: T, index: number) => string | number` | `index` | Extract unique key for each item |
135
+
136
+ ### `useIsSkeletonMode()`
137
+
138
+ Hook to detect if a component is rendered inside a skeleton.
139
+
140
+ ```tsx
141
+ import { useIsSkeletonMode } from "react-loaded";
142
+
143
+ function Avatar({ src }) {
144
+ const isSkeleton = useIsSkeletonMode();
145
+
146
+ // Skip expensive operations during skeleton render
147
+ if (isSkeleton) {
148
+ return <div className="avatar-placeholder" />;
149
+ }
150
+
151
+ return <img src={src} onLoad={trackAnalytics} />;
152
+ }
153
+ ```
154
+
155
+ ### `usePersistedCount()`
156
+
157
+ Low-level hook for custom persistence logic.
158
+
159
+ ```tsx
160
+ import { usePersistedCount } from "react-loaded";
161
+
162
+ const count = usePersistedCount({
163
+ storageKey: "my-list",
164
+ defaultCount: 5,
165
+ currentCount: items?.length,
166
+ loading: isLoading,
167
+ minCount: 1,
168
+ maxCount: 20,
169
+ });
170
+ ```
171
+
172
+ ## Customization
173
+
174
+ Override CSS custom properties to match your design system:
175
+
176
+ ```css
177
+ :root {
178
+ --loaded-bg-wrapper: rgba(229, 231, 235, 1); /* Skeleton background */
179
+ --loaded-bg-content: rgba(156, 163, 175, 0.6); /* Content block color */
180
+ --loaded-border-radius: 4px; /* Border radius */
181
+ --loaded-text-inset: 0.3em; /* Text bar vertical padding */
182
+ }
183
+ ```
184
+
185
+ ### Dark Mode Example
186
+
187
+ ```css
188
+ @media (prefers-color-scheme: dark) {
189
+ :root {
190
+ --loaded-bg-wrapper: rgba(55, 65, 81, 1);
191
+ --loaded-bg-content: rgba(107, 114, 128, 0.6);
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## How It Works
197
+
198
+ 1. **Render phase:** Your component renders with mock data
199
+ 2. **CSS masking:** Text becomes transparent, backgrounds neutralized
200
+ 3. **Visual overlay:** Skeleton bars appear over text, media gets placeholder backgrounds
201
+ 4. **Transition:** When `loading` becomes `false`, your real component renders in place
202
+
203
+ **SSR note:** React Loaded is primarily designed for client-side loading states (navigation/refetch).
204
+ If you render skeletons during SSR, the full overlay (text widths, media/content classes) is applied on the client via refs.
205
+ For best SSR results, ensure your skeleton `element` forwards `className` and `ref` to a DOM node.
206
+
207
+ The skeleton preserves:
208
+ - Exact dimensions and spacing
209
+ - Text alignment (left, center, right)
210
+ - Responsive behavior
211
+ - Component hierarchy
212
+
213
+ ## Ref Handling
214
+
215
+ Components must forward refs for optimal rendering. If a component does not forward its ref, React Loaded automatically wraps it in a `div` and logs a development warning.
216
+
217
+ To suppress the warning:
218
+
219
+ ```tsx
220
+ <SmartSkeleton
221
+ suppressRefWarning
222
+ element={<ThirdPartyComponent />}
223
+ />
224
+ ```
225
+
226
+ Or better, wrap third-party components:
227
+
228
+ ```tsx
229
+ const WrappedComponent = forwardRef((props, ref) => (
230
+ <div ref={ref}>
231
+ <ThirdPartyComponent {...props} />
232
+ </div>
233
+ ));
234
+ ```
235
+
236
+ ## Stable Text Widths with `seed`
237
+
238
+ By default, skeleton text bars have slightly randomized widths to look more natural. If you need consistent widths across renders (useful for tests or SSR hydration), pass a `seed`:
239
+
240
+ ```tsx
241
+ <SmartSkeleton
242
+ loading={isLoading}
243
+ seed="user-profile"
244
+ element={<ProfileCard user={mockUser} />}
245
+ >
246
+ <ProfileCard user={user} />
247
+ </SmartSkeleton>
248
+ ```
249
+
250
+ The same seed always produces the same text widths, making skeleton output deterministic.
251
+
252
+ ## Notes
253
+
254
+ - **React 18 and 19** are supported.
255
+ - Persistence uses `localStorage` under the root key `react-loaded` with a versioned schema.
256
+ - In skeleton mode, the library applies CSS classes on the subtree. Components should accept `className` and ideally forward refs.
257
+ - Dev warnings are enabled when `NODE_ENV !== "production"`. If your environment doesn’t inject `NODE_ENV`, you can force them with `globalThis.__REACT_LOADED_DEV__ = true`.
258
+ - SSR: the library uses an isomorphic layout effect to avoid server warnings and keep hydration stable.
259
+ - JSR/Deno: CSS module imports aren’t supported. For Node/bundlers, import `react-loaded/style.css`. For Deno, you’ll need to copy the CSS into your app (or recreate the styles).
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var h=react.createContext(false);function Z(){return react.useContext(h)}function S(){let e=globalThis,t=e.__REACT_LOADED_DEV__;if(typeof t=="boolean")return t;let n=e.__DEV__;if(typeof n=="boolean")return n;let o=globalThis.process,s=typeof o=="object"&&o!==null?o.env?.NODE_ENV:void 0;return typeof s=="string"?s!=="production":false}var ne=typeof globalThis<"u"&&typeof globalThis.document<"u",w=ne?react.useLayoutEffect:react.useEffect;var le=6,ie=40;function V(e){if(!e||typeof e!="object")return false;let t=e;return !(t.nodeType!==1||typeof t.tagName!="string"||typeof t.querySelectorAll!="function"||typeof Element<"u"&&!(e instanceof Element))}var x=new Set;function M(e){if(!V(e))return false;try{return e.querySelectorAll("*"),!0}catch{return false}}function de(e){if(M(e))return e;if(e&&typeof e=="object"&&"nativeElement"in e){let t=e.nativeElement;if(M(t))return t}return null}function P(e){let t=e.type;if(typeof t=="string")return `<${t}>`;if(typeof t=="function"){let n=t;return `<${n.displayName||n.name||"Unknown"}>`}if(typeof t=="object"&&t!==null){let n=t;return `<${n.displayName||n.name||"Unknown"}>`}return "<Unknown>"}function ue(e){let t=e.props?.ref;if(t)return t;let n=e.ref;if(n)return n}function ce(e,t){e&&(typeof e=="function"?e(t):e.current=t);}var T=new Set(["IMG","VIDEO","CANVAS"]),R=new Set(["SVG"]),fe=new Set(["BUTTON","INPUT","TEXTAREA","SELECT","A"]),me="button,input,textarea,select,a,[role='button']",pe=new Set(["SCRIPT","STYLE","LINK","META","NOSCRIPT","TEMPLATE"]);function v(e){return e.tagName.toUpperCase()}function L(e,t=v(e)){return fe.has(t)?true:e.getAttribute("role")==="button"}function ke(e,t){return !!(e.closest(me)&&!L(e,t))}function be(e,t=v(e)){return !!(T.has(t)||R.has(t)||L(e,t)||e.childElementCount===0&&e.textContent?.trim())}function ge(e,t){let n=e.length,o=Math.max(4,.8*n),s=Ee(t)*o,a=n+2+s;return Math.max(le,Math.min(ie,a))}function Ee(e){if(!e)return 0;let t=2166136261;for(let o=0;o<e.length;o+=1)t^=e.charCodeAt(o),t=Math.imul(t,16777619);return (t>>>0)/4294967295*2-1}function Se(e){let t=globalThis.getComputedStyle(e).textAlign;return t==="center"?"center":t==="right"||t==="end"?"right":"left"}function ye(e,t={}){let{animate:n=true,seed:o}=t,s=o==null?"loaded":String(o);if(!V(e))return;let a=e;a.classList.add("loaded-skeleton-mode"),n&&a.classList.add("loaded-animate"),a.classList.contains("loaded-skeleton-wrapper")||a.classList.add("loaded-skeleton-bg");let m=e.getElementsByTagName("*"),f=0,u=l=>{let i=v(l);if(pe.has(i)||!be(l,i)||ke(l,i))return;let r=l,d=l.textContent?.trim();if(l.childElementCount===0&&d&&!T.has(i)&&!R.has(i)&&!L(l,i)){r.classList.add("loaded-text-skeleton"),r.dataset.skeletonAlign=Se(r);let b=`${s}|${f}|${d??""}`;f+=1;let O=ge(d??"",b);r.style.setProperty("--skeleton-text-width",`${O}ch`);}else T.has(i)?r.classList.add("loaded-skeleton-media"):R.has(i)?(r.classList.add("loaded-skeleton-content"),r.classList.add("loaded-skeleton-svg")):(r.classList.add("loaded-skeleton-content"),r.setAttribute("tabindex","-1"));};u(e);for(let l of m)u(l);}function N({element:e,children:t,loading:n=false,animate:o=true,className:s="",seed:a,suppressRefWarning:k=false}){let m=react.useRef(false),f=react.useRef(false),u=react.useRef(null),l=react.useRef(null),i=react.useRef(null),[r,d]=react.useState(false);(!n||u.current!==e)&&(m.current=false,f.current=false,u.current=e),react.useEffect(()=>{let p=e.type,g=e.key??null,E=l.current,q=i.current;E!==null&&(E!==p||q!==g)&&d(false),l.current=p,i.current=g;},[e.type,e.key]);let c=ue(e),b=react.useCallback(p=>{f.current=true;let g=de(p);if(p!==null&&!g&&!r){if(d(true),!k&&S()){let E=P(e);x.has(E)||(console.warn(`[SmartSkeleton] ${E} does not forward its ref to a DOM element. A wrapper <div> has been added automatically. Use forwardRef to avoid this.`),x.add(E));}return}g&&n&&!m.current&&(ye(g,{animate:o,seed:a}),m.current=true),ce(c,p);},[n,e,r,k,c,o,a]);if(w(()=>{if(!(!n||r)&&!f.current&&(d(true),!k&&S())){let p=P(e);x.has(p)||(console.warn(`[SmartSkeleton] ${p} does not accept a ref. A wrapper <div> has been added automatically. Use forwardRef to avoid this.`),x.add(p));}},[n,r,e,k]),!n)return t??null;let F=e.props.className??"",I=["loaded-skeleton-mode",o&&"loaded-animate"].filter(Boolean).join(" "),J=[I,"loaded-skeleton-wrapper",s].filter(Boolean).join(" "),Y=[F,I,"loaded-skeleton-bg",s].filter(Boolean).join(" ");return jsxRuntime.jsx(h.Provider,{value:true,children:r?jsxRuntime.jsx("div",{ref:b,className:J,"aria-hidden":"true",children:e}):react.cloneElement(e,{ref:b,className:Y,"aria-hidden":true})})}var B="react-loaded",xe="loaded",$=1;function G(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function U(e){if(!G(e))return {};let t={};for(let[n,o]of Object.entries(e))typeof o=="number"&&(t[n]=o);return t}function W(e){return G(e)&&e.v===$?U(e.counts):U(e)}function Ce(e){if(typeof localStorage>"u")return {};try{let t=localStorage.getItem(e);return t===null?{}:W(JSON.parse(t))}catch{return {}}}function H(e){if(typeof localStorage>"u")return;let t={v:$,counts:e};try{localStorage.setItem(B,JSON.stringify(t));}catch{}}function K(){if(typeof localStorage>"u")return {};try{let e=localStorage.getItem(B);if(e!==null)return W(JSON.parse(e));let t=Ce(xe);return Object.keys(t).length>0&&H(t),t}catch{return {}}}function Te(e){let n=K()[e];return typeof n=="number"?n:null}function Re(e,t){if(!(typeof localStorage>"u"))try{let n=K();n[e]=t,H(n);}catch{}}function _({storageKey:e,defaultCount:t=3,currentCount:n,loading:o,minCount:s=1,maxCount:a}){let[k,m]=react.useState(()=>A(t,s,a)),f=react.useRef(false);return w(()=>{if(!e)return;let u=Te(e);if(u===null)return;let l=A(u,s,a);m(i=>Object.is(i,l)?i:l);},[e,s,a]),react.useEffect(()=>{if(!o&&n!==void 0){let u=A(n,s,a);m(u),e&&Re(e,u);}},[o,n,e,s,a]),react.useEffect(()=>{S()&&!e&&!f.current&&(console.warn("[Loaded] SmartSkeletonList used without storageKey. The count will reset on remount. Add a storageKey to persist across sessions."),f.current=true);},[e]),k}function A(e,t,n){let o=Math.max(e,t);return n!==void 0&&(o=Math.min(o,n)),o}function Le({loading:e=false,items:t,renderItem:n,renderSkeleton:o,storageKey:s,defaultCount:a=3,minCount:k=1,maxCount:m,animate:f=true,seed:u,suppressRefWarning:l=false,keyExtractor:i=(r,d)=>d}){let r=_({storageKey:s,defaultCount:a,currentCount:t?.length,loading:e,minCount:k,maxCount:m});if(e){let d=new Array(r);for(let c=0;c<r;c+=1){let b=u===void 0?`${c}`:`${u}:${c}`;d[c]=jsxRuntime.jsx(N,{loading:true,element:o(c),animate:f,seed:b,suppressRefWarning:l},`skeleton-${c}`);}return jsxRuntime.jsx(jsxRuntime.Fragment,{children:d})}return !t||t.length===0?null:jsxRuntime.jsx(jsxRuntime.Fragment,{children:t.map((d,c)=>jsxRuntime.jsx(react.Fragment,{children:n(d,c)},i(d,c)))})}exports.SkeletonContext=h;exports.SmartSkeleton=N;exports.SmartSkeletonList=Le;exports.useIsSkeletonMode=Z;exports.usePersistedCount=_;//# sourceMappingURL=index.cjs.map
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SkeletonContext/SkeletonContext.tsx","../src/utils/isDevEnv.ts","../src/utils/useIsomorphicLayoutEffect.ts","../src/components/SmartSkeleton/SmartSkeleton.tsx","../src/hooks/usePersistedCount/usePersistedCount.ts","../src/components/SmartSkeletonList/SmartSkeletonList.tsx"],"names":["SkeletonContext","createContext","useIsSkeletonMode","useContext","isDevEnv","maybeGlobal","override","devFlag","maybeProcess","nodeEnv","canUseDOM","useIsomorphicLayoutEffect","useLayoutEffect","useEffect","TEXT_WIDTH_MIN_CH","TEXT_WIDTH_MAX_CH","isElement","value","maybeElement","warnedComponents","isUsableElement","resolveRefTarget","node","nativeElement","getElementDisplayName","element","type","fn","obj","getOriginalRef","propsRef","legacyRef","forwardRef","originalRef","MEDIA_ELEMENTS","SVG_ELEMENTS","INTERACTIVE_ELEMENTS","BUTTON_LIKE_SELECTOR","SKIPPED_TAGS","getTagName","el","isButtonLikeElement","tagName","isButtonLikeDescendant","isContentElement","calculateTextWidthCh","text","seedKey","textLength","jitterRange","jitter","deterministicJitter","width","hash","index","resolveTextAlign","align","applySkeletonClasses","rootElement","options","animate","seed","baseSeed","htmlRoot","descendants","textIndex","processElement","htmlEl","textContent","widthCh","SmartSkeleton","children","loading","className","suppressRefWarning","hasAppliedRef","useRef","refWasCalledRef","lastElementRef","previousElementTypeRef","previousElementKeyRef","needsWrapper","setNeedsWrapper","useState","elementType","elementKey","previousType","previousKey","refCallback","useCallback","target","displayName","existingClassName","baseClasses","wrapperClassName","mergedClassName","jsx","cloneElement","STORAGE_KEY","LEGACY_STORAGE_KEY","STORAGE_VERSION","isRecord","toNumberRecord","result","key","maybeNumber","parseStoredCounts","readStoredCountsFromKey","raw","writeStoredCounts","counts","payload","getStoredCounts","rawNew","legacyCounts","getStoredCount","setStoredCount","count","usePersistedCount","storageKey","defaultCount","currentCount","minCount","maxCount","setCount","clampCount","hasWarnedRef","stored","next","prev","newCount","min","max","SmartSkeletonList","items","renderItem","renderSkeleton","keyExtractor","_","skeletonCount","skeletons","itemSeed","Fragment","item"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext } from \"react\";\n\nexport const SkeletonContext = createContext(false);\n\nexport function useIsSkeletonMode(): boolean {\n return useContext(SkeletonContext);\n}\n","export function isDevEnv(): boolean {\n const maybeGlobal = globalThis as unknown as Record<string, unknown>;\n\n // Manual override for environments where NODE_ENV isn't injected.\n // Example: `globalThis.__REACT_LOADED_DEV__ = true`.\n const override = maybeGlobal.__REACT_LOADED_DEV__;\n if (typeof override === \"boolean\") return override;\n\n // Common global used by some toolchains/runtimes.\n const devFlag = maybeGlobal.__DEV__;\n if (typeof devFlag === \"boolean\") return devFlag;\n\n const maybeProcess = (globalThis as unknown as { process?: unknown }).process;\n const nodeEnv =\n typeof maybeProcess === \"object\" && maybeProcess !== null\n ? (maybeProcess as { env?: { NODE_ENV?: unknown } }).env?.NODE_ENV\n : undefined;\n\n if (typeof nodeEnv === \"string\") {\n return nodeEnv !== \"production\";\n }\n\n // No environment detected — assume production (convention: opt-in to dev mode).\n return false;\n}\n","import { useEffect, useLayoutEffect } from \"react\";\n\nconst canUseDOM =\n typeof globalThis !== \"undefined\" &&\n typeof (globalThis as { document?: unknown }).document !== \"undefined\";\n\nexport const useIsomorphicLayoutEffect = canUseDOM\n ? useLayoutEffect\n : useEffect;\n","import {\n cloneElement,\n type ReactElement,\n type Ref,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { isDevEnv } from \"../../utils/isDevEnv\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\nimport { SkeletonContext } from \"../SkeletonContext/SkeletonContext\";\nimport \"./SmartSkeleton.css\";\n\n// Text width configuration (in ch units)\nconst TEXT_WIDTH_MIN_CH = 6;\nconst TEXT_WIDTH_MAX_CH = 40;\n\nfunction isElement(value: unknown): value is Element {\n if (!value || typeof value !== \"object\") return false;\n const maybeElement = value as Element;\n // Must have nodeType 1 (Element) and a working querySelectorAll\n if (maybeElement.nodeType !== 1) return false;\n if (typeof maybeElement.tagName !== \"string\") return false;\n if (typeof maybeElement.querySelectorAll !== \"function\") return false;\n // Additional check: instanceof Element if available\n if (typeof Element !== \"undefined\" && !(value instanceof Element)) {\n return false;\n }\n return true;\n}\n\nconst warnedComponents = new Set<string>();\n\nfunction isUsableElement(value: unknown): value is Element {\n if (!isElement(value)) return false;\n // Test that querySelectorAll actually works\n try {\n (value as Element).querySelectorAll(\"*\");\n return true;\n } catch {\n return false;\n }\n}\n\nfunction resolveRefTarget(node: unknown): Element | null {\n if (isUsableElement(node)) return node;\n if (node && typeof node === \"object\" && \"nativeElement\" in node) {\n const nativeElement = (node as { nativeElement?: unknown }).nativeElement;\n if (isUsableElement(nativeElement)) return nativeElement;\n }\n return null;\n}\n\nfunction getElementDisplayName(element: ReactElement): string {\n const type = element.type;\n if (typeof type === \"string\") {\n return `<${type}>`;\n }\n if (typeof type === \"function\") {\n const fn = type as { displayName?: string; name?: string };\n return `<${fn.displayName || fn.name || \"Unknown\"}>`;\n }\n if (typeof type === \"object\" && type !== null) {\n const obj = type as { displayName?: string; name?: string };\n return `<${obj.displayName || obj.name || \"Unknown\"}>`;\n }\n return \"<Unknown>\";\n}\n\n/**\n * Get the original ref from the element, supporting both React 18 and React 19.\n * React 19: ref is a regular prop on element.props.ref\n * React 18: ref is on element.ref\n */\nfunction getOriginalRef(element: ReactElement): Ref<unknown> | undefined {\n // React 19 style (ref as prop)\n const propsRef = (element.props as { ref?: Ref<unknown> })?.ref;\n if (propsRef) return propsRef;\n\n // React 18 style\n const legacyRef = (element as ReactElement & { ref?: Ref<unknown> }).ref;\n if (legacyRef) return legacyRef;\n\n return undefined;\n}\n\n/**\n * Forward a ref value to the original ref (callback or object style).\n */\nfunction forwardRef(originalRef: Ref<unknown> | undefined, node: unknown) {\n if (!originalRef) return;\n if (typeof originalRef === \"function\") {\n originalRef(node);\n } else {\n (originalRef as React.MutableRefObject<unknown>).current = node;\n }\n}\n\nconst MEDIA_ELEMENTS = new Set([\"IMG\", \"VIDEO\", \"CANVAS\"]);\nconst SVG_ELEMENTS = new Set([\"SVG\"]);\n\nconst INTERACTIVE_ELEMENTS = new Set([\n \"BUTTON\",\n \"INPUT\",\n \"TEXTAREA\",\n \"SELECT\",\n \"A\",\n]);\n\nconst BUTTON_LIKE_SELECTOR = \"button,input,textarea,select,a,[role='button']\";\nconst SKIPPED_TAGS = new Set([\n \"SCRIPT\",\n \"STYLE\",\n \"LINK\",\n \"META\",\n \"NOSCRIPT\",\n \"TEMPLATE\",\n]);\n\nfunction getTagName(el: Element): string {\n return el.tagName.toUpperCase();\n}\n\nfunction isButtonLikeElement(el: Element, tagName = getTagName(el)): boolean {\n if (INTERACTIVE_ELEMENTS.has(tagName)) return true;\n const role = el.getAttribute(\"role\");\n return role === \"button\";\n}\n\nfunction isButtonLikeDescendant(el: Element, tagName: string): boolean {\n const closestButton = el.closest(BUTTON_LIKE_SELECTOR);\n return Boolean(closestButton && !isButtonLikeElement(el, tagName));\n}\n\nfunction isContentElement(el: Element, tagName = getTagName(el)): boolean {\n if (MEDIA_ELEMENTS.has(tagName)) return true;\n if (SVG_ELEMENTS.has(tagName)) return true;\n if (isButtonLikeElement(el, tagName)) return true;\n\n const isLeafNode = el.childElementCount === 0;\n\n // Text elements that are leaf nodes (no child elements, only text)\n if (isLeafNode && el.textContent?.trim()) return true;\n\n return false;\n}\n\n/**\n * Calculate text skeleton width in ch units based on text content.\n * Uses a deterministic jitter: widthCh = clamp(6, 40, len + 2 + jitter)\n */\nfunction calculateTextWidthCh(text: string, seedKey: string): number {\n const textLength = text.length;\n const jitterRange = Math.max(4, 0.8 * textLength);\n const jitter = deterministicJitter(seedKey) * jitterRange;\n const width = textLength + 2 + jitter;\n return Math.max(TEXT_WIDTH_MIN_CH, Math.min(TEXT_WIDTH_MAX_CH, width));\n}\n\nfunction deterministicJitter(seedKey: string): number {\n if (!seedKey) return 0;\n let hash = 2166136261;\n for (let index = 0; index < seedKey.length; index += 1) {\n hash ^= seedKey.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n const normalized = (hash >>> 0) / 0xffffffff;\n return normalized * 2 - 1;\n}\n\nfunction resolveTextAlign(el: HTMLElement): \"left\" | \"center\" | \"right\" {\n const align = globalThis.getComputedStyle(el).textAlign;\n if (align === \"center\") return \"center\";\n if (align === \"right\" || align === \"end\") return \"right\";\n return \"left\";\n}\n\nexport function applySkeletonClasses(\n rootElement: Element,\n options: { animate?: boolean; seed?: string | number } = {},\n): void {\n const { animate = true, seed } = options;\n const baseSeed =\n seed === undefined || seed === null ? \"loaded\" : String(seed);\n\n if (!isElement(rootElement)) {\n return;\n }\n\n const htmlRoot = rootElement as HTMLElement;\n\n // Apply skeleton mode to the root element\n htmlRoot.classList.add(\"loaded-skeleton-mode\");\n\n if (animate) {\n htmlRoot.classList.add(\"loaded-animate\");\n }\n\n // Apply background class for standalone usage (when not used via SmartSkeleton JSX)\n // If element has loaded-skeleton-wrapper, CSS handles bg via > :first-child rule\n // If element already has loaded-skeleton-bg (from JSX), this is a no-op\n const isWrapper = htmlRoot.classList.contains(\"loaded-skeleton-wrapper\");\n if (!isWrapper) {\n htmlRoot.classList.add(\"loaded-skeleton-bg\");\n }\n\n // Only add specific classes where needed (text, media, content)\n const descendants = rootElement.getElementsByTagName(\"*\");\n\n let textIndex = 0;\n\n const processElement = (el: Element) => {\n const tagName = getTagName(el);\n if (SKIPPED_TAGS.has(tagName)) return;\n if (!isContentElement(el, tagName)) return;\n if (isButtonLikeDescendant(el, tagName)) return;\n\n const htmlEl = el as HTMLElement;\n const textContent = el.textContent?.trim();\n const isLeafWithText = el.childElementCount === 0 && textContent;\n\n if (\n isLeafWithText &&\n !MEDIA_ELEMENTS.has(tagName) &&\n !SVG_ELEMENTS.has(tagName) &&\n !isButtonLikeElement(el, tagName)\n ) {\n // Text elements: overlay bar with ch-based width\n htmlEl.classList.add(\"loaded-text-skeleton\");\n htmlEl.dataset.skeletonAlign = resolveTextAlign(htmlEl);\n const seedKey = `${baseSeed}|${textIndex}|${textContent ?? \"\"}`;\n textIndex += 1;\n const widthCh = calculateTextWidthCh(textContent ?? \"\", seedKey);\n htmlEl.style.setProperty(\"--skeleton-text-width\", `${widthCh}ch`);\n } else if (MEDIA_ELEMENTS.has(tagName)) {\n // Media elements\n htmlEl.classList.add(\"loaded-skeleton-media\");\n } else if (SVG_ELEMENTS.has(tagName)) {\n // SVG elements rendered as rounded content blocks\n htmlEl.classList.add(\"loaded-skeleton-content\");\n htmlEl.classList.add(\"loaded-skeleton-svg\");\n } else {\n // Interactive elements (buttons, inputs, etc.)\n htmlEl.classList.add(\"loaded-skeleton-content\");\n // Prevent keyboard focus / interaction while in skeleton mode.\n // aria-hidden does not remove elements from the tab order.\n htmlEl.setAttribute(\"tabindex\", \"-1\");\n }\n };\n\n processElement(rootElement);\n for (const el of descendants) {\n processElement(el);\n }\n}\n\nexport interface SmartSkeletonProps {\n /** The skeleton element with mock data, rendered when loading */\n element: ReactElement;\n /** The real content to render when not loading. If omitted, returns null when loading=false. */\n children?: ReactElement;\n /** Whether the skeleton is currently loading. Default: false */\n loading?: boolean;\n /** Enable shimmer animation. Default: true */\n animate?: boolean;\n /** Additional CSS class name */\n className?: string;\n /** Optional seed to stabilize skeleton text widths */\n seed?: string | number;\n /** Suppress warning when auto-wrapper is applied. Default: false */\n suppressRefWarning?: boolean;\n}\n\nexport function SmartSkeleton({\n element,\n children,\n loading = false,\n animate = true,\n className = \"\",\n seed,\n suppressRefWarning = false,\n}: SmartSkeletonProps): ReactElement | null {\n const hasAppliedRef = useRef(false);\n const refWasCalledRef = useRef(false);\n const lastElementRef = useRef<ReactElement | null>(null);\n const previousElementTypeRef = useRef<ReactElement[\"type\"] | null>(null);\n const previousElementKeyRef = useRef<ReactElement[\"key\"] | null>(null);\n const [needsWrapper, setNeedsWrapper] = useState(false);\n\n // Reset flags when loading changes or element changes\n if (!loading || lastElementRef.current !== element) {\n hasAppliedRef.current = false;\n refWasCalledRef.current = false;\n lastElementRef.current = element;\n }\n\n useEffect(() => {\n const elementType = element.type;\n const elementKey = element.key ?? null;\n const previousType = previousElementTypeRef.current;\n const previousKey = previousElementKeyRef.current;\n\n if (\n previousType !== null &&\n (previousType !== elementType || previousKey !== elementKey)\n ) {\n setNeedsWrapper(false);\n }\n\n previousElementTypeRef.current = elementType;\n previousElementKeyRef.current = elementKey;\n }, [element.type, element.key]);\n\n const originalRef = getOriginalRef(element);\n\n const refCallback = useCallback(\n (node: unknown) => {\n refWasCalledRef.current = true;\n\n const target = resolveRefTarget(node);\n\n // If we received a node but couldn't get a DOM element from it,\n // we need to use a wrapper\n if (node !== null && !target && !needsWrapper) {\n setNeedsWrapper(true);\n\n // Emit warning (deduplicated by component name)\n if (!suppressRefWarning && isDevEnv()) {\n const displayName = getElementDisplayName(element);\n if (!warnedComponents.has(displayName)) {\n console.warn(\n `[SmartSkeleton] ${displayName} does not forward its ref to a DOM element. ` +\n `A wrapper <div> has been added automatically. Use forwardRef to avoid this.`,\n );\n warnedComponents.add(displayName);\n }\n }\n return;\n }\n\n if (target && loading && !hasAppliedRef.current) {\n applySkeletonClasses(target, { animate, seed });\n hasAppliedRef.current = true;\n }\n\n // Forward ref to original element\n forwardRef(originalRef, node);\n },\n [\n loading,\n element,\n needsWrapper,\n suppressRefWarning,\n originalRef,\n animate,\n seed,\n ],\n );\n\n // Detect if ref was never called (component ignores ref entirely)\n // useLayoutEffect runs synchronously after DOM mutations but before paint\n useIsomorphicLayoutEffect(() => {\n if (!loading || needsWrapper) return;\n\n // At this point, React has already attempted to attach refs\n // If refWasCalledRef is still false, the component ignored the ref\n if (!refWasCalledRef.current) {\n setNeedsWrapper(true);\n\n // Emit warning\n if (!suppressRefWarning && isDevEnv()) {\n const displayName = getElementDisplayName(element);\n if (!warnedComponents.has(displayName)) {\n console.warn(\n `[SmartSkeleton] ${displayName} does not accept a ref. ` +\n `A wrapper <div> has been added automatically. Use forwardRef to avoid this.`,\n );\n warnedComponents.add(displayName);\n }\n }\n }\n }, [loading, needsWrapper, element, suppressRefWarning]);\n\n // Not loading: return children or null\n if (!loading) {\n return children ?? null;\n }\n\n // Build merged className for skeleton mode\n const elementProps = element.props as { className?: string };\n const existingClassName = elementProps.className ?? \"\";\n\n // Base classes for skeleton mode\n const baseClasses = [\"loaded-skeleton-mode\", animate && \"loaded-animate\"]\n .filter(Boolean)\n .join(\" \");\n\n // When wrapping: wrapper gets mode + wrapper marker (no bg - it goes on child via ref)\n const wrapperClassName = [baseClasses, \"loaded-skeleton-wrapper\", className]\n .filter(Boolean)\n .join(\" \");\n\n // When not wrapping: element gets mode + bg directly (for SSR)\n const mergedClassName = [\n existingClassName,\n baseClasses,\n \"loaded-skeleton-bg\",\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <SkeletonContext.Provider value={true}>\n {needsWrapper ? (\n <div ref={refCallback} className={wrapperClassName} aria-hidden=\"true\">\n {element}\n </div>\n ) : (\n cloneElement(element as ReactElement<Record<string, unknown>>, {\n ref: refCallback,\n className: mergedClassName,\n \"aria-hidden\": true,\n })\n )}\n </SkeletonContext.Provider>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { isDevEnv } from \"../../utils/isDevEnv\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\n\nconst STORAGE_KEY = \"react-loaded\";\nconst LEGACY_STORAGE_KEY = \"loaded\";\nconst STORAGE_VERSION = 1 as const;\n\ntype StoredCounts = Record<string, number>;\ntype StoredPayloadV1 = { v: typeof STORAGE_VERSION; counts: StoredCounts };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction toNumberRecord(value: unknown): StoredCounts {\n if (!isRecord(value)) return {};\n const result: StoredCounts = {};\n for (const [key, maybeNumber] of Object.entries(value)) {\n if (typeof maybeNumber === \"number\") {\n result[key] = maybeNumber;\n }\n }\n return result;\n}\n\nfunction parseStoredCounts(value: unknown): StoredCounts {\n // Current schema: { v: 1, counts: Record<string, number> }\n if (isRecord(value) && value.v === STORAGE_VERSION) {\n return toNumberRecord(value.counts);\n }\n\n // Legacy schema: Record<string, number>\n return toNumberRecord(value);\n}\n\nfunction readStoredCountsFromKey(key: string): StoredCounts {\n if (typeof localStorage === \"undefined\") return {};\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return {};\n return parseStoredCounts(JSON.parse(raw));\n } catch {\n return {};\n }\n}\n\nfunction writeStoredCounts(counts: StoredCounts): void {\n if (typeof localStorage === \"undefined\") return;\n const payload: StoredPayloadV1 = { v: STORAGE_VERSION, counts };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));\n } catch {\n // Silently fail if localStorage is full or unavailable\n }\n}\n\nfunction getStoredCounts(): Record<string, number> {\n if (typeof localStorage === \"undefined\") return {};\n\n try {\n const rawNew = localStorage.getItem(STORAGE_KEY);\n if (rawNew !== null) {\n return parseStoredCounts(JSON.parse(rawNew));\n }\n\n // Backward compatibility: migrate legacy key once if present.\n const legacyCounts = readStoredCountsFromKey(LEGACY_STORAGE_KEY);\n if (Object.keys(legacyCounts).length > 0) {\n writeStoredCounts(legacyCounts);\n }\n return legacyCounts;\n } catch {\n return {};\n }\n}\n\nfunction getStoredCount(key: string): number | null {\n const counts = getStoredCounts();\n const value = counts[key];\n return typeof value === \"number\" ? value : null;\n}\n\nfunction setStoredCount(key: string, count: number): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n const counts = getStoredCounts();\n counts[key] = count;\n writeStoredCounts(counts);\n } catch {\n // Silently fail if localStorage is full or unavailable\n }\n}\n\nexport interface UsePersistedCountOptions {\n storageKey?: string;\n defaultCount?: number;\n currentCount?: number;\n loading: boolean;\n minCount?: number;\n maxCount?: number;\n}\n\nexport function usePersistedCount({\n storageKey,\n defaultCount = 3,\n currentCount,\n loading,\n minCount = 1,\n maxCount,\n}: UsePersistedCountOptions): number {\n // Always start from the default to match SSR output, then (on the client)\n // sync to the persisted value in a layout effect before first paint.\n const [count, setCount] = useState<number>(() =>\n clampCount(defaultCount, minCount, maxCount),\n );\n\n const hasWarnedRef = useRef(false);\n\n useIsomorphicLayoutEffect(() => {\n if (!storageKey) return;\n const stored = getStoredCount(storageKey);\n if (stored === null) return;\n const next = clampCount(stored, minCount, maxCount);\n setCount((prev) => (Object.is(prev, next) ? prev : next));\n }, [storageKey, minCount, maxCount]);\n\n useEffect(() => {\n if (!loading && currentCount !== undefined) {\n const newCount = clampCount(currentCount, minCount, maxCount);\n setCount(newCount);\n\n if (storageKey) {\n setStoredCount(storageKey, newCount);\n }\n }\n }, [loading, currentCount, storageKey, minCount, maxCount]);\n\n useEffect(() => {\n if (isDevEnv() && !storageKey && !hasWarnedRef.current) {\n console.warn(\n \"[Loaded] SmartSkeletonList used without storageKey. \" +\n \"The count will reset on remount. Add a storageKey to persist across sessions.\",\n );\n hasWarnedRef.current = true;\n }\n }, [storageKey]);\n\n return count;\n}\n\nfunction clampCount(\n value: number,\n min: number,\n max: number | undefined,\n): number {\n let result = Math.max(value, min);\n if (max !== undefined) {\n result = Math.min(result, max);\n }\n return result;\n}\n","import { Fragment, type ReactElement } from \"react\";\nimport { usePersistedCount } from \"../../hooks/usePersistedCount/usePersistedCount\";\nimport { SmartSkeleton } from \"../SmartSkeleton/SmartSkeleton\";\n\nexport interface SmartSkeletonListProps<T> {\n /** Whether the list is currently loading. Default: false */\n loading?: boolean;\n /** The items to render. Pass undefined while loading. */\n items: T[] | undefined;\n /** Render function for each item when loaded */\n renderItem: (item: T, index: number) => ReactElement;\n /** Render function for skeleton placeholders */\n renderSkeleton: (index: number) => ReactElement;\n /** Key for localStorage persistence. Without it, count resets on remount. */\n storageKey?: string;\n /** Initial skeleton count before any data is known. Default: 3 */\n defaultCount?: number;\n /** Minimum skeletons to show. Default: 1 */\n minCount?: number;\n /** Maximum skeletons to show */\n maxCount?: number;\n /** Enable shimmer animation. Default: true */\n animate?: boolean;\n /** Optional seed to stabilize skeleton text widths */\n seed?: string | number;\n /** Suppress warning when auto-wrapper is applied. Default: false */\n suppressRefWarning?: boolean;\n /** Extract unique key for each item. Default: index */\n keyExtractor?: (item: T, index: number) => string | number;\n}\n\nexport function SmartSkeletonList<T>({\n loading = false,\n items,\n renderItem,\n renderSkeleton,\n storageKey,\n defaultCount = 3,\n minCount = 1,\n maxCount,\n animate = true,\n seed,\n suppressRefWarning = false,\n keyExtractor = (_, index) => index,\n}: SmartSkeletonListProps<T>): ReactElement | null {\n const skeletonCount = usePersistedCount({\n storageKey,\n defaultCount,\n currentCount: items?.length,\n loading,\n minCount,\n maxCount,\n });\n\n if (loading) {\n const skeletons = new Array(skeletonCount);\n for (let index = 0; index < skeletonCount; index += 1) {\n const itemSeed = seed === undefined ? `${index}` : `${seed}:${index}`;\n skeletons[index] = (\n <SmartSkeleton\n key={`skeleton-${index}`}\n loading={true}\n element={renderSkeleton(index)}\n animate={animate}\n seed={itemSeed}\n suppressRefWarning={suppressRefWarning}\n />\n );\n }\n return <>{skeletons}</>;\n }\n\n if (!items || items.length === 0) {\n return null;\n }\n\n return (\n <>\n {items.map((item, index) => (\n <Fragment key={keyExtractor(item, index)}>\n {renderItem(item, index)}\n </Fragment>\n ))}\n </>\n );\n}\n"]}
package/dist/index.css ADDED
@@ -0,0 +1,2 @@
1
+ :root{--loaded-bg-wrapper: rgba(229, 231, 235, 1);--loaded-bg-content: rgba(156, 163, 175, .6);--loaded-border-radius: 4px;--loaded-text-inset: .3em}.loaded-skeleton-mode{user-select:none;pointer-events:none}.loaded-skeleton-mode,.loaded-skeleton-mode *{color:transparent!important;background-color:transparent!important;border-color:transparent!important;background-image:none!important;box-shadow:none!important;text-shadow:none!important}.loaded-skeleton-mode *:before,.loaded-skeleton-mode *:after{background:transparent!important;background-image:none!important;color:transparent!important;border-color:transparent!important;box-shadow:none!important;text-shadow:none!important}.loaded-skeleton-mode ::placeholder{color:transparent!important;opacity:0!important}.loaded-skeleton-mode.loaded-skeleton-bg,.loaded-skeleton-mode .loaded-skeleton-bg{background-color:var(--loaded-bg-wrapper)!important}.loaded-skeleton-mode.loaded-skeleton-wrapper>:first-child{background-color:var(--loaded-bg-wrapper)!important}.loaded-skeleton-mode .loaded-skeleton-content{background-color:var(--loaded-bg-content)!important}.loaded-skeleton-mode .loaded-text-skeleton{position:relative;background-color:transparent!important}.loaded-skeleton-mode .loaded-text-skeleton:before{content:""!important;position:absolute!important;left:0!important;width:var(--skeleton-text-width, 100%)!important;max-width:90%!important;top:var(--loaded-text-inset)!important;bottom:var(--loaded-text-inset)!important;background:var(--loaded-bg-content)!important;background-image:none!important;border-radius:var(--loaded-border-radius)!important;pointer-events:none!important;transform:none!important}.loaded-skeleton-mode .loaded-text-skeleton[data-skeleton-align=center]:before{left:50%!important;transform:translate(-50%)!important}.loaded-skeleton-mode .loaded-text-skeleton[data-skeleton-align=right]:before{left:auto!important;right:0!important}.loaded-skeleton-mode .loaded-skeleton-media{background-color:var(--loaded-bg-content)!important;opacity:1!important}.loaded-skeleton-mode .loaded-skeleton-svg{background-color:var(--loaded-bg-content)!important;border-radius:50%!important}.loaded-skeleton-mode .loaded-skeleton-svg *{visibility:hidden!important}.loaded-skeleton-mode img.loaded-skeleton-media{object-position:-9999px -9999px!important}@keyframes loaded-shimmer{0%{opacity:1}50%{opacity:.6}to{opacity:1}}.loaded-skeleton-mode.loaded-animate{animation:loaded-shimmer 1.5s ease-in-out infinite}
2
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SmartSkeleton/SmartSkeleton.css"],"sourcesContent":[":root {\n --loaded-bg-wrapper: rgba(229, 231, 235, 1);\n --loaded-bg-content: rgba(156, 163, 175, 0.6);\n --loaded-border-radius: 4px;\n --loaded-text-inset: 0.3em;\n}\n\n.loaded-skeleton-mode {\n user-select: none;\n pointer-events: none;\n}\n\n/* Base styles applied to ALL elements inside skeleton via CSS selector */\n.loaded-skeleton-mode,\n.loaded-skeleton-mode * {\n color: transparent !important;\n background-color: transparent !important;\n border-color: transparent !important;\n background-image: none !important;\n box-shadow: none !important;\n text-shadow: none !important;\n}\n\n/* Reset pseudo-elements */\n.loaded-skeleton-mode *::before,\n.loaded-skeleton-mode *::after {\n background: transparent !important;\n background-image: none !important;\n color: transparent !important;\n border-color: transparent !important;\n box-shadow: none !important;\n text-shadow: none !important;\n}\n\n/* Hide input placeholders during skeleton */\n.loaded-skeleton-mode ::placeholder {\n color: transparent !important;\n opacity: 0 !important;\n}\n\n/* Main background for non-wrapper case (element has loaded-skeleton-bg directly) */\n.loaded-skeleton-mode.loaded-skeleton-bg,\n.loaded-skeleton-mode .loaded-skeleton-bg {\n background-color: var(--loaded-bg-wrapper) !important;\n}\n\n/* Main background for wrapper case (applies to first child to preserve border-radius) */\n.loaded-skeleton-mode.loaded-skeleton-wrapper > :first-child {\n background-color: var(--loaded-bg-wrapper) !important;\n}\n\n/* Content elements (buttons, inputs, etc.) */\n.loaded-skeleton-mode .loaded-skeleton-content {\n background-color: var(--loaded-bg-content) !important;\n}\n\n/* Text skeleton with pseudo-element */\n.loaded-skeleton-mode .loaded-text-skeleton {\n position: relative;\n background-color: transparent !important;\n}\n\n/* Text skeleton bar drawn via pseudo-element */\n.loaded-skeleton-mode .loaded-text-skeleton::before {\n content: \"\" !important;\n position: absolute !important;\n left: 0 !important;\n width: var(--skeleton-text-width, 100%) !important;\n max-width: 90% !important;\n top: var(--loaded-text-inset) !important;\n bottom: var(--loaded-text-inset) !important;\n background: var(--loaded-bg-content) !important;\n background-image: none !important;\n border-radius: var(--loaded-border-radius) !important;\n pointer-events: none !important;\n transform: none !important;\n}\n\n.loaded-skeleton-mode\n .loaded-text-skeleton[data-skeleton-align=\"center\"]::before {\n left: 50% !important;\n transform: translateX(-50%) !important;\n}\n\n.loaded-skeleton-mode\n .loaded-text-skeleton[data-skeleton-align=\"right\"]::before {\n left: auto !important;\n right: 0 !important;\n}\n\n/* Media elements (images, videos, etc.) */\n.loaded-skeleton-mode .loaded-skeleton-media {\n background-color: var(--loaded-bg-content) !important;\n opacity: 1 !important;\n}\n\n/* SVG elements rendered as rounded blocks */\n.loaded-skeleton-mode .loaded-skeleton-svg {\n background-color: var(--loaded-bg-content) !important;\n border-radius: 50% !important;\n}\n\n/* Hide SVG internal content (paths, circles, etc.) */\n.loaded-skeleton-mode .loaded-skeleton-svg * {\n visibility: hidden !important;\n}\n\n.loaded-skeleton-mode img.loaded-skeleton-media {\n object-position: -9999px -9999px !important;\n}\n\n/* Animation */\n@keyframes loaded-shimmer {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\n\n.loaded-skeleton-mode.loaded-animate {\n animation: loaded-shimmer 1.5s ease-in-out infinite;\n}\n"],"mappings":"AAAA,MACE,qBAAqB,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GACzC,qBAAqB,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IACzC,wBAAwB,IACxB,qBAAqB,IACvB,CAEA,CAAC,qBACC,YAAa,KACb,eAAgB,IAClB,CAGA,CANC,qBAOD,CAPC,qBAOqB,EACpB,MAAO,sBACP,iBAAkB,sBAClB,aAAc,sBACd,iBAAkB,eAClB,WAAY,eACZ,YAAa,cACf,CAGA,CAjBC,qBAiBqB,CAAC,QACvB,CAlBC,qBAkBqB,CAAC,OACrB,WAAY,sBACZ,iBAAkB,eAClB,MAAO,sBACP,aAAc,sBACd,WAAY,eACZ,YAAa,cACf,CAGA,CA5BC,qBA4BqB,cACpB,MAAO,sBACP,QAAS,WACX,CAGA,CAlCC,oBAkCoB,CAAC,mBACtB,CAnCC,qBAmCqB,CADA,mBAEpB,iBAAkB,IAAI,8BACxB,CAGA,CAxCC,oBAwCoB,CAAC,uBAAwB,CAAE,aAC9C,iBAAkB,IAAI,8BACxB,CAGA,CA7CC,qBA6CqB,CAAC,wBACrB,iBAAkB,IAAI,8BACxB,CAGA,CAlDC,qBAkDqB,CAAC,qBACrB,SAAU,SACV,iBAAkB,qBACpB,CAGA,CAxDC,qBAwDqB,CANC,oBAMoB,QACzC,QAAS,aACT,SAAU,mBACV,KAAM,YACN,MAAO,IAAI,qBAAqB,EAAE,gBAClC,UAAW,cACX,IAAK,IAAI,+BACT,OAAQ,IAAI,+BACZ,WAAY,IAAI,+BAChB,iBAAkB,eAClB,cAAe,IAAI,kCACnB,eAAgB,eAChB,UAAW,cACb,CAEA,CAvEC,qBAwEC,CAtBqB,oBAsBA,CAAC,2BAA6B,QACnD,KAAM,cACN,UAAW,UAAW,eACxB,CAEA,CA7EC,qBA8EC,CA5BqB,oBA4BA,CAAC,0BAA4B,QAClD,KAAM,eACN,MAAO,WACT,CAGA,CApFC,qBAoFqB,CAAC,sBACrB,iBAAkB,IAAI,+BACtB,QAAS,WACX,CAGA,CA1FC,qBA0FqB,CAAC,oBACrB,iBAAkB,IAAI,+BAlGxB,cAmGiB,aACjB,CAGA,CAhGC,qBAgGqB,CANC,oBAMoB,EACzC,WAAY,gBACd,CAEA,CApGC,qBAoGqB,GAAG,CAhBF,sBAiBrB,gBAAiB,QAAQ,iBAC3B,CAGA,WAAW,eACT,GACE,QAAS,CACX,CACA,IACE,QAAS,EACX,CACA,GACE,QAAS,CACX,CACF,CAEA,CArHC,oBAqHoB,CAAC,eACpB,UAAW,eAAe,KAAK,YAAY,QAC7C","names":[]}
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactElement } from 'react';
3
+
4
+ declare const SkeletonContext: react.Context<boolean>;
5
+ declare function useIsSkeletonMode(): boolean;
6
+
7
+ interface SmartSkeletonProps {
8
+ /** The skeleton element with mock data, rendered when loading */
9
+ element: ReactElement;
10
+ /** The real content to render when not loading. If omitted, returns null when loading=false. */
11
+ children?: ReactElement;
12
+ /** Whether the skeleton is currently loading. Default: false */
13
+ loading?: boolean;
14
+ /** Enable shimmer animation. Default: true */
15
+ animate?: boolean;
16
+ /** Additional CSS class name */
17
+ className?: string;
18
+ /** Optional seed to stabilize skeleton text widths */
19
+ seed?: string | number;
20
+ /** Suppress warning when auto-wrapper is applied. Default: false */
21
+ suppressRefWarning?: boolean;
22
+ }
23
+ declare function SmartSkeleton({ element, children, loading, animate, className, seed, suppressRefWarning, }: SmartSkeletonProps): ReactElement | null;
24
+
25
+ interface SmartSkeletonListProps<T> {
26
+ /** Whether the list is currently loading. Default: false */
27
+ loading?: boolean;
28
+ /** The items to render. Pass undefined while loading. */
29
+ items: T[] | undefined;
30
+ /** Render function for each item when loaded */
31
+ renderItem: (item: T, index: number) => ReactElement;
32
+ /** Render function for skeleton placeholders */
33
+ renderSkeleton: (index: number) => ReactElement;
34
+ /** Key for localStorage persistence. Without it, count resets on remount. */
35
+ storageKey?: string;
36
+ /** Initial skeleton count before any data is known. Default: 3 */
37
+ defaultCount?: number;
38
+ /** Minimum skeletons to show. Default: 1 */
39
+ minCount?: number;
40
+ /** Maximum skeletons to show */
41
+ maxCount?: number;
42
+ /** Enable shimmer animation. Default: true */
43
+ animate?: boolean;
44
+ /** Optional seed to stabilize skeleton text widths */
45
+ seed?: string | number;
46
+ /** Suppress warning when auto-wrapper is applied. Default: false */
47
+ suppressRefWarning?: boolean;
48
+ /** Extract unique key for each item. Default: index */
49
+ keyExtractor?: (item: T, index: number) => string | number;
50
+ }
51
+ declare function SmartSkeletonList<T>({ loading, items, renderItem, renderSkeleton, storageKey, defaultCount, minCount, maxCount, animate, seed, suppressRefWarning, keyExtractor, }: SmartSkeletonListProps<T>): ReactElement | null;
52
+
53
+ interface UsePersistedCountOptions {
54
+ storageKey?: string;
55
+ defaultCount?: number;
56
+ currentCount?: number;
57
+ loading: boolean;
58
+ minCount?: number;
59
+ maxCount?: number;
60
+ }
61
+ declare function usePersistedCount({ storageKey, defaultCount, currentCount, loading, minCount, maxCount, }: UsePersistedCountOptions): number;
62
+
63
+ export { SkeletonContext, SmartSkeleton, SmartSkeletonList, type SmartSkeletonListProps, type SmartSkeletonProps, type UsePersistedCountOptions, useIsSkeletonMode, usePersistedCount };
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactElement } from 'react';
3
+
4
+ declare const SkeletonContext: react.Context<boolean>;
5
+ declare function useIsSkeletonMode(): boolean;
6
+
7
+ interface SmartSkeletonProps {
8
+ /** The skeleton element with mock data, rendered when loading */
9
+ element: ReactElement;
10
+ /** The real content to render when not loading. If omitted, returns null when loading=false. */
11
+ children?: ReactElement;
12
+ /** Whether the skeleton is currently loading. Default: false */
13
+ loading?: boolean;
14
+ /** Enable shimmer animation. Default: true */
15
+ animate?: boolean;
16
+ /** Additional CSS class name */
17
+ className?: string;
18
+ /** Optional seed to stabilize skeleton text widths */
19
+ seed?: string | number;
20
+ /** Suppress warning when auto-wrapper is applied. Default: false */
21
+ suppressRefWarning?: boolean;
22
+ }
23
+ declare function SmartSkeleton({ element, children, loading, animate, className, seed, suppressRefWarning, }: SmartSkeletonProps): ReactElement | null;
24
+
25
+ interface SmartSkeletonListProps<T> {
26
+ /** Whether the list is currently loading. Default: false */
27
+ loading?: boolean;
28
+ /** The items to render. Pass undefined while loading. */
29
+ items: T[] | undefined;
30
+ /** Render function for each item when loaded */
31
+ renderItem: (item: T, index: number) => ReactElement;
32
+ /** Render function for skeleton placeholders */
33
+ renderSkeleton: (index: number) => ReactElement;
34
+ /** Key for localStorage persistence. Without it, count resets on remount. */
35
+ storageKey?: string;
36
+ /** Initial skeleton count before any data is known. Default: 3 */
37
+ defaultCount?: number;
38
+ /** Minimum skeletons to show. Default: 1 */
39
+ minCount?: number;
40
+ /** Maximum skeletons to show */
41
+ maxCount?: number;
42
+ /** Enable shimmer animation. Default: true */
43
+ animate?: boolean;
44
+ /** Optional seed to stabilize skeleton text widths */
45
+ seed?: string | number;
46
+ /** Suppress warning when auto-wrapper is applied. Default: false */
47
+ suppressRefWarning?: boolean;
48
+ /** Extract unique key for each item. Default: index */
49
+ keyExtractor?: (item: T, index: number) => string | number;
50
+ }
51
+ declare function SmartSkeletonList<T>({ loading, items, renderItem, renderSkeleton, storageKey, defaultCount, minCount, maxCount, animate, seed, suppressRefWarning, keyExtractor, }: SmartSkeletonListProps<T>): ReactElement | null;
52
+
53
+ interface UsePersistedCountOptions {
54
+ storageKey?: string;
55
+ defaultCount?: number;
56
+ currentCount?: number;
57
+ loading: boolean;
58
+ minCount?: number;
59
+ maxCount?: number;
60
+ }
61
+ declare function usePersistedCount({ storageKey, defaultCount, currentCount, loading, minCount, maxCount, }: UsePersistedCountOptions): number;
62
+
63
+ export { SkeletonContext, SmartSkeleton, SmartSkeletonList, type SmartSkeletonListProps, type SmartSkeletonProps, type UsePersistedCountOptions, useIsSkeletonMode, usePersistedCount };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {createContext,useContext,useRef,useState,useEffect,useCallback,cloneElement,Fragment as Fragment$1,useLayoutEffect}from'react';import {jsx,Fragment}from'react/jsx-runtime';var h=createContext(false);function Z(){return useContext(h)}function S(){let e=globalThis,t=e.__REACT_LOADED_DEV__;if(typeof t=="boolean")return t;let n=e.__DEV__;if(typeof n=="boolean")return n;let o=globalThis.process,s=typeof o=="object"&&o!==null?o.env?.NODE_ENV:void 0;return typeof s=="string"?s!=="production":false}var ne=typeof globalThis<"u"&&typeof globalThis.document<"u",w=ne?useLayoutEffect:useEffect;var le=6,ie=40;function V(e){if(!e||typeof e!="object")return false;let t=e;return !(t.nodeType!==1||typeof t.tagName!="string"||typeof t.querySelectorAll!="function"||typeof Element<"u"&&!(e instanceof Element))}var x=new Set;function M(e){if(!V(e))return false;try{return e.querySelectorAll("*"),!0}catch{return false}}function de(e){if(M(e))return e;if(e&&typeof e=="object"&&"nativeElement"in e){let t=e.nativeElement;if(M(t))return t}return null}function P(e){let t=e.type;if(typeof t=="string")return `<${t}>`;if(typeof t=="function"){let n=t;return `<${n.displayName||n.name||"Unknown"}>`}if(typeof t=="object"&&t!==null){let n=t;return `<${n.displayName||n.name||"Unknown"}>`}return "<Unknown>"}function ue(e){let t=e.props?.ref;if(t)return t;let n=e.ref;if(n)return n}function ce(e,t){e&&(typeof e=="function"?e(t):e.current=t);}var T=new Set(["IMG","VIDEO","CANVAS"]),R=new Set(["SVG"]),fe=new Set(["BUTTON","INPUT","TEXTAREA","SELECT","A"]),me="button,input,textarea,select,a,[role='button']",pe=new Set(["SCRIPT","STYLE","LINK","META","NOSCRIPT","TEMPLATE"]);function v(e){return e.tagName.toUpperCase()}function L(e,t=v(e)){return fe.has(t)?true:e.getAttribute("role")==="button"}function ke(e,t){return !!(e.closest(me)&&!L(e,t))}function be(e,t=v(e)){return !!(T.has(t)||R.has(t)||L(e,t)||e.childElementCount===0&&e.textContent?.trim())}function ge(e,t){let n=e.length,o=Math.max(4,.8*n),s=Ee(t)*o,a=n+2+s;return Math.max(le,Math.min(ie,a))}function Ee(e){if(!e)return 0;let t=2166136261;for(let o=0;o<e.length;o+=1)t^=e.charCodeAt(o),t=Math.imul(t,16777619);return (t>>>0)/4294967295*2-1}function Se(e){let t=globalThis.getComputedStyle(e).textAlign;return t==="center"?"center":t==="right"||t==="end"?"right":"left"}function ye(e,t={}){let{animate:n=true,seed:o}=t,s=o==null?"loaded":String(o);if(!V(e))return;let a=e;a.classList.add("loaded-skeleton-mode"),n&&a.classList.add("loaded-animate"),a.classList.contains("loaded-skeleton-wrapper")||a.classList.add("loaded-skeleton-bg");let m=e.getElementsByTagName("*"),f=0,u=l=>{let i=v(l);if(pe.has(i)||!be(l,i)||ke(l,i))return;let r=l,d=l.textContent?.trim();if(l.childElementCount===0&&d&&!T.has(i)&&!R.has(i)&&!L(l,i)){r.classList.add("loaded-text-skeleton"),r.dataset.skeletonAlign=Se(r);let b=`${s}|${f}|${d??""}`;f+=1;let O=ge(d??"",b);r.style.setProperty("--skeleton-text-width",`${O}ch`);}else T.has(i)?r.classList.add("loaded-skeleton-media"):R.has(i)?(r.classList.add("loaded-skeleton-content"),r.classList.add("loaded-skeleton-svg")):(r.classList.add("loaded-skeleton-content"),r.setAttribute("tabindex","-1"));};u(e);for(let l of m)u(l);}function N({element:e,children:t,loading:n=false,animate:o=true,className:s="",seed:a,suppressRefWarning:k=false}){let m=useRef(false),f=useRef(false),u=useRef(null),l=useRef(null),i=useRef(null),[r,d]=useState(false);(!n||u.current!==e)&&(m.current=false,f.current=false,u.current=e),useEffect(()=>{let p=e.type,g=e.key??null,E=l.current,q=i.current;E!==null&&(E!==p||q!==g)&&d(false),l.current=p,i.current=g;},[e.type,e.key]);let c=ue(e),b=useCallback(p=>{f.current=true;let g=de(p);if(p!==null&&!g&&!r){if(d(true),!k&&S()){let E=P(e);x.has(E)||(console.warn(`[SmartSkeleton] ${E} does not forward its ref to a DOM element. A wrapper <div> has been added automatically. Use forwardRef to avoid this.`),x.add(E));}return}g&&n&&!m.current&&(ye(g,{animate:o,seed:a}),m.current=true),ce(c,p);},[n,e,r,k,c,o,a]);if(w(()=>{if(!(!n||r)&&!f.current&&(d(true),!k&&S())){let p=P(e);x.has(p)||(console.warn(`[SmartSkeleton] ${p} does not accept a ref. A wrapper <div> has been added automatically. Use forwardRef to avoid this.`),x.add(p));}},[n,r,e,k]),!n)return t??null;let F=e.props.className??"",I=["loaded-skeleton-mode",o&&"loaded-animate"].filter(Boolean).join(" "),J=[I,"loaded-skeleton-wrapper",s].filter(Boolean).join(" "),Y=[F,I,"loaded-skeleton-bg",s].filter(Boolean).join(" ");return jsx(h.Provider,{value:true,children:r?jsx("div",{ref:b,className:J,"aria-hidden":"true",children:e}):cloneElement(e,{ref:b,className:Y,"aria-hidden":true})})}var B="react-loaded",xe="loaded",$=1;function G(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function U(e){if(!G(e))return {};let t={};for(let[n,o]of Object.entries(e))typeof o=="number"&&(t[n]=o);return t}function W(e){return G(e)&&e.v===$?U(e.counts):U(e)}function Ce(e){if(typeof localStorage>"u")return {};try{let t=localStorage.getItem(e);return t===null?{}:W(JSON.parse(t))}catch{return {}}}function H(e){if(typeof localStorage>"u")return;let t={v:$,counts:e};try{localStorage.setItem(B,JSON.stringify(t));}catch{}}function K(){if(typeof localStorage>"u")return {};try{let e=localStorage.getItem(B);if(e!==null)return W(JSON.parse(e));let t=Ce(xe);return Object.keys(t).length>0&&H(t),t}catch{return {}}}function Te(e){let n=K()[e];return typeof n=="number"?n:null}function Re(e,t){if(!(typeof localStorage>"u"))try{let n=K();n[e]=t,H(n);}catch{}}function _({storageKey:e,defaultCount:t=3,currentCount:n,loading:o,minCount:s=1,maxCount:a}){let[k,m]=useState(()=>A(t,s,a)),f=useRef(false);return w(()=>{if(!e)return;let u=Te(e);if(u===null)return;let l=A(u,s,a);m(i=>Object.is(i,l)?i:l);},[e,s,a]),useEffect(()=>{if(!o&&n!==void 0){let u=A(n,s,a);m(u),e&&Re(e,u);}},[o,n,e,s,a]),useEffect(()=>{S()&&!e&&!f.current&&(console.warn("[Loaded] SmartSkeletonList used without storageKey. The count will reset on remount. Add a storageKey to persist across sessions."),f.current=true);},[e]),k}function A(e,t,n){let o=Math.max(e,t);return n!==void 0&&(o=Math.min(o,n)),o}function Le({loading:e=false,items:t,renderItem:n,renderSkeleton:o,storageKey:s,defaultCount:a=3,minCount:k=1,maxCount:m,animate:f=true,seed:u,suppressRefWarning:l=false,keyExtractor:i=(r,d)=>d}){let r=_({storageKey:s,defaultCount:a,currentCount:t?.length,loading:e,minCount:k,maxCount:m});if(e){let d=new Array(r);for(let c=0;c<r;c+=1){let b=u===void 0?`${c}`:`${u}:${c}`;d[c]=jsx(N,{loading:true,element:o(c),animate:f,seed:b,suppressRefWarning:l},`skeleton-${c}`);}return jsx(Fragment,{children:d})}return !t||t.length===0?null:jsx(Fragment,{children:t.map((d,c)=>jsx(Fragment$1,{children:n(d,c)},i(d,c)))})}export{h as SkeletonContext,N as SmartSkeleton,Le as SmartSkeletonList,Z as useIsSkeletonMode,_ as usePersistedCount};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SkeletonContext/SkeletonContext.tsx","../src/utils/isDevEnv.ts","../src/utils/useIsomorphicLayoutEffect.ts","../src/components/SmartSkeleton/SmartSkeleton.tsx","../src/hooks/usePersistedCount/usePersistedCount.ts","../src/components/SmartSkeletonList/SmartSkeletonList.tsx"],"names":["SkeletonContext","createContext","useIsSkeletonMode","useContext","isDevEnv","maybeGlobal","override","devFlag","maybeProcess","nodeEnv","canUseDOM","useIsomorphicLayoutEffect","useLayoutEffect","useEffect","TEXT_WIDTH_MIN_CH","TEXT_WIDTH_MAX_CH","isElement","value","maybeElement","warnedComponents","isUsableElement","resolveRefTarget","node","nativeElement","getElementDisplayName","element","type","fn","obj","getOriginalRef","propsRef","legacyRef","forwardRef","originalRef","MEDIA_ELEMENTS","SVG_ELEMENTS","INTERACTIVE_ELEMENTS","BUTTON_LIKE_SELECTOR","SKIPPED_TAGS","getTagName","el","isButtonLikeElement","tagName","isButtonLikeDescendant","isContentElement","calculateTextWidthCh","text","seedKey","textLength","jitterRange","jitter","deterministicJitter","width","hash","index","resolveTextAlign","align","applySkeletonClasses","rootElement","options","animate","seed","baseSeed","htmlRoot","descendants","textIndex","processElement","htmlEl","textContent","widthCh","SmartSkeleton","children","loading","className","suppressRefWarning","hasAppliedRef","useRef","refWasCalledRef","lastElementRef","previousElementTypeRef","previousElementKeyRef","needsWrapper","setNeedsWrapper","useState","elementType","elementKey","previousType","previousKey","refCallback","useCallback","target","displayName","existingClassName","baseClasses","wrapperClassName","mergedClassName","jsx","cloneElement","STORAGE_KEY","LEGACY_STORAGE_KEY","STORAGE_VERSION","isRecord","toNumberRecord","result","key","maybeNumber","parseStoredCounts","readStoredCountsFromKey","raw","writeStoredCounts","counts","payload","getStoredCounts","rawNew","legacyCounts","getStoredCount","setStoredCount","count","usePersistedCount","storageKey","defaultCount","currentCount","minCount","maxCount","setCount","clampCount","hasWarnedRef","stored","next","prev","newCount","min","max","SmartSkeletonList","items","renderItem","renderSkeleton","keyExtractor","_","skeletonCount","skeletons","itemSeed","Fragment","item"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext } from \"react\";\n\nexport const SkeletonContext = createContext(false);\n\nexport function useIsSkeletonMode(): boolean {\n return useContext(SkeletonContext);\n}\n","export function isDevEnv(): boolean {\n const maybeGlobal = globalThis as unknown as Record<string, unknown>;\n\n // Manual override for environments where NODE_ENV isn't injected.\n // Example: `globalThis.__REACT_LOADED_DEV__ = true`.\n const override = maybeGlobal.__REACT_LOADED_DEV__;\n if (typeof override === \"boolean\") return override;\n\n // Common global used by some toolchains/runtimes.\n const devFlag = maybeGlobal.__DEV__;\n if (typeof devFlag === \"boolean\") return devFlag;\n\n const maybeProcess = (globalThis as unknown as { process?: unknown }).process;\n const nodeEnv =\n typeof maybeProcess === \"object\" && maybeProcess !== null\n ? (maybeProcess as { env?: { NODE_ENV?: unknown } }).env?.NODE_ENV\n : undefined;\n\n if (typeof nodeEnv === \"string\") {\n return nodeEnv !== \"production\";\n }\n\n // No environment detected — assume production (convention: opt-in to dev mode).\n return false;\n}\n","import { useEffect, useLayoutEffect } from \"react\";\n\nconst canUseDOM =\n typeof globalThis !== \"undefined\" &&\n typeof (globalThis as { document?: unknown }).document !== \"undefined\";\n\nexport const useIsomorphicLayoutEffect = canUseDOM\n ? useLayoutEffect\n : useEffect;\n","import {\n cloneElement,\n type ReactElement,\n type Ref,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { isDevEnv } from \"../../utils/isDevEnv\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\nimport { SkeletonContext } from \"../SkeletonContext/SkeletonContext\";\nimport \"./SmartSkeleton.css\";\n\n// Text width configuration (in ch units)\nconst TEXT_WIDTH_MIN_CH = 6;\nconst TEXT_WIDTH_MAX_CH = 40;\n\nfunction isElement(value: unknown): value is Element {\n if (!value || typeof value !== \"object\") return false;\n const maybeElement = value as Element;\n // Must have nodeType 1 (Element) and a working querySelectorAll\n if (maybeElement.nodeType !== 1) return false;\n if (typeof maybeElement.tagName !== \"string\") return false;\n if (typeof maybeElement.querySelectorAll !== \"function\") return false;\n // Additional check: instanceof Element if available\n if (typeof Element !== \"undefined\" && !(value instanceof Element)) {\n return false;\n }\n return true;\n}\n\nconst warnedComponents = new Set<string>();\n\nfunction isUsableElement(value: unknown): value is Element {\n if (!isElement(value)) return false;\n // Test that querySelectorAll actually works\n try {\n (value as Element).querySelectorAll(\"*\");\n return true;\n } catch {\n return false;\n }\n}\n\nfunction resolveRefTarget(node: unknown): Element | null {\n if (isUsableElement(node)) return node;\n if (node && typeof node === \"object\" && \"nativeElement\" in node) {\n const nativeElement = (node as { nativeElement?: unknown }).nativeElement;\n if (isUsableElement(nativeElement)) return nativeElement;\n }\n return null;\n}\n\nfunction getElementDisplayName(element: ReactElement): string {\n const type = element.type;\n if (typeof type === \"string\") {\n return `<${type}>`;\n }\n if (typeof type === \"function\") {\n const fn = type as { displayName?: string; name?: string };\n return `<${fn.displayName || fn.name || \"Unknown\"}>`;\n }\n if (typeof type === \"object\" && type !== null) {\n const obj = type as { displayName?: string; name?: string };\n return `<${obj.displayName || obj.name || \"Unknown\"}>`;\n }\n return \"<Unknown>\";\n}\n\n/**\n * Get the original ref from the element, supporting both React 18 and React 19.\n * React 19: ref is a regular prop on element.props.ref\n * React 18: ref is on element.ref\n */\nfunction getOriginalRef(element: ReactElement): Ref<unknown> | undefined {\n // React 19 style (ref as prop)\n const propsRef = (element.props as { ref?: Ref<unknown> })?.ref;\n if (propsRef) return propsRef;\n\n // React 18 style\n const legacyRef = (element as ReactElement & { ref?: Ref<unknown> }).ref;\n if (legacyRef) return legacyRef;\n\n return undefined;\n}\n\n/**\n * Forward a ref value to the original ref (callback or object style).\n */\nfunction forwardRef(originalRef: Ref<unknown> | undefined, node: unknown) {\n if (!originalRef) return;\n if (typeof originalRef === \"function\") {\n originalRef(node);\n } else {\n (originalRef as React.MutableRefObject<unknown>).current = node;\n }\n}\n\nconst MEDIA_ELEMENTS = new Set([\"IMG\", \"VIDEO\", \"CANVAS\"]);\nconst SVG_ELEMENTS = new Set([\"SVG\"]);\n\nconst INTERACTIVE_ELEMENTS = new Set([\n \"BUTTON\",\n \"INPUT\",\n \"TEXTAREA\",\n \"SELECT\",\n \"A\",\n]);\n\nconst BUTTON_LIKE_SELECTOR = \"button,input,textarea,select,a,[role='button']\";\nconst SKIPPED_TAGS = new Set([\n \"SCRIPT\",\n \"STYLE\",\n \"LINK\",\n \"META\",\n \"NOSCRIPT\",\n \"TEMPLATE\",\n]);\n\nfunction getTagName(el: Element): string {\n return el.tagName.toUpperCase();\n}\n\nfunction isButtonLikeElement(el: Element, tagName = getTagName(el)): boolean {\n if (INTERACTIVE_ELEMENTS.has(tagName)) return true;\n const role = el.getAttribute(\"role\");\n return role === \"button\";\n}\n\nfunction isButtonLikeDescendant(el: Element, tagName: string): boolean {\n const closestButton = el.closest(BUTTON_LIKE_SELECTOR);\n return Boolean(closestButton && !isButtonLikeElement(el, tagName));\n}\n\nfunction isContentElement(el: Element, tagName = getTagName(el)): boolean {\n if (MEDIA_ELEMENTS.has(tagName)) return true;\n if (SVG_ELEMENTS.has(tagName)) return true;\n if (isButtonLikeElement(el, tagName)) return true;\n\n const isLeafNode = el.childElementCount === 0;\n\n // Text elements that are leaf nodes (no child elements, only text)\n if (isLeafNode && el.textContent?.trim()) return true;\n\n return false;\n}\n\n/**\n * Calculate text skeleton width in ch units based on text content.\n * Uses a deterministic jitter: widthCh = clamp(6, 40, len + 2 + jitter)\n */\nfunction calculateTextWidthCh(text: string, seedKey: string): number {\n const textLength = text.length;\n const jitterRange = Math.max(4, 0.8 * textLength);\n const jitter = deterministicJitter(seedKey) * jitterRange;\n const width = textLength + 2 + jitter;\n return Math.max(TEXT_WIDTH_MIN_CH, Math.min(TEXT_WIDTH_MAX_CH, width));\n}\n\nfunction deterministicJitter(seedKey: string): number {\n if (!seedKey) return 0;\n let hash = 2166136261;\n for (let index = 0; index < seedKey.length; index += 1) {\n hash ^= seedKey.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n const normalized = (hash >>> 0) / 0xffffffff;\n return normalized * 2 - 1;\n}\n\nfunction resolveTextAlign(el: HTMLElement): \"left\" | \"center\" | \"right\" {\n const align = globalThis.getComputedStyle(el).textAlign;\n if (align === \"center\") return \"center\";\n if (align === \"right\" || align === \"end\") return \"right\";\n return \"left\";\n}\n\nexport function applySkeletonClasses(\n rootElement: Element,\n options: { animate?: boolean; seed?: string | number } = {},\n): void {\n const { animate = true, seed } = options;\n const baseSeed =\n seed === undefined || seed === null ? \"loaded\" : String(seed);\n\n if (!isElement(rootElement)) {\n return;\n }\n\n const htmlRoot = rootElement as HTMLElement;\n\n // Apply skeleton mode to the root element\n htmlRoot.classList.add(\"loaded-skeleton-mode\");\n\n if (animate) {\n htmlRoot.classList.add(\"loaded-animate\");\n }\n\n // Apply background class for standalone usage (when not used via SmartSkeleton JSX)\n // If element has loaded-skeleton-wrapper, CSS handles bg via > :first-child rule\n // If element already has loaded-skeleton-bg (from JSX), this is a no-op\n const isWrapper = htmlRoot.classList.contains(\"loaded-skeleton-wrapper\");\n if (!isWrapper) {\n htmlRoot.classList.add(\"loaded-skeleton-bg\");\n }\n\n // Only add specific classes where needed (text, media, content)\n const descendants = rootElement.getElementsByTagName(\"*\");\n\n let textIndex = 0;\n\n const processElement = (el: Element) => {\n const tagName = getTagName(el);\n if (SKIPPED_TAGS.has(tagName)) return;\n if (!isContentElement(el, tagName)) return;\n if (isButtonLikeDescendant(el, tagName)) return;\n\n const htmlEl = el as HTMLElement;\n const textContent = el.textContent?.trim();\n const isLeafWithText = el.childElementCount === 0 && textContent;\n\n if (\n isLeafWithText &&\n !MEDIA_ELEMENTS.has(tagName) &&\n !SVG_ELEMENTS.has(tagName) &&\n !isButtonLikeElement(el, tagName)\n ) {\n // Text elements: overlay bar with ch-based width\n htmlEl.classList.add(\"loaded-text-skeleton\");\n htmlEl.dataset.skeletonAlign = resolveTextAlign(htmlEl);\n const seedKey = `${baseSeed}|${textIndex}|${textContent ?? \"\"}`;\n textIndex += 1;\n const widthCh = calculateTextWidthCh(textContent ?? \"\", seedKey);\n htmlEl.style.setProperty(\"--skeleton-text-width\", `${widthCh}ch`);\n } else if (MEDIA_ELEMENTS.has(tagName)) {\n // Media elements\n htmlEl.classList.add(\"loaded-skeleton-media\");\n } else if (SVG_ELEMENTS.has(tagName)) {\n // SVG elements rendered as rounded content blocks\n htmlEl.classList.add(\"loaded-skeleton-content\");\n htmlEl.classList.add(\"loaded-skeleton-svg\");\n } else {\n // Interactive elements (buttons, inputs, etc.)\n htmlEl.classList.add(\"loaded-skeleton-content\");\n // Prevent keyboard focus / interaction while in skeleton mode.\n // aria-hidden does not remove elements from the tab order.\n htmlEl.setAttribute(\"tabindex\", \"-1\");\n }\n };\n\n processElement(rootElement);\n for (const el of descendants) {\n processElement(el);\n }\n}\n\nexport interface SmartSkeletonProps {\n /** The skeleton element with mock data, rendered when loading */\n element: ReactElement;\n /** The real content to render when not loading. If omitted, returns null when loading=false. */\n children?: ReactElement;\n /** Whether the skeleton is currently loading. Default: false */\n loading?: boolean;\n /** Enable shimmer animation. Default: true */\n animate?: boolean;\n /** Additional CSS class name */\n className?: string;\n /** Optional seed to stabilize skeleton text widths */\n seed?: string | number;\n /** Suppress warning when auto-wrapper is applied. Default: false */\n suppressRefWarning?: boolean;\n}\n\nexport function SmartSkeleton({\n element,\n children,\n loading = false,\n animate = true,\n className = \"\",\n seed,\n suppressRefWarning = false,\n}: SmartSkeletonProps): ReactElement | null {\n const hasAppliedRef = useRef(false);\n const refWasCalledRef = useRef(false);\n const lastElementRef = useRef<ReactElement | null>(null);\n const previousElementTypeRef = useRef<ReactElement[\"type\"] | null>(null);\n const previousElementKeyRef = useRef<ReactElement[\"key\"] | null>(null);\n const [needsWrapper, setNeedsWrapper] = useState(false);\n\n // Reset flags when loading changes or element changes\n if (!loading || lastElementRef.current !== element) {\n hasAppliedRef.current = false;\n refWasCalledRef.current = false;\n lastElementRef.current = element;\n }\n\n useEffect(() => {\n const elementType = element.type;\n const elementKey = element.key ?? null;\n const previousType = previousElementTypeRef.current;\n const previousKey = previousElementKeyRef.current;\n\n if (\n previousType !== null &&\n (previousType !== elementType || previousKey !== elementKey)\n ) {\n setNeedsWrapper(false);\n }\n\n previousElementTypeRef.current = elementType;\n previousElementKeyRef.current = elementKey;\n }, [element.type, element.key]);\n\n const originalRef = getOriginalRef(element);\n\n const refCallback = useCallback(\n (node: unknown) => {\n refWasCalledRef.current = true;\n\n const target = resolveRefTarget(node);\n\n // If we received a node but couldn't get a DOM element from it,\n // we need to use a wrapper\n if (node !== null && !target && !needsWrapper) {\n setNeedsWrapper(true);\n\n // Emit warning (deduplicated by component name)\n if (!suppressRefWarning && isDevEnv()) {\n const displayName = getElementDisplayName(element);\n if (!warnedComponents.has(displayName)) {\n console.warn(\n `[SmartSkeleton] ${displayName} does not forward its ref to a DOM element. ` +\n `A wrapper <div> has been added automatically. Use forwardRef to avoid this.`,\n );\n warnedComponents.add(displayName);\n }\n }\n return;\n }\n\n if (target && loading && !hasAppliedRef.current) {\n applySkeletonClasses(target, { animate, seed });\n hasAppliedRef.current = true;\n }\n\n // Forward ref to original element\n forwardRef(originalRef, node);\n },\n [\n loading,\n element,\n needsWrapper,\n suppressRefWarning,\n originalRef,\n animate,\n seed,\n ],\n );\n\n // Detect if ref was never called (component ignores ref entirely)\n // useLayoutEffect runs synchronously after DOM mutations but before paint\n useIsomorphicLayoutEffect(() => {\n if (!loading || needsWrapper) return;\n\n // At this point, React has already attempted to attach refs\n // If refWasCalledRef is still false, the component ignored the ref\n if (!refWasCalledRef.current) {\n setNeedsWrapper(true);\n\n // Emit warning\n if (!suppressRefWarning && isDevEnv()) {\n const displayName = getElementDisplayName(element);\n if (!warnedComponents.has(displayName)) {\n console.warn(\n `[SmartSkeleton] ${displayName} does not accept a ref. ` +\n `A wrapper <div> has been added automatically. Use forwardRef to avoid this.`,\n );\n warnedComponents.add(displayName);\n }\n }\n }\n }, [loading, needsWrapper, element, suppressRefWarning]);\n\n // Not loading: return children or null\n if (!loading) {\n return children ?? null;\n }\n\n // Build merged className for skeleton mode\n const elementProps = element.props as { className?: string };\n const existingClassName = elementProps.className ?? \"\";\n\n // Base classes for skeleton mode\n const baseClasses = [\"loaded-skeleton-mode\", animate && \"loaded-animate\"]\n .filter(Boolean)\n .join(\" \");\n\n // When wrapping: wrapper gets mode + wrapper marker (no bg - it goes on child via ref)\n const wrapperClassName = [baseClasses, \"loaded-skeleton-wrapper\", className]\n .filter(Boolean)\n .join(\" \");\n\n // When not wrapping: element gets mode + bg directly (for SSR)\n const mergedClassName = [\n existingClassName,\n baseClasses,\n \"loaded-skeleton-bg\",\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <SkeletonContext.Provider value={true}>\n {needsWrapper ? (\n <div ref={refCallback} className={wrapperClassName} aria-hidden=\"true\">\n {element}\n </div>\n ) : (\n cloneElement(element as ReactElement<Record<string, unknown>>, {\n ref: refCallback,\n className: mergedClassName,\n \"aria-hidden\": true,\n })\n )}\n </SkeletonContext.Provider>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { isDevEnv } from \"../../utils/isDevEnv\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\n\nconst STORAGE_KEY = \"react-loaded\";\nconst LEGACY_STORAGE_KEY = \"loaded\";\nconst STORAGE_VERSION = 1 as const;\n\ntype StoredCounts = Record<string, number>;\ntype StoredPayloadV1 = { v: typeof STORAGE_VERSION; counts: StoredCounts };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction toNumberRecord(value: unknown): StoredCounts {\n if (!isRecord(value)) return {};\n const result: StoredCounts = {};\n for (const [key, maybeNumber] of Object.entries(value)) {\n if (typeof maybeNumber === \"number\") {\n result[key] = maybeNumber;\n }\n }\n return result;\n}\n\nfunction parseStoredCounts(value: unknown): StoredCounts {\n // Current schema: { v: 1, counts: Record<string, number> }\n if (isRecord(value) && value.v === STORAGE_VERSION) {\n return toNumberRecord(value.counts);\n }\n\n // Legacy schema: Record<string, number>\n return toNumberRecord(value);\n}\n\nfunction readStoredCountsFromKey(key: string): StoredCounts {\n if (typeof localStorage === \"undefined\") return {};\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return {};\n return parseStoredCounts(JSON.parse(raw));\n } catch {\n return {};\n }\n}\n\nfunction writeStoredCounts(counts: StoredCounts): void {\n if (typeof localStorage === \"undefined\") return;\n const payload: StoredPayloadV1 = { v: STORAGE_VERSION, counts };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));\n } catch {\n // Silently fail if localStorage is full or unavailable\n }\n}\n\nfunction getStoredCounts(): Record<string, number> {\n if (typeof localStorage === \"undefined\") return {};\n\n try {\n const rawNew = localStorage.getItem(STORAGE_KEY);\n if (rawNew !== null) {\n return parseStoredCounts(JSON.parse(rawNew));\n }\n\n // Backward compatibility: migrate legacy key once if present.\n const legacyCounts = readStoredCountsFromKey(LEGACY_STORAGE_KEY);\n if (Object.keys(legacyCounts).length > 0) {\n writeStoredCounts(legacyCounts);\n }\n return legacyCounts;\n } catch {\n return {};\n }\n}\n\nfunction getStoredCount(key: string): number | null {\n const counts = getStoredCounts();\n const value = counts[key];\n return typeof value === \"number\" ? value : null;\n}\n\nfunction setStoredCount(key: string, count: number): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n const counts = getStoredCounts();\n counts[key] = count;\n writeStoredCounts(counts);\n } catch {\n // Silently fail if localStorage is full or unavailable\n }\n}\n\nexport interface UsePersistedCountOptions {\n storageKey?: string;\n defaultCount?: number;\n currentCount?: number;\n loading: boolean;\n minCount?: number;\n maxCount?: number;\n}\n\nexport function usePersistedCount({\n storageKey,\n defaultCount = 3,\n currentCount,\n loading,\n minCount = 1,\n maxCount,\n}: UsePersistedCountOptions): number {\n // Always start from the default to match SSR output, then (on the client)\n // sync to the persisted value in a layout effect before first paint.\n const [count, setCount] = useState<number>(() =>\n clampCount(defaultCount, minCount, maxCount),\n );\n\n const hasWarnedRef = useRef(false);\n\n useIsomorphicLayoutEffect(() => {\n if (!storageKey) return;\n const stored = getStoredCount(storageKey);\n if (stored === null) return;\n const next = clampCount(stored, minCount, maxCount);\n setCount((prev) => (Object.is(prev, next) ? prev : next));\n }, [storageKey, minCount, maxCount]);\n\n useEffect(() => {\n if (!loading && currentCount !== undefined) {\n const newCount = clampCount(currentCount, minCount, maxCount);\n setCount(newCount);\n\n if (storageKey) {\n setStoredCount(storageKey, newCount);\n }\n }\n }, [loading, currentCount, storageKey, minCount, maxCount]);\n\n useEffect(() => {\n if (isDevEnv() && !storageKey && !hasWarnedRef.current) {\n console.warn(\n \"[Loaded] SmartSkeletonList used without storageKey. \" +\n \"The count will reset on remount. Add a storageKey to persist across sessions.\",\n );\n hasWarnedRef.current = true;\n }\n }, [storageKey]);\n\n return count;\n}\n\nfunction clampCount(\n value: number,\n min: number,\n max: number | undefined,\n): number {\n let result = Math.max(value, min);\n if (max !== undefined) {\n result = Math.min(result, max);\n }\n return result;\n}\n","import { Fragment, type ReactElement } from \"react\";\nimport { usePersistedCount } from \"../../hooks/usePersistedCount/usePersistedCount\";\nimport { SmartSkeleton } from \"../SmartSkeleton/SmartSkeleton\";\n\nexport interface SmartSkeletonListProps<T> {\n /** Whether the list is currently loading. Default: false */\n loading?: boolean;\n /** The items to render. Pass undefined while loading. */\n items: T[] | undefined;\n /** Render function for each item when loaded */\n renderItem: (item: T, index: number) => ReactElement;\n /** Render function for skeleton placeholders */\n renderSkeleton: (index: number) => ReactElement;\n /** Key for localStorage persistence. Without it, count resets on remount. */\n storageKey?: string;\n /** Initial skeleton count before any data is known. Default: 3 */\n defaultCount?: number;\n /** Minimum skeletons to show. Default: 1 */\n minCount?: number;\n /** Maximum skeletons to show */\n maxCount?: number;\n /** Enable shimmer animation. Default: true */\n animate?: boolean;\n /** Optional seed to stabilize skeleton text widths */\n seed?: string | number;\n /** Suppress warning when auto-wrapper is applied. Default: false */\n suppressRefWarning?: boolean;\n /** Extract unique key for each item. Default: index */\n keyExtractor?: (item: T, index: number) => string | number;\n}\n\nexport function SmartSkeletonList<T>({\n loading = false,\n items,\n renderItem,\n renderSkeleton,\n storageKey,\n defaultCount = 3,\n minCount = 1,\n maxCount,\n animate = true,\n seed,\n suppressRefWarning = false,\n keyExtractor = (_, index) => index,\n}: SmartSkeletonListProps<T>): ReactElement | null {\n const skeletonCount = usePersistedCount({\n storageKey,\n defaultCount,\n currentCount: items?.length,\n loading,\n minCount,\n maxCount,\n });\n\n if (loading) {\n const skeletons = new Array(skeletonCount);\n for (let index = 0; index < skeletonCount; index += 1) {\n const itemSeed = seed === undefined ? `${index}` : `${seed}:${index}`;\n skeletons[index] = (\n <SmartSkeleton\n key={`skeleton-${index}`}\n loading={true}\n element={renderSkeleton(index)}\n animate={animate}\n seed={itemSeed}\n suppressRefWarning={suppressRefWarning}\n />\n );\n }\n return <>{skeletons}</>;\n }\n\n if (!items || items.length === 0) {\n return null;\n }\n\n return (\n <>\n {items.map((item, index) => (\n <Fragment key={keyExtractor(item, index)}>\n {renderItem(item, index)}\n </Fragment>\n ))}\n </>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "react-loaded",
3
+ "version": "0.1.0",
4
+ "description": "Zero-layout-shift skeleton screens for React",
5
+ "homepage": "https://github.com/Pierre-LHOSTE/react-loaded#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Pierre-LHOSTE/react-loaded.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/Pierre-LHOSTE/react-loaded/issues"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "./style.css": "./dist/index.css"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "sideEffects": [
34
+ "**/*.css"
35
+ ],
36
+ "keywords": [
37
+ "react",
38
+ "skeleton",
39
+ "loading",
40
+ "placeholder",
41
+ "layout-shift",
42
+ "cls",
43
+ "ux"
44
+ ],
45
+ "author": "vingt-douze <vingt-douze@protonmail.com> (https://vingt-douze.com)",
46
+ "license": "MIT",
47
+ "peerDependencies": {
48
+ "react": "^18.0.0 || ^19.0.0",
49
+ "react-dom": "^18.0.0 || ^19.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@ant-design/icons": "^6.1.0",
53
+ "@biomejs/biome": "^2.3.13",
54
+ "@chromatic-com/storybook": "^5.0.0",
55
+ "@storybook/addon-a11y": "^10.2.1",
56
+ "@storybook/addon-docs": "^10.2.1",
57
+ "@storybook/addon-onboarding": "^10.2.1",
58
+ "@storybook/addon-vitest": "^10.2.1",
59
+ "@storybook/react": "^10.2.1",
60
+ "@storybook/react-vite": "^10.2.1",
61
+ "@testing-library/jest-dom": "^6.9.0",
62
+ "@testing-library/react": "^16.3.0",
63
+ "@testing-library/user-event": "^14.6.1",
64
+ "@types/node": "^25.2.0",
65
+ "@types/react": "^19.0.0",
66
+ "@types/react-dom": "^19.0.0",
67
+ "@vitest/browser-playwright": "^4.0.18",
68
+ "@vitest/coverage-v8": "^4.0.18",
69
+ "antd": "^6.2.2",
70
+ "jsdom": "^26.1.0",
71
+ "playwright": "^1.58.0",
72
+ "react": "^19.0.0",
73
+ "react-dom": "^19.0.0",
74
+ "storybook": "^10.2.1",
75
+ "tsup": "^8.0.0",
76
+ "typescript": "^5.7.0",
77
+ "vitest": "^4.0.18"
78
+ },
79
+ "scripts": {
80
+ "build": "tsup",
81
+ "dev": "tsup --watch",
82
+ "pack": "pnpm pack --pack-destination .artifacts",
83
+ "typecheck": "tsc --noEmit",
84
+ "lint": "biome lint src",
85
+ "format": "biome format --write src",
86
+ "format:check": "biome format src",
87
+ "check": "biome check src",
88
+ "test": "vitest run --project unit",
89
+ "test:watch": "vitest --project unit --watch",
90
+ "test:coverage": "vitest run --project unit --coverage",
91
+ "bench": "vitest bench --config vitest.bench.config.ts",
92
+ "bench:baseline": "vitest bench --config vitest.bench.config.ts --outputJson .benchmarks/baseline.json",
93
+ "bench:compare": "vitest bench --config vitest.bench.config.ts --compare .benchmarks/baseline.json",
94
+ "storybook": "storybook dev -p 6006",
95
+ "build-storybook": "storybook build"
96
+ }
97
+ }