smart-skeleton-loader 1.0.0 → 2.1.1

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/SkeletonPrimitive.tsx","../src/AutoSkeleton.tsx"],"names":["jsx","motion","useRef","useState","node","useEffect","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import React from \"react\";\nimport { motion, HTMLMotionProps } from \"framer-motion\";\n\nexport interface SkeletonPrimitiveProps extends HTMLMotionProps<\"div\"> {\n width?: string | number;\n height?: string | number;\n borderRadius?: string | number;\n color?: string;\n highlightColor?: string;\n}\n\nexport const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps> = ({\n width,\n height,\n borderRadius = \"4px\",\n color = \"var(--auto-skeleton-color, #eee)\",\n highlightColor = \"var(--auto-skeleton-highlight, #f5f5f5)\",\n style,\n ...props\n}) => {\n return (\n <motion.div\n style={{\n width,\n height,\n borderRadius,\n backgroundColor: color,\n ...style,\n }}\n animate={{\n backgroundColor: [color, highlightColor, color],\n }}\n transition={{\n duration: 1.5,\n repeat: Infinity,\n ease: \"easeInOut\",\n }}\n data-testid=\"skeleton\"\n {...props}\n />\n );\n};\n","import React, { useRef, useEffect, useState, ReactNode } from \"react\";\nimport { SkeletonPrimitive } from \"./SkeletonPrimitive\";\n\nexport interface AutoSkeletonProps {\n loading: boolean;\n children: ReactNode;\n config?: {\n color?: string;\n highlightColor?: string;\n borderRadius?: string | number;\n theme?: \"light\" | \"dark\" | \"auto\";\n // Theme-specific colors (override CSS variables)\n lightColor?: string;\n lightHighlightColor?: string;\n darkColor?: string;\n darkHighlightColor?: string;\n };\n}\n\nexport const AutoSkeleton: React.FC<AutoSkeletonProps> = ({\n loading,\n children,\n config,\n}) => {\n const theme = config?.theme || \"auto\";\n const containerRef = useRef<HTMLDivElement>(null);\n const [skeletons, setSkeletons] = useState<\n Array<{\n key: string;\n type: \"primitive\" | \"container\";\n width: number;\n height: number;\n top: number;\n left: number;\n borderRadius: string;\n style?: React.CSSProperties; // For container styles\n }>\n >([]);\n\n const updateSkeletons = () => {\n if (!containerRef.current) return;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const newSkeletons: typeof skeletons = [];\n\n // Helper to categorize node\n const getSkeletonType = (\n el: Element,\n ): {\n type: \"primitive\" | \"container\";\n styles?: React.CSSProperties;\n } | null => {\n const tagName = el.tagName.toLowerCase();\n const style = window.getComputedStyle(el);\n\n // 1. Atomic Content (always pulse)\n if ([\"img\", \"input\", \"button\", \"textarea\", \"select\"].includes(tagName)) {\n return { type: \"primitive\" };\n }\n\n // 2. Text Content (always pulse)\n const hasText = Array.from(el.childNodes).some(\n (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim(),\n );\n if (hasText) return { type: \"primitive\" };\n\n // 3. Structural Container (static clone)\n // If it has visible styles but no direct text => it's a container\n const hasBackground =\n style.backgroundColor !== \"rgba(0, 0, 0, 0)\" &&\n style.backgroundColor !== \"transparent\";\n const hasBorder =\n style.borderWidth !== \"0px\" &&\n style.borderStyle !== \"none\" &&\n style.borderColor !== \"rgba(0, 0, 0, 0)\" &&\n style.borderColor !== \"transparent\";\n const hasShadow = style.boxShadow !== \"none\";\n\n if (hasBackground || hasBorder || hasShadow) {\n return {\n type: \"container\",\n styles: {\n backgroundColor: style.backgroundColor,\n border: style.border,\n borderRadius: style.borderRadius,\n boxShadow: style.boxShadow,\n },\n };\n }\n\n return null;\n };\n\n // Traverse\n const walker = document.createTreeWalker(\n containerRef.current,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (node) => {\n const el = node as Element;\n const style = window.getComputedStyle(el);\n if (style.display === \"none\") return NodeFilter.FILTER_REJECT;\n\n // If it matches a type, accept it.\n // Note: We want to accept containers AND traverse their children.\n // FILTER_ACCEPT does both.\n if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;\n\n return NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n let node = walker.nextNode();\n let idx = 0;\n while (node) {\n const el = node as Element;\n const rect = el.getBoundingClientRect();\n const typeData = getSkeletonType(el);\n\n if (typeData) {\n // Determine border radius logic\n let borderRadius =\n config?.borderRadius ||\n typeData.styles?.borderRadius ||\n window.getComputedStyle(el).borderRadius;\n\n if (typeData.type === \"primitive\") {\n // Auto-detect circle for rounded primitives\n if (borderRadius === \"0px\" || !borderRadius) {\n if (\n window.getComputedStyle(el).borderRadius === \"50%\" ||\n (rect.width === rect.height && rect.width < 50)\n ) {\n // Keep heuristic or default\n }\n borderRadius = \"4px\";\n if (el.tagName.toLowerCase() === \"button\") borderRadius = \"6px\";\n if (el.tagName.toLowerCase() === \"img\") borderRadius = \"4px\";\n }\n }\n\n newSkeletons.push({\n key: `${idx++}`,\n type: typeData.type,\n width: rect.width,\n height: rect.height,\n top: rect.top - containerRect.top,\n left: rect.left - containerRect.left,\n borderRadius: borderRadius as string,\n style: typeData.styles,\n });\n }\n\n node = walker.nextNode();\n }\n\n // If no content nodes found, but container has size (maybe a canvas or simple div w/ background),\n // Fallback? Or maybe user wants the single block behavior.\n // Let's add that fallback if newSkeletons is empty but container has dimension.\n if (\n newSkeletons.length === 0 &&\n (containerRect.width > 0 || containerRect.height > 0)\n ) {\n newSkeletons.push({\n key: \"fallback\",\n type: \"primitive\",\n width: containerRect.width,\n height: containerRect.height,\n top: 0,\n left: 0,\n borderRadius: (config?.borderRadius as string) || \"4px\",\n });\n }\n\n setSkeletons(newSkeletons);\n };\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n // Initial measure\n updateSkeletons();\n\n const observer = new ResizeObserver(() => {\n updateSkeletons();\n });\n\n observer.observe(containerRef.current);\n // Also observe children mutations if possible, but ResizeObserver on container catches layout changes often.\n // For robust child changes:\n const mutationObserver = new MutationObserver(() => updateSkeletons());\n mutationObserver.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n mutationObserver.disconnect();\n };\n }, [loading, children]); // Re-scan when children change\n\n // Determine effective colors based on theme\n const getEffectiveColors = () => {\n // If user passed explicit color/highlightColor, use those\n if (config?.color || config?.highlightColor) {\n return {\n color: config.color,\n highlightColor: config.highlightColor,\n };\n }\n // If theme-specific colors are provided, use them\n if (theme === \"dark\" && (config?.darkColor || config?.darkHighlightColor)) {\n return {\n color: config.darkColor,\n highlightColor: config.darkHighlightColor,\n };\n }\n if (\n theme === \"light\" &&\n (config?.lightColor || config?.lightHighlightColor)\n ) {\n return {\n color: config.lightColor,\n highlightColor: config.lightHighlightColor,\n };\n }\n // Otherwise, let CSS variables handle it (undefined = use CSS var defaults)\n return { color: undefined, highlightColor: undefined };\n };\n\n const effectiveColors = getEffectiveColors();\n\n return (\n <div\n style={{ position: \"relative\", display: \"inline-block\", width: \"auto\" }}\n {...(theme !== \"auto\" ? { \"data-skeleton-theme\": theme } : {})}\n >\n <div\n ref={containerRef}\n style={{\n opacity: loading ? 0 : 1,\n transition: \"opacity 0.2s ease\", // Smooth transition when loading finishes\n }}\n aria-hidden={loading}\n >\n {children}\n </div>\n\n {loading && (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n }}\n >\n {skeletons.map((sk) => (\n <div\n key={sk.key}\n style={{ position: \"absolute\", top: sk.top, left: sk.left }}\n >\n {sk.type === \"primitive\" ? (\n <SkeletonPrimitive\n width={sk.width}\n height={sk.height}\n borderRadius={sk.borderRadius}\n color={effectiveColors.color}\n highlightColor={effectiveColors.highlightColor}\n />\n ) : (\n /* Container Clone: Renders static style */\n <div\n style={{\n width: sk.width,\n height: sk.height,\n borderRadius: sk.borderRadius,\n ...sk.style, // Apply captured container styles\n // Ensure it doesn't hide children if z-index issues arise,\n // but since we render in DOM order, children come later and sit on top.\n boxSizing: \"border-box\",\n }}\n />\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/AutoSkeleton.tsx","../src/measure.ts","../src/SkeletonPrimitive.tsx","../src/useDisplayedLoading.ts"],"sourcesContent":["export * from './AutoSkeleton';\nexport * from './SkeletonPrimitive';\n","import React, {\n ReactNode,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { getTextLineBoxes } from './measure';\nimport { SkeletonPrimitive } from './SkeletonPrimitive';\nimport { useDisplayedLoading } from './useDisplayedLoading';\n\n/** Text bars are drawn at this fraction of the line box height, vertically centered. */\nconst TEXT_BAR_RATIO = 0.7;\n\n/** Crossfade duration (ms) between the skeleton and the real content. */\nconst FADE_MS = 200;\n\nexport interface AutoSkeletonProps {\n loading: boolean;\n children: ReactNode;\n /** Text announced to screen readers while loading. */\n loadingLabel?: string;\n /** Class name for the outer wrapper. */\n className?: string;\n /** Style for the outer wrapper; merged over the defaults (`position: relative; display: block`). */\n style?: React.CSSProperties;\n /** Wait this long (ms) before showing the skeleton, so fast loads never flash it. */\n delay?: number;\n /** Once shown, keep the skeleton at least this long (ms). */\n minDuration?: number;\n config?: {\n color?: string;\n highlightColor?: string;\n borderRadius?: string | number;\n theme?: 'light' | 'dark' | 'auto';\n // Theme-specific colors (override CSS variables)\n lightColor?: string;\n lightHighlightColor?: string;\n darkColor?: string;\n darkHighlightColor?: string;\n };\n}\n\nconst VISUALLY_HIDDEN: React.CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\nexport const AutoSkeleton: React.FC<AutoSkeletonProps> = ({\n loading,\n children,\n loadingLabel = 'Loading…',\n className,\n style,\n delay = 0,\n minDuration = 0,\n config,\n}) => {\n const theme = config?.theme || 'auto';\n const showSkeleton = useDisplayedLoading(loading, { delay, minDuration });\n // Content stays hidden while data is missing or the skeleton is still held on screen\n const busy = loading || showSkeleton;\n\n // Once real content has been shown, a refetch keeps it on screen during `delay`\n // (so fast refetches change nothing visually). Before that, there is only\n // placeholder content, which must stay hidden.\n const [revealed, setRevealed] = useState(!busy);\n if (!busy && !revealed) setRevealed(true);\n const hidden = showSkeleton || (loading && !revealed);\n\n // Keep the overlay mounted while it fades out (set-state-during-render avoids a blank frame)\n const [fadingOut, setFadingOut] = useState(false);\n const [prevShowSkeleton, setPrevShowSkeleton] = useState(showSkeleton);\n if (prevShowSkeleton !== showSkeleton) {\n setPrevShowSkeleton(showSkeleton);\n setFadingOut(!showSkeleton);\n }\n useEffect(() => {\n if (!fadingOut) return;\n const timer = setTimeout(() => setFadingOut(false), FADE_MS);\n return () => clearTimeout(timer);\n }, [fadingOut]);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const [skeletons, setSkeletons] = useState<\n Array<{\n key: string;\n type: 'primitive' | 'container';\n width: number;\n height: number;\n top: number;\n left: number;\n borderRadius: string;\n style?: React.CSSProperties; // For container styles\n }>\n >([]);\n // False until the first client-side measurement (always false on the server)\n const [measured, setMeasured] = useState(false);\n\n const updateSkeletons = () => {\n if (!containerRef.current) return;\n\n const containerRect = containerRef.current.getBoundingClientRect();\n const newSkeletons: typeof skeletons = [];\n\n // Helper to categorize node\n const getSkeletonType = (\n el: Element,\n ): {\n type: 'primitive' | 'container';\n text?: boolean;\n styles?: React.CSSProperties;\n } | null => {\n const tagName = el.tagName.toLowerCase();\n const style = window.getComputedStyle(el);\n\n // 1. Atomic Content (always pulse)\n if (['img', 'input', 'button', 'textarea', 'select'].includes(tagName)) {\n return { type: 'primitive' };\n }\n\n // 2. Text Content (always pulse)\n const hasText = Array.from(el.childNodes).some(\n (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim(),\n );\n if (hasText) return { type: 'primitive', text: true };\n\n // 3. Structural Container (static clone)\n // If it has visible styles but no direct text => it's a container\n const hasBackground =\n style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&\n style.backgroundColor !== 'transparent';\n const hasBorder =\n style.borderWidth !== '0px' &&\n style.borderStyle !== 'none' &&\n style.borderColor !== 'rgba(0, 0, 0, 0)' &&\n style.borderColor !== 'transparent';\n const hasShadow = style.boxShadow !== 'none';\n\n if (hasBackground || hasBorder || hasShadow) {\n return {\n type: 'container',\n styles: {\n backgroundColor: style.backgroundColor,\n border: style.border,\n borderRadius: style.borderRadius,\n boxShadow: style.boxShadow,\n },\n };\n }\n\n return null;\n };\n\n // Traverse\n const walker = document.createTreeWalker(\n containerRef.current,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (node) => {\n const el = node as Element;\n const style = window.getComputedStyle(el);\n if (style.display === 'none') return NodeFilter.FILTER_REJECT;\n\n // If it matches a type, accept it.\n // Note: We want to accept containers AND traverse their children.\n // FILTER_ACCEPT does both.\n if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;\n\n return NodeFilter.FILTER_SKIP;\n },\n },\n );\n\n let node = walker.nextNode();\n let idx = 0;\n while (node) {\n const el = node as Element;\n const rect = el.getBoundingClientRect();\n const typeData = getSkeletonType(el);\n\n // Text: one bar per rendered line instead of one block per element\n const lines = typeData?.text ? getTextLineBoxes(el) : [];\n if (lines.length > 0) {\n for (const line of lines) {\n const height = Math.max(4, line.height * TEXT_BAR_RATIO);\n newSkeletons.push({\n key: `${idx++}`,\n type: 'primitive',\n width: line.width,\n height,\n top: line.top + (line.height - height) / 2 - containerRect.top,\n left: line.left - containerRect.left,\n borderRadius:\n typeof config?.borderRadius === 'number'\n ? `${config.borderRadius}px`\n : (config?.borderRadius ?? '4px'),\n });\n }\n } else if (typeData) {\n // Determine border radius logic\n let borderRadius =\n config?.borderRadius ||\n typeData.styles?.borderRadius ||\n window.getComputedStyle(el).borderRadius;\n\n if (typeData.type === 'primitive') {\n // Auto-detect circle for rounded primitives\n if (borderRadius === '0px' || !borderRadius) {\n if (\n window.getComputedStyle(el).borderRadius === '50%' ||\n (rect.width === rect.height && rect.width < 50)\n ) {\n // Keep heuristic or default\n }\n borderRadius = '4px';\n if (el.tagName.toLowerCase() === 'button') borderRadius = '6px';\n if (el.tagName.toLowerCase() === 'img') borderRadius = '4px';\n }\n }\n\n newSkeletons.push({\n key: `${idx++}`,\n type: typeData.type,\n width: rect.width,\n height: rect.height,\n top: rect.top - containerRect.top,\n left: rect.left - containerRect.left,\n borderRadius: borderRadius as string,\n style: typeData.styles,\n });\n }\n\n node = walker.nextNode();\n }\n\n // If no content nodes found, but container has size (maybe a canvas or simple div w/ background),\n // Fallback? Or maybe user wants the single block behavior.\n // Let's add that fallback if newSkeletons is empty but container has dimension.\n if (\n newSkeletons.length === 0 &&\n (containerRect.width > 0 || containerRect.height > 0)\n ) {\n newSkeletons.push({\n key: 'fallback',\n type: 'primitive',\n width: containerRect.width,\n height: containerRect.height,\n top: 0,\n left: 0,\n borderRadius: (config?.borderRadius as string) || '4px',\n });\n }\n\n setSkeletons(newSkeletons);\n setMeasured(true);\n };\n\n // Layout effect: measure before paint so the overlay never shows a frame without bars\n useLayoutEffect(() => {\n if (!containerRef.current) return;\n\n // Initial measure\n updateSkeletons();\n\n const observer = new ResizeObserver(() => {\n updateSkeletons();\n });\n\n observer.observe(containerRef.current);\n // Also observe children mutations if possible, but ResizeObserver on container catches layout changes often.\n // For robust child changes:\n const mutationObserver = new MutationObserver(() => updateSkeletons());\n mutationObserver.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n mutationObserver.disconnect();\n };\n }, [showSkeleton, children]); // Re-scan when children change\n\n // Determine effective colors based on theme\n const getEffectiveColors = () => {\n // If user passed explicit color/highlightColor, use those\n if (config?.color || config?.highlightColor) {\n return {\n color: config.color,\n highlightColor: config.highlightColor,\n };\n }\n // If theme-specific colors are provided, use them\n if (theme === 'dark' && (config?.darkColor || config?.darkHighlightColor)) {\n return {\n color: config.darkColor,\n highlightColor: config.darkHighlightColor,\n };\n }\n if (\n theme === 'light' &&\n (config?.lightColor || config?.lightHighlightColor)\n ) {\n return {\n color: config.lightColor,\n highlightColor: config.lightHighlightColor,\n };\n }\n // Otherwise, let CSS variables handle it (undefined = use CSS var defaults)\n return { color: undefined, highlightColor: undefined };\n };\n\n const effectiveColors = getEffectiveColors();\n\n return (\n <div\n className={className}\n style={{ position: 'relative', display: 'block', ...style }}\n {...(theme !== 'auto' ? { 'data-skeleton-theme': theme } : {})}\n aria-busy={busy}\n >\n {/* <output> is not reliably announced as a live region; role=\"status\" is. */}\n {/* oxlint-disable-next-line jsx-a11y/prefer-tag-over-role */}\n <span role=\"status\" style={VISUALLY_HIDDEN}>\n {busy ? loadingLabel : ''}\n </span>\n <div\n ref={containerRef}\n style={{\n opacity: hidden ? 0 : 1,\n // Hide instantly (no ghost content under the bars); fade only on reveal\n transition: hidden ? 'none' : `opacity ${FADE_MS}ms ease`,\n }}\n aria-hidden={hidden}\n inert={busy}\n >\n {children}\n </div>\n\n {(showSkeleton || fadingOut) && (\n <div\n data-testid=\"skeleton-overlay\"\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n opacity: showSkeleton ? 1 : 0,\n transition: `opacity ${FADE_MS}ms ease`,\n }}\n >\n {!measured && (\n // Server render / before hydration: nothing can be measured yet, so\n // cover the reserved space with one block instead of showing nothing\n <SkeletonPrimitive\n width=\"100%\"\n height=\"100%\"\n color={effectiveColors.color}\n highlightColor={effectiveColors.highlightColor}\n />\n )}\n {skeletons.map((sk) => (\n <div\n key={sk.key}\n style={{ position: 'absolute', top: sk.top, left: sk.left }}\n >\n {sk.type === 'primitive' ? (\n <SkeletonPrimitive\n width={sk.width}\n height={sk.height}\n borderRadius={sk.borderRadius}\n color={effectiveColors.color}\n highlightColor={effectiveColors.highlightColor}\n />\n ) : (\n /* Container Clone: Renders static style */\n <div\n style={{\n width: sk.width,\n height: sk.height,\n borderRadius: sk.borderRadius,\n ...sk.style, // Apply captured container styles\n // Ensure it doesn't hide children if z-index issues arise,\n // but since we render in DOM order, children come later and sit on top.\n boxSizing: 'border-box',\n }}\n />\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n};\n","export interface Box {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/**\n * Merges rects that sit on the same visual line (tops within `tolerance` px)\n * into one box per line, ordered top to bottom.\n */\nexport function groupRectsIntoLines(\n rects: ArrayLike<Box>,\n tolerance = 2,\n): Box[] {\n const sorted = Array.from(rects)\n .filter((r) => r.width > 0 && r.height > 0)\n .sort((a, b) => a.top - b.top || a.left - b.left);\n\n const lines: Array<{\n top: number;\n left: number;\n right: number;\n bottom: number;\n }> = [];\n for (const r of sorted) {\n const line = lines[lines.length - 1];\n if (line && Math.abs(r.top - line.top) <= tolerance) {\n line.left = Math.min(line.left, r.left);\n line.right = Math.max(line.right, r.left + r.width);\n line.bottom = Math.max(line.bottom, r.top + r.height);\n } else {\n lines.push({\n top: r.top,\n left: r.left,\n right: r.left + r.width,\n bottom: r.top + r.height,\n });\n }\n }\n\n return lines.map((l) => ({\n top: l.top,\n left: l.left,\n width: l.right - l.left,\n height: l.bottom - l.top,\n }));\n}\n\n/**\n * Line boxes of an element's own (direct) text nodes, in viewport coordinates.\n * Returns [] when the environment can't measure text ranges (e.g. jsdom).\n */\nexport function getTextLineBoxes(el: Element): Box[] {\n if (typeof document.createRange !== 'function') return [];\n\n const rects: Box[] = [];\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) continue;\n const range = document.createRange();\n range.selectNodeContents(node);\n if (typeof range.getClientRects !== 'function') return [];\n rects.push(...Array.from(range.getClientRects()));\n }\n return groupRectsIntoLines(rects);\n}\n","import React from 'react';\n\nexport interface SkeletonPrimitiveProps extends React.HTMLAttributes<HTMLDivElement> {\n width?: string | number;\n height?: string | number;\n borderRadius?: string | number;\n color?: string;\n highlightColor?: string;\n}\n\nconst PULSE_CSS = `\n@keyframes auto-skeleton-pulse {\n 0%, 100% { background-color: var(--auto-skeleton-c); }\n 50% { background-color: var(--auto-skeleton-h); }\n}\n.auto-skeleton { animation: auto-skeleton-pulse 1.5s ease-in-out infinite; }\n@media (prefers-reduced-motion: reduce) {\n .auto-skeleton { animation: none; }\n}\n`;\n\nexport const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps> = ({\n width,\n height,\n borderRadius = '4px',\n color = 'var(--auto-skeleton-color, #eee)',\n highlightColor = 'var(--auto-skeleton-highlight, #f5f5f5)',\n className,\n style,\n ...props\n}) => {\n return (\n <>\n {/* React 19 hoists this into <head> and dedupes it by href */}\n <style href=\"auto-skeleton-pulse\" precedence=\"default\">\n {PULSE_CSS}\n </style>\n <div\n className={className ? `auto-skeleton ${className}` : 'auto-skeleton'}\n style={\n {\n width,\n height,\n borderRadius,\n backgroundColor: color,\n '--auto-skeleton-c': color,\n '--auto-skeleton-h': highlightColor,\n ...style,\n } as React.CSSProperties\n }\n data-testid=\"skeleton\"\n {...props}\n />\n </>\n );\n};\n","import { useLayoutEffect, useRef, useState } from 'react';\n\nexport interface DisplayedLoadingOptions {\n /** Wait this long (ms) before showing the skeleton; fast loads never show it. */\n delay?: number;\n /** Once shown, keep the skeleton at least this long (ms) to avoid a flash. */\n minDuration?: number;\n}\n\n/**\n * Turns a raw `loading` flag into \"should the skeleton be on screen\",\n * applying a show delay and a minimum display time.\n */\nexport function useDisplayedLoading(\n loading: boolean,\n { delay = 0, minDuration = 0 }: DisplayedLoadingOptions = {},\n): boolean {\n const [shown, setShown] = useState(() => loading && delay <= 0);\n const shownAt = useRef(0);\n\n useLayoutEffect(() => {\n if (shown) shownAt.current = Date.now();\n }, [shown]);\n\n useLayoutEffect(() => {\n if (loading === shown) return;\n\n const wait = loading\n ? delay\n : Math.max(0, minDuration - (Date.now() - shownAt.current));\n\n if (wait <= 0) {\n setShown(loading);\n return;\n }\n const timer = setTimeout(() => setShown(loading), wait);\n return () => clearTimeout(timer);\n }, [loading, shown, delay, minDuration]);\n\n return shown;\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime","style","node"]}
package/dist/index.d.cts CHANGED
@@ -1,14 +1,23 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { HTMLMotionProps } from 'framer-motion';
3
2
 
4
3
  interface AutoSkeletonProps {
5
4
  loading: boolean;
6
5
  children: ReactNode;
6
+ /** Text announced to screen readers while loading. */
7
+ loadingLabel?: string;
8
+ /** Class name for the outer wrapper. */
9
+ className?: string;
10
+ /** Style for the outer wrapper; merged over the defaults (`position: relative; display: block`). */
11
+ style?: React.CSSProperties;
12
+ /** Wait this long (ms) before showing the skeleton, so fast loads never flash it. */
13
+ delay?: number;
14
+ /** Once shown, keep the skeleton at least this long (ms). */
15
+ minDuration?: number;
7
16
  config?: {
8
17
  color?: string;
9
18
  highlightColor?: string;
10
19
  borderRadius?: string | number;
11
- theme?: "light" | "dark" | "auto";
20
+ theme?: 'light' | 'dark' | 'auto';
12
21
  lightColor?: string;
13
22
  lightHighlightColor?: string;
14
23
  darkColor?: string;
@@ -17,7 +26,7 @@ interface AutoSkeletonProps {
17
26
  }
18
27
  declare const AutoSkeleton: React.FC<AutoSkeletonProps>;
19
28
 
20
- interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
29
+ interface SkeletonPrimitiveProps extends React.HTMLAttributes<HTMLDivElement> {
21
30
  width?: string | number;
22
31
  height?: string | number;
23
32
  borderRadius?: string | number;
@@ -26,4 +35,5 @@ interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
26
35
  }
27
36
  declare const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps>;
28
37
 
29
- export { AutoSkeleton, type AutoSkeletonProps, SkeletonPrimitive, type SkeletonPrimitiveProps };
38
+ export { AutoSkeleton, SkeletonPrimitive };
39
+ export type { AutoSkeletonProps, SkeletonPrimitiveProps };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,23 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { HTMLMotionProps } from 'framer-motion';
3
2
 
4
3
  interface AutoSkeletonProps {
5
4
  loading: boolean;
6
5
  children: ReactNode;
6
+ /** Text announced to screen readers while loading. */
7
+ loadingLabel?: string;
8
+ /** Class name for the outer wrapper. */
9
+ className?: string;
10
+ /** Style for the outer wrapper; merged over the defaults (`position: relative; display: block`). */
11
+ style?: React.CSSProperties;
12
+ /** Wait this long (ms) before showing the skeleton, so fast loads never flash it. */
13
+ delay?: number;
14
+ /** Once shown, keep the skeleton at least this long (ms). */
15
+ minDuration?: number;
7
16
  config?: {
8
17
  color?: string;
9
18
  highlightColor?: string;
10
19
  borderRadius?: string | number;
11
- theme?: "light" | "dark" | "auto";
20
+ theme?: 'light' | 'dark' | 'auto';
12
21
  lightColor?: string;
13
22
  lightHighlightColor?: string;
14
23
  darkColor?: string;
@@ -17,7 +26,7 @@ interface AutoSkeletonProps {
17
26
  }
18
27
  declare const AutoSkeleton: React.FC<AutoSkeletonProps>;
19
28
 
20
- interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
29
+ interface SkeletonPrimitiveProps extends React.HTMLAttributes<HTMLDivElement> {
21
30
  width?: string | number;
22
31
  height?: string | number;
23
32
  borderRadius?: string | number;
@@ -26,4 +35,5 @@ interface SkeletonPrimitiveProps extends HTMLMotionProps<"div"> {
26
35
  }
27
36
  declare const SkeletonPrimitive: React.FC<SkeletonPrimitiveProps>;
28
37
 
29
- export { AutoSkeleton, type AutoSkeletonProps, SkeletonPrimitive, type SkeletonPrimitiveProps };
38
+ export { AutoSkeleton, SkeletonPrimitive };
39
+ export type { AutoSkeletonProps, SkeletonPrimitiveProps };
package/dist/index.js CHANGED
@@ -1,73 +1,187 @@
1
- import { useRef, useState, useEffect } from 'react';
2
- import { motion } from 'framer-motion';
3
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ 'use client';
4
2
 
5
3
  // src/AutoSkeleton.tsx
4
+ import {
5
+ useEffect,
6
+ useLayoutEffect as useLayoutEffect2,
7
+ useRef as useRef2,
8
+ useState as useState2
9
+ } from "react";
10
+
11
+ // src/measure.ts
12
+ function groupRectsIntoLines(rects, tolerance = 2) {
13
+ const sorted = Array.from(rects).filter((r) => r.width > 0 && r.height > 0).sort((a, b) => a.top - b.top || a.left - b.left);
14
+ const lines = [];
15
+ for (const r of sorted) {
16
+ const line = lines[lines.length - 1];
17
+ if (line && Math.abs(r.top - line.top) <= tolerance) {
18
+ line.left = Math.min(line.left, r.left);
19
+ line.right = Math.max(line.right, r.left + r.width);
20
+ line.bottom = Math.max(line.bottom, r.top + r.height);
21
+ } else {
22
+ lines.push({
23
+ top: r.top,
24
+ left: r.left,
25
+ right: r.left + r.width,
26
+ bottom: r.top + r.height
27
+ });
28
+ }
29
+ }
30
+ return lines.map((l) => ({
31
+ top: l.top,
32
+ left: l.left,
33
+ width: l.right - l.left,
34
+ height: l.bottom - l.top
35
+ }));
36
+ }
37
+ function getTextLineBoxes(el) {
38
+ if (typeof document.createRange !== "function") return [];
39
+ const rects = [];
40
+ for (const node of Array.from(el.childNodes)) {
41
+ if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) continue;
42
+ const range = document.createRange();
43
+ range.selectNodeContents(node);
44
+ if (typeof range.getClientRects !== "function") return [];
45
+ rects.push(...Array.from(range.getClientRects()));
46
+ }
47
+ return groupRectsIntoLines(rects);
48
+ }
49
+
50
+ // src/SkeletonPrimitive.tsx
51
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
52
+ var PULSE_CSS = `
53
+ @keyframes auto-skeleton-pulse {
54
+ 0%, 100% { background-color: var(--auto-skeleton-c); }
55
+ 50% { background-color: var(--auto-skeleton-h); }
56
+ }
57
+ .auto-skeleton { animation: auto-skeleton-pulse 1.5s ease-in-out infinite; }
58
+ @media (prefers-reduced-motion: reduce) {
59
+ .auto-skeleton { animation: none; }
60
+ }
61
+ `;
6
62
  var SkeletonPrimitive = ({
7
63
  width,
8
64
  height,
9
65
  borderRadius = "4px",
10
66
  color = "var(--auto-skeleton-color, #eee)",
11
67
  highlightColor = "var(--auto-skeleton-highlight, #f5f5f5)",
68
+ className,
12
69
  style,
13
70
  ...props
14
71
  }) => {
15
- return /* @__PURE__ */ jsx(
16
- motion.div,
17
- {
18
- style: {
19
- width,
20
- height,
21
- borderRadius,
22
- backgroundColor: color,
23
- ...style
24
- },
25
- animate: {
26
- backgroundColor: [color, highlightColor, color]
27
- },
28
- transition: {
29
- duration: 1.5,
30
- repeat: Infinity,
31
- ease: "easeInOut"
32
- },
33
- "data-testid": "skeleton",
34
- ...props
72
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ /* @__PURE__ */ jsx("style", { href: "auto-skeleton-pulse", precedence: "default", children: PULSE_CSS }),
74
+ /* @__PURE__ */ jsx(
75
+ "div",
76
+ {
77
+ className: className ? `auto-skeleton ${className}` : "auto-skeleton",
78
+ style: {
79
+ width,
80
+ height,
81
+ borderRadius,
82
+ backgroundColor: color,
83
+ "--auto-skeleton-c": color,
84
+ "--auto-skeleton-h": highlightColor,
85
+ ...style
86
+ },
87
+ "data-testid": "skeleton",
88
+ ...props
89
+ }
90
+ )
91
+ ] });
92
+ };
93
+
94
+ // src/useDisplayedLoading.ts
95
+ import { useLayoutEffect, useRef, useState } from "react";
96
+ function useDisplayedLoading(loading, { delay = 0, minDuration = 0 } = {}) {
97
+ const [shown, setShown] = useState(() => loading && delay <= 0);
98
+ const shownAt = useRef(0);
99
+ useLayoutEffect(() => {
100
+ if (shown) shownAt.current = Date.now();
101
+ }, [shown]);
102
+ useLayoutEffect(() => {
103
+ if (loading === shown) return;
104
+ const wait = loading ? delay : Math.max(0, minDuration - (Date.now() - shownAt.current));
105
+ if (wait <= 0) {
106
+ setShown(loading);
107
+ return;
35
108
  }
36
- );
109
+ const timer = setTimeout(() => setShown(loading), wait);
110
+ return () => clearTimeout(timer);
111
+ }, [loading, shown, delay, minDuration]);
112
+ return shown;
113
+ }
114
+
115
+ // src/AutoSkeleton.tsx
116
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
117
+ var TEXT_BAR_RATIO = 0.7;
118
+ var FADE_MS = 200;
119
+ var VISUALLY_HIDDEN = {
120
+ position: "absolute",
121
+ width: 1,
122
+ height: 1,
123
+ padding: 0,
124
+ margin: -1,
125
+ overflow: "hidden",
126
+ clip: "rect(0, 0, 0, 0)",
127
+ whiteSpace: "nowrap",
128
+ border: 0
37
129
  };
38
130
  var AutoSkeleton = ({
39
131
  loading,
40
132
  children,
133
+ loadingLabel = "Loading\u2026",
134
+ className,
135
+ style,
136
+ delay = 0,
137
+ minDuration = 0,
41
138
  config
42
139
  }) => {
43
140
  const theme = config?.theme || "auto";
44
- const containerRef = useRef(null);
45
- const [skeletons, setSkeletons] = useState([]);
141
+ const showSkeleton = useDisplayedLoading(loading, { delay, minDuration });
142
+ const busy = loading || showSkeleton;
143
+ const [revealed, setRevealed] = useState2(!busy);
144
+ if (!busy && !revealed) setRevealed(true);
145
+ const hidden = showSkeleton || loading && !revealed;
146
+ const [fadingOut, setFadingOut] = useState2(false);
147
+ const [prevShowSkeleton, setPrevShowSkeleton] = useState2(showSkeleton);
148
+ if (prevShowSkeleton !== showSkeleton) {
149
+ setPrevShowSkeleton(showSkeleton);
150
+ setFadingOut(!showSkeleton);
151
+ }
152
+ useEffect(() => {
153
+ if (!fadingOut) return;
154
+ const timer = setTimeout(() => setFadingOut(false), FADE_MS);
155
+ return () => clearTimeout(timer);
156
+ }, [fadingOut]);
157
+ const containerRef = useRef2(null);
158
+ const [skeletons, setSkeletons] = useState2([]);
159
+ const [measured, setMeasured] = useState2(false);
46
160
  const updateSkeletons = () => {
47
161
  if (!containerRef.current) return;
48
162
  const containerRect = containerRef.current.getBoundingClientRect();
49
163
  const newSkeletons = [];
50
164
  const getSkeletonType = (el) => {
51
165
  const tagName = el.tagName.toLowerCase();
52
- const style = window.getComputedStyle(el);
166
+ const style2 = window.getComputedStyle(el);
53
167
  if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
54
168
  return { type: "primitive" };
55
169
  }
56
170
  const hasText = Array.from(el.childNodes).some(
57
171
  (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
58
172
  );
59
- if (hasText) return { type: "primitive" };
60
- const hasBackground = style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent";
61
- const hasBorder = style.borderWidth !== "0px" && style.borderStyle !== "none" && style.borderColor !== "rgba(0, 0, 0, 0)" && style.borderColor !== "transparent";
62
- const hasShadow = style.boxShadow !== "none";
173
+ if (hasText) return { type: "primitive", text: true };
174
+ const hasBackground = style2.backgroundColor !== "rgba(0, 0, 0, 0)" && style2.backgroundColor !== "transparent";
175
+ const hasBorder = style2.borderWidth !== "0px" && style2.borderStyle !== "none" && style2.borderColor !== "rgba(0, 0, 0, 0)" && style2.borderColor !== "transparent";
176
+ const hasShadow = style2.boxShadow !== "none";
63
177
  if (hasBackground || hasBorder || hasShadow) {
64
178
  return {
65
179
  type: "container",
66
180
  styles: {
67
- backgroundColor: style.backgroundColor,
68
- border: style.border,
69
- borderRadius: style.borderRadius,
70
- boxShadow: style.boxShadow
181
+ backgroundColor: style2.backgroundColor,
182
+ border: style2.border,
183
+ borderRadius: style2.borderRadius,
184
+ boxShadow: style2.boxShadow
71
185
  }
72
186
  };
73
187
  }
@@ -79,8 +193,8 @@ var AutoSkeleton = ({
79
193
  {
80
194
  acceptNode: (node2) => {
81
195
  const el = node2;
82
- const style = window.getComputedStyle(el);
83
- if (style.display === "none") return NodeFilter.FILTER_REJECT;
196
+ const style2 = window.getComputedStyle(el);
197
+ if (style2.display === "none") return NodeFilter.FILTER_REJECT;
84
198
  if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
85
199
  return NodeFilter.FILTER_SKIP;
86
200
  }
@@ -92,11 +206,26 @@ var AutoSkeleton = ({
92
206
  const el = node;
93
207
  const rect = el.getBoundingClientRect();
94
208
  const typeData = getSkeletonType(el);
95
- if (typeData) {
209
+ const lines = typeData?.text ? getTextLineBoxes(el) : [];
210
+ if (lines.length > 0) {
211
+ for (const line of lines) {
212
+ const height = Math.max(4, line.height * TEXT_BAR_RATIO);
213
+ newSkeletons.push({
214
+ key: `${idx++}`,
215
+ type: "primitive",
216
+ width: line.width,
217
+ height,
218
+ top: line.top + (line.height - height) / 2 - containerRect.top,
219
+ left: line.left - containerRect.left,
220
+ borderRadius: typeof config?.borderRadius === "number" ? `${config.borderRadius}px` : config?.borderRadius ?? "4px"
221
+ });
222
+ }
223
+ } else if (typeData) {
96
224
  let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
97
225
  if (typeData.type === "primitive") {
98
226
  if (borderRadius === "0px" || !borderRadius) {
99
- if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
227
+ if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) {
228
+ }
100
229
  borderRadius = "4px";
101
230
  if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
102
231
  if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
@@ -127,8 +256,9 @@ var AutoSkeleton = ({
127
256
  });
128
257
  }
129
258
  setSkeletons(newSkeletons);
259
+ setMeasured(true);
130
260
  };
131
- useEffect(() => {
261
+ useLayoutEffect2(() => {
132
262
  if (!containerRef.current) return;
133
263
  updateSkeletons();
134
264
  const observer = new ResizeObserver(() => {
@@ -145,7 +275,7 @@ var AutoSkeleton = ({
145
275
  observer.disconnect();
146
276
  mutationObserver.disconnect();
147
277
  };
148
- }, [loading, children]);
278
+ }, [showSkeleton, children]);
149
279
  const getEffectiveColors = () => {
150
280
  if (config?.color || config?.highlightColor) {
151
281
  return {
@@ -168,77 +298,98 @@ var AutoSkeleton = ({
168
298
  return { color: void 0, highlightColor: void 0 };
169
299
  };
170
300
  const effectiveColors = getEffectiveColors();
171
- return /* @__PURE__ */ jsxs(
301
+ return /* @__PURE__ */ jsxs2(
172
302
  "div",
173
303
  {
174
- style: { position: "relative", display: "inline-block", width: "auto" },
304
+ className,
305
+ style: { position: "relative", display: "block", ...style },
175
306
  ...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
307
+ "aria-busy": busy,
176
308
  children: [
177
- /* @__PURE__ */ jsx(
309
+ /* @__PURE__ */ jsx2("span", { role: "status", style: VISUALLY_HIDDEN, children: busy ? loadingLabel : "" }),
310
+ /* @__PURE__ */ jsx2(
178
311
  "div",
179
312
  {
180
313
  ref: containerRef,
181
314
  style: {
182
- opacity: loading ? 0 : 1,
183
- transition: "opacity 0.2s ease"
184
- // Smooth transition when loading finishes
315
+ opacity: hidden ? 0 : 1,
316
+ // Hide instantly (no ghost content under the bars); fade only on reveal
317
+ transition: hidden ? "none" : `opacity ${FADE_MS}ms ease`
185
318
  },
186
- "aria-hidden": loading,
319
+ "aria-hidden": hidden,
320
+ inert: busy,
187
321
  children
188
322
  }
189
323
  ),
190
- loading && /* @__PURE__ */ jsx(
324
+ (showSkeleton || fadingOut) && /* @__PURE__ */ jsxs2(
191
325
  "div",
192
326
  {
327
+ "data-testid": "skeleton-overlay",
193
328
  style: {
194
329
  position: "absolute",
195
330
  top: 0,
196
331
  left: 0,
197
332
  width: "100%",
198
333
  height: "100%",
199
- pointerEvents: "none"
334
+ pointerEvents: "none",
335
+ opacity: showSkeleton ? 1 : 0,
336
+ transition: `opacity ${FADE_MS}ms ease`
200
337
  },
201
- children: skeletons.map((sk) => /* @__PURE__ */ jsx(
202
- "div",
203
- {
204
- style: { position: "absolute", top: sk.top, left: sk.left },
205
- children: sk.type === "primitive" ? /* @__PURE__ */ jsx(
206
- SkeletonPrimitive,
207
- {
208
- width: sk.width,
209
- height: sk.height,
210
- borderRadius: sk.borderRadius,
211
- color: effectiveColors.color,
212
- highlightColor: effectiveColors.highlightColor
213
- }
214
- ) : (
215
- /* Container Clone: Renders static style */
216
- /* @__PURE__ */ jsx(
217
- "div",
338
+ children: [
339
+ !measured && // Server render / before hydration: nothing can be measured yet, so
340
+ // cover the reserved space with one block instead of showing nothing
341
+ /* @__PURE__ */ jsx2(
342
+ SkeletonPrimitive,
343
+ {
344
+ width: "100%",
345
+ height: "100%",
346
+ color: effectiveColors.color,
347
+ highlightColor: effectiveColors.highlightColor
348
+ }
349
+ ),
350
+ skeletons.map((sk) => /* @__PURE__ */ jsx2(
351
+ "div",
352
+ {
353
+ style: { position: "absolute", top: sk.top, left: sk.left },
354
+ children: sk.type === "primitive" ? /* @__PURE__ */ jsx2(
355
+ SkeletonPrimitive,
218
356
  {
219
- style: {
220
- width: sk.width,
221
- height: sk.height,
222
- borderRadius: sk.borderRadius,
223
- ...sk.style,
224
- // Apply captured container styles
225
- // Ensure it doesn't hide children if z-index issues arise,
226
- // but since we render in DOM order, children come later and sit on top.
227
- boxSizing: "border-box"
228
- }
357
+ width: sk.width,
358
+ height: sk.height,
359
+ borderRadius: sk.borderRadius,
360
+ color: effectiveColors.color,
361
+ highlightColor: effectiveColors.highlightColor
229
362
  }
363
+ ) : (
364
+ /* Container Clone: Renders static style */
365
+ /* @__PURE__ */ jsx2(
366
+ "div",
367
+ {
368
+ style: {
369
+ width: sk.width,
370
+ height: sk.height,
371
+ borderRadius: sk.borderRadius,
372
+ ...sk.style,
373
+ // Apply captured container styles
374
+ // Ensure it doesn't hide children if z-index issues arise,
375
+ // but since we render in DOM order, children come later and sit on top.
376
+ boxSizing: "border-box"
377
+ }
378
+ }
379
+ )
230
380
  )
231
- )
232
- },
233
- sk.key
234
- ))
381
+ },
382
+ sk.key
383
+ ))
384
+ ]
235
385
  }
236
386
  )
237
387
  ]
238
388
  }
239
389
  );
240
390
  };
241
-
242
- export { AutoSkeleton, SkeletonPrimitive };
243
- //# sourceMappingURL=index.js.map
391
+ export {
392
+ AutoSkeleton,
393
+ SkeletonPrimitive
394
+ };
244
395
  //# sourceMappingURL=index.js.map