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.
- package/README.md +36 -23
- package/dist/index.cjs +276 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +255 -85
- package/dist/index.js.map +1 -1
- package/dist/styles.d.cts +1 -1
- package/dist/styles.d.ts +1 -1
- package/package.json +44 -42
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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,
|
|
38
|
+
export { AutoSkeleton, SkeletonPrimitive };
|
|
39
|
+
export type { AutoSkeletonProps, SkeletonPrimitiveProps };
|
package/dist/index.js
CHANGED
|
@@ -1,73 +1,206 @@
|
|
|
1
|
-
|
|
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__ */
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
45
|
-
const
|
|
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
|
|
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 =
|
|
61
|
-
const hasBorder =
|
|
62
|
-
const hasShadow =
|
|
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:
|
|
68
|
-
border:
|
|
69
|
-
borderRadius:
|
|
70
|
-
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
|
|
83
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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 (
|
|
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__ */
|
|
320
|
+
return /* @__PURE__ */ jsxs2(
|
|
172
321
|
"div",
|
|
173
322
|
{
|
|
174
|
-
|
|
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__ */
|
|
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:
|
|
183
|
-
|
|
184
|
-
|
|
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":
|
|
338
|
+
"aria-hidden": hidden,
|
|
339
|
+
inert: busy,
|
|
187
340
|
children
|
|
188
341
|
}
|
|
189
342
|
),
|
|
190
|
-
|
|
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:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
234
|
-
|
|
400
|
+
},
|
|
401
|
+
sk.key
|
|
402
|
+
))
|
|
403
|
+
]
|
|
235
404
|
}
|
|
236
405
|
)
|
|
237
406
|
]
|
|
238
407
|
}
|
|
239
408
|
);
|
|
240
409
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
410
|
+
export {
|
|
411
|
+
AutoSkeleton,
|
|
412
|
+
SkeletonPrimitive
|
|
413
|
+
};
|
|
244
414
|
//# sourceMappingURL=index.js.map
|