smart-skeleton-loader 1.0.1 → 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.
- package/README.md +28 -18
- package/dist/index.cjs +256 -86
- 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 +235 -84
- 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"],"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,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,187 @@
|
|
|
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
|
}
|
|
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
|
|
45
|
-
const
|
|
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
|
|
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 =
|
|
61
|
-
const hasBorder =
|
|
62
|
-
const hasShadow =
|
|
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:
|
|
68
|
-
border:
|
|
69
|
-
borderRadius:
|
|
70
|
-
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
|
|
83
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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__ */
|
|
301
|
+
return /* @__PURE__ */ jsxs2(
|
|
172
302
|
"div",
|
|
173
303
|
{
|
|
174
|
-
|
|
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__ */
|
|
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:
|
|
183
|
-
|
|
184
|
-
|
|
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":
|
|
319
|
+
"aria-hidden": hidden,
|
|
320
|
+
inert: busy,
|
|
187
321
|
children
|
|
188
322
|
}
|
|
189
323
|
),
|
|
190
|
-
|
|
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:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
234
|
-
|
|
381
|
+
},
|
|
382
|
+
sk.key
|
|
383
|
+
))
|
|
384
|
+
]
|
|
235
385
|
}
|
|
236
386
|
)
|
|
237
387
|
]
|
|
238
388
|
}
|
|
239
389
|
);
|
|
240
390
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
391
|
+
export {
|
|
392
|
+
AutoSkeleton,
|
|
393
|
+
SkeletonPrimitive
|
|
394
|
+
};
|
|
244
395
|
//# sourceMappingURL=index.js.map
|