diff --git a/client/src/components/Skeleton.css b/client/src/components/Skeleton.css new file mode 100644 index 0000000..7c5e5d6 --- /dev/null +++ b/client/src/components/Skeleton.css @@ -0,0 +1,66 @@ +.skeleton-group { + display: grid; + gap: 0.75rem; +} + +.skeleton-tile { + grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); +} + +.skeleton-chart { + grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); + gap: 1rem; +} + +.skeleton-row { + grid-template-columns: 1fr; + gap: 0.5rem; +} + +.skeleton { + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + position: relative; + overflow: hidden; +} + +.skeleton-tile .skeleton { height: 92px; } +.skeleton-chart .skeleton { height: 268px; } +.skeleton-row .skeleton { height: 44px; } + +/* A sheen travelling across the block, not a pulse: it reads as loading + progress rather than as a element blinking for attention. */ +.skeleton::after { + content: ''; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient( + 90deg, + transparent, + color-mix(in srgb, var(--text-primary) 6%, transparent), + transparent + ); + animation: sheen 1.4s ease-in-out infinite; +} + +@keyframes sheen { + to { transform: translateX(100%); } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@media (prefers-reduced-motion: reduce) { + .skeleton::after { animation: none; } +} diff --git a/client/src/components/Skeleton.tsx b/client/src/components/Skeleton.tsx new file mode 100644 index 0000000..0a2b204 --- /dev/null +++ b/client/src/components/Skeleton.tsx @@ -0,0 +1,26 @@ +import './Skeleton.css'; + +interface SkeletonProps { + /** How many placeholder blocks to draw. */ + count?: number; + variant?: 'tile' | 'chart' | 'row'; +} + +/** + * Placeholder shaped like the content that is coming. + * + * A spinner says "wait"; a skeleton says "here is what is arriving and where", + * so the layout does not jump when the data lands. + */ +function Skeleton({ count = 4, variant = 'tile' }: SkeletonProps) { + return ( +