smart-skeleton-loader 1.0.1 → 2.1.2

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 { HTMLMotionProps, motion } from 'framer-motion';\nimport React from 'react';\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, { ReactNode, useEffect, useRef, useState } 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","../src/usePrefersDark.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';\nimport { usePrefersDark } from './usePrefersDark';\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 prefersDark = usePrefersDark();\n // 'auto' follows the OS color scheme so light*/dark* colors apply there too\n const resolvedTheme =\n theme === 'auto' ? (prefersDark ? 'dark' : 'light') : theme;\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 (\n resolvedTheme === 'dark' &&\n (config?.darkColor || config?.darkHighlightColor)\n ) {\n return {\n color: config.darkColor,\n highlightColor: config.darkHighlightColor,\n };\n }\n if (\n resolvedTheme === '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","import { useSyncExternalStore } from 'react';\n\nconst QUERY = '(prefers-color-scheme: dark)';\n\nconst getMedia = () =>\n typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia(QUERY)\n : null;\n\nconst subscribe = (onChange: () => void) => {\n const media = getMedia();\n media?.addEventListener('change', onChange);\n return () => media?.removeEventListener('change', onChange);\n};\n\n/** Live `prefers-color-scheme: dark` match; `false` on the server. */\nexport function usePrefersDark(): boolean {\n return useSyncExternalStore(\n subscribe,\n () => getMedia()?.matches ?? false,\n () => false,\n );\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","style","node"]}
package/dist/index.d.cts CHANGED
@@ -1,9 +1,18 @@
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;
@@ -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,9 +1,18 @@
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;
@@ -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,206 @@
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
  }
109
+ const timer = setTimeout(() => setShown(loading), wait);
110
+ return () => clearTimeout(timer);
111
+ }, [loading, shown, delay, minDuration]);
112
+ return shown;
113
+ }
114
+
115
+ // src/usePrefersDark.ts
116
+ import { useSyncExternalStore } from "react";
117
+ var QUERY = "(prefers-color-scheme: dark)";
118
+ var getMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null;
119
+ var subscribe = (onChange) => {
120
+ const media = getMedia();
121
+ media?.addEventListener("change", onChange);
122
+ return () => media?.removeEventListener("change", onChange);
123
+ };
124
+ function usePrefersDark() {
125
+ return useSyncExternalStore(
126
+ subscribe,
127
+ () => getMedia()?.matches ?? false,
128
+ () => false
36
129
  );
130
+ }
131
+
132
+ // src/AutoSkeleton.tsx
133
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
134
+ var TEXT_BAR_RATIO = 0.7;
135
+ var FADE_MS = 200;
136
+ var VISUALLY_HIDDEN = {
137
+ position: "absolute",
138
+ width: 1,
139
+ height: 1,
140
+ padding: 0,
141
+ margin: -1,
142
+ overflow: "hidden",
143
+ clip: "rect(0, 0, 0, 0)",
144
+ whiteSpace: "nowrap",
145
+ border: 0
37
146
  };
38
147
  var AutoSkeleton = ({
39
148
  loading,
40
149
  children,
150
+ loadingLabel = "Loading\u2026",
151
+ className,
152
+ style,
153
+ delay = 0,
154
+ minDuration = 0,
41
155
  config
42
156
  }) => {
43
157
  const theme = config?.theme || "auto";
44
- const containerRef = useRef(null);
45
- const [skeletons, setSkeletons] = useState([]);
158
+ const prefersDark = usePrefersDark();
159
+ const resolvedTheme = theme === "auto" ? prefersDark ? "dark" : "light" : theme;
160
+ const showSkeleton = useDisplayedLoading(loading, { delay, minDuration });
161
+ const busy = loading || showSkeleton;
162
+ const [revealed, setRevealed] = useState2(!busy);
163
+ if (!busy && !revealed) setRevealed(true);
164
+ const hidden = showSkeleton || loading && !revealed;
165
+ const [fadingOut, setFadingOut] = useState2(false);
166
+ const [prevShowSkeleton, setPrevShowSkeleton] = useState2(showSkeleton);
167
+ if (prevShowSkeleton !== showSkeleton) {
168
+ setPrevShowSkeleton(showSkeleton);
169
+ setFadingOut(!showSkeleton);
170
+ }
171
+ useEffect(() => {
172
+ if (!fadingOut) return;
173
+ const timer = setTimeout(() => setFadingOut(false), FADE_MS);
174
+ return () => clearTimeout(timer);
175
+ }, [fadingOut]);
176
+ const containerRef = useRef2(null);
177
+ const [skeletons, setSkeletons] = useState2([]);
178
+ const [measured, setMeasured] = useState2(false);
46
179
  const updateSkeletons = () => {
47
180
  if (!containerRef.current) return;
48
181
  const containerRect = containerRef.current.getBoundingClientRect();
49
182
  const newSkeletons = [];
50
183
  const getSkeletonType = (el) => {
51
184
  const tagName = el.tagName.toLowerCase();
52
- const style = window.getComputedStyle(el);
185
+ const style2 = window.getComputedStyle(el);
53
186
  if (["img", "input", "button", "textarea", "select"].includes(tagName)) {
54
187
  return { type: "primitive" };
55
188
  }
56
189
  const hasText = Array.from(el.childNodes).some(
57
190
  (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
58
191
  );
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";
192
+ if (hasText) return { type: "primitive", text: true };
193
+ const hasBackground = style2.backgroundColor !== "rgba(0, 0, 0, 0)" && style2.backgroundColor !== "transparent";
194
+ const hasBorder = style2.borderWidth !== "0px" && style2.borderStyle !== "none" && style2.borderColor !== "rgba(0, 0, 0, 0)" && style2.borderColor !== "transparent";
195
+ const hasShadow = style2.boxShadow !== "none";
63
196
  if (hasBackground || hasBorder || hasShadow) {
64
197
  return {
65
198
  type: "container",
66
199
  styles: {
67
- backgroundColor: style.backgroundColor,
68
- border: style.border,
69
- borderRadius: style.borderRadius,
70
- boxShadow: style.boxShadow
200
+ backgroundColor: style2.backgroundColor,
201
+ border: style2.border,
202
+ borderRadius: style2.borderRadius,
203
+ boxShadow: style2.boxShadow
71
204
  }
72
205
  };
73
206
  }
@@ -79,8 +212,8 @@ var AutoSkeleton = ({
79
212
  {
80
213
  acceptNode: (node2) => {
81
214
  const el = node2;
82
- const style = window.getComputedStyle(el);
83
- if (style.display === "none") return NodeFilter.FILTER_REJECT;
215
+ const style2 = window.getComputedStyle(el);
216
+ if (style2.display === "none") return NodeFilter.FILTER_REJECT;
84
217
  if (getSkeletonType(el)) return NodeFilter.FILTER_ACCEPT;
85
218
  return NodeFilter.FILTER_SKIP;
86
219
  }
@@ -92,11 +225,26 @@ var AutoSkeleton = ({
92
225
  const el = node;
93
226
  const rect = el.getBoundingClientRect();
94
227
  const typeData = getSkeletonType(el);
95
- if (typeData) {
228
+ const lines = typeData?.text ? getTextLineBoxes(el) : [];
229
+ if (lines.length > 0) {
230
+ for (const line of lines) {
231
+ const height = Math.max(4, line.height * TEXT_BAR_RATIO);
232
+ newSkeletons.push({
233
+ key: `${idx++}`,
234
+ type: "primitive",
235
+ width: line.width,
236
+ height,
237
+ top: line.top + (line.height - height) / 2 - containerRect.top,
238
+ left: line.left - containerRect.left,
239
+ borderRadius: typeof config?.borderRadius === "number" ? `${config.borderRadius}px` : config?.borderRadius ?? "4px"
240
+ });
241
+ }
242
+ } else if (typeData) {
96
243
  let borderRadius = config?.borderRadius || typeData.styles?.borderRadius || window.getComputedStyle(el).borderRadius;
97
244
  if (typeData.type === "primitive") {
98
245
  if (borderRadius === "0px" || !borderRadius) {
99
- if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) ;
246
+ if (window.getComputedStyle(el).borderRadius === "50%" || rect.width === rect.height && rect.width < 50) {
247
+ }
100
248
  borderRadius = "4px";
101
249
  if (el.tagName.toLowerCase() === "button") borderRadius = "6px";
102
250
  if (el.tagName.toLowerCase() === "img") borderRadius = "4px";
@@ -127,8 +275,9 @@ var AutoSkeleton = ({
127
275
  });
128
276
  }
129
277
  setSkeletons(newSkeletons);
278
+ setMeasured(true);
130
279
  };
131
- useEffect(() => {
280
+ useLayoutEffect2(() => {
132
281
  if (!containerRef.current) return;
133
282
  updateSkeletons();
134
283
  const observer = new ResizeObserver(() => {
@@ -145,7 +294,7 @@ var AutoSkeleton = ({
145
294
  observer.disconnect();
146
295
  mutationObserver.disconnect();
147
296
  };
148
- }, [loading, children]);
297
+ }, [showSkeleton, children]);
149
298
  const getEffectiveColors = () => {
150
299
  if (config?.color || config?.highlightColor) {
151
300
  return {
@@ -153,13 +302,13 @@ var AutoSkeleton = ({
153
302
  highlightColor: config.highlightColor
154
303
  };
155
304
  }
156
- if (theme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
305
+ if (resolvedTheme === "dark" && (config?.darkColor || config?.darkHighlightColor)) {
157
306
  return {
158
307
  color: config.darkColor,
159
308
  highlightColor: config.darkHighlightColor
160
309
  };
161
310
  }
162
- if (theme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
311
+ if (resolvedTheme === "light" && (config?.lightColor || config?.lightHighlightColor)) {
163
312
  return {
164
313
  color: config.lightColor,
165
314
  highlightColor: config.lightHighlightColor
@@ -168,77 +317,98 @@ var AutoSkeleton = ({
168
317
  return { color: void 0, highlightColor: void 0 };
169
318
  };
170
319
  const effectiveColors = getEffectiveColors();
171
- return /* @__PURE__ */ jsxs(
320
+ return /* @__PURE__ */ jsxs2(
172
321
  "div",
173
322
  {
174
- style: { position: "relative", display: "inline-block", width: "auto" },
323
+ className,
324
+ style: { position: "relative", display: "block", ...style },
175
325
  ...theme !== "auto" ? { "data-skeleton-theme": theme } : {},
326
+ "aria-busy": busy,
176
327
  children: [
177
- /* @__PURE__ */ jsx(
328
+ /* @__PURE__ */ jsx2("span", { role: "status", style: VISUALLY_HIDDEN, children: busy ? loadingLabel : "" }),
329
+ /* @__PURE__ */ jsx2(
178
330
  "div",
179
331
  {
180
332
  ref: containerRef,
181
333
  style: {
182
- opacity: loading ? 0 : 1,
183
- transition: "opacity 0.2s ease"
184
- // Smooth transition when loading finishes
334
+ opacity: hidden ? 0 : 1,
335
+ // Hide instantly (no ghost content under the bars); fade only on reveal
336
+ transition: hidden ? "none" : `opacity ${FADE_MS}ms ease`
185
337
  },
186
- "aria-hidden": loading,
338
+ "aria-hidden": hidden,
339
+ inert: busy,
187
340
  children
188
341
  }
189
342
  ),
190
- loading && /* @__PURE__ */ jsx(
343
+ (showSkeleton || fadingOut) && /* @__PURE__ */ jsxs2(
191
344
  "div",
192
345
  {
346
+ "data-testid": "skeleton-overlay",
193
347
  style: {
194
348
  position: "absolute",
195
349
  top: 0,
196
350
  left: 0,
197
351
  width: "100%",
198
352
  height: "100%",
199
- pointerEvents: "none"
353
+ pointerEvents: "none",
354
+ opacity: showSkeleton ? 1 : 0,
355
+ transition: `opacity ${FADE_MS}ms ease`
200
356
  },
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",
357
+ children: [
358
+ !measured && // Server render / before hydration: nothing can be measured yet, so
359
+ // cover the reserved space with one block instead of showing nothing
360
+ /* @__PURE__ */ jsx2(
361
+ SkeletonPrimitive,
362
+ {
363
+ width: "100%",
364
+ height: "100%",
365
+ color: effectiveColors.color,
366
+ highlightColor: effectiveColors.highlightColor
367
+ }
368
+ ),
369
+ skeletons.map((sk) => /* @__PURE__ */ jsx2(
370
+ "div",
371
+ {
372
+ style: { position: "absolute", top: sk.top, left: sk.left },
373
+ children: sk.type === "primitive" ? /* @__PURE__ */ jsx2(
374
+ SkeletonPrimitive,
218
375
  {
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
- }
376
+ width: sk.width,
377
+ height: sk.height,
378
+ borderRadius: sk.borderRadius,
379
+ color: effectiveColors.color,
380
+ highlightColor: effectiveColors.highlightColor
229
381
  }
382
+ ) : (
383
+ /* Container Clone: Renders static style */
384
+ /* @__PURE__ */ jsx2(
385
+ "div",
386
+ {
387
+ style: {
388
+ width: sk.width,
389
+ height: sk.height,
390
+ borderRadius: sk.borderRadius,
391
+ ...sk.style,
392
+ // Apply captured container styles
393
+ // Ensure it doesn't hide children if z-index issues arise,
394
+ // but since we render in DOM order, children come later and sit on top.
395
+ boxSizing: "border-box"
396
+ }
397
+ }
398
+ )
230
399
  )
231
- )
232
- },
233
- sk.key
234
- ))
400
+ },
401
+ sk.key
402
+ ))
403
+ ]
235
404
  }
236
405
  )
237
406
  ]
238
407
  }
239
408
  );
240
409
  };
241
-
242
- export { AutoSkeleton, SkeletonPrimitive };
243
- //# sourceMappingURL=index.js.map
410
+ export {
411
+ AutoSkeleton,
412
+ SkeletonPrimitive
413
+ };
244
414
  //# sourceMappingURL=index.js.map