49
common/resources/client/ui/skeleton/skeleton.css
vendored
Executable file
49
common/resources/client/ui/skeleton/skeleton.css
vendored
Executable file
@@ -0,0 +1,49 @@
|
||||
.skeleton:before {
|
||||
content: "\00a0";
|
||||
}
|
||||
|
||||
.skeleton-wave {
|
||||
animation: skeleton-wave 2s ease-in-out infinite;
|
||||
background-size: 200px 100%;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.6),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
}
|
||||
|
||||
.dark .skeleton-wave {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.15),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
}
|
||||
|
||||
.skeleton-pulsate {
|
||||
animation: skeleton-pulsate 1.5s ease-in-out infinite;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes skeleton-wave {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-pulsate {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
61
common/resources/client/ui/skeleton/skeleton.tsx
Executable file
61
common/resources/client/ui/skeleton/skeleton.tsx
Executable file
@@ -0,0 +1,61 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface SkeletonProps {
|
||||
variant?: 'avatar' | 'text' | 'rect' | 'icon';
|
||||
animation?: 'pulsate' | 'wave' | null; // disable animation completely with null
|
||||
className?: string;
|
||||
size?: string;
|
||||
display?: string;
|
||||
radius?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export function Skeleton({
|
||||
variant = 'text',
|
||||
animation = 'wave',
|
||||
size,
|
||||
className,
|
||||
display = 'block',
|
||||
radius = 'rounded',
|
||||
style,
|
||||
}: SkeletonProps) {
|
||||
return (
|
||||
<span
|
||||
style={style}
|
||||
className={clsx(
|
||||
'skeleton relative overflow-hidden bg-fg-base/4 bg-no-repeat will-change-transform',
|
||||
radius,
|
||||
skeletonSize({variant, size}),
|
||||
display,
|
||||
variant === 'text' && 'origin-[0_55%] scale-y-[0.6]',
|
||||
variant === 'avatar' && 'flex-shrink-0',
|
||||
variant === 'icon' && 'mx-8 flex-shrink-0',
|
||||
animation === 'wave' && 'skeleton-wave',
|
||||
animation === 'pulsate' && 'skeleton-pulsate',
|
||||
className,
|
||||
)}
|
||||
aria-busy
|
||||
aria-live="polite"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface SkeletonSizeProps {
|
||||
variant: SkeletonProps['variant'];
|
||||
size: SkeletonProps['size'];
|
||||
}
|
||||
function skeletonSize({variant, size}: SkeletonSizeProps): string | undefined {
|
||||
if (size) {
|
||||
return size;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'avatar':
|
||||
return 'h-40 w-40';
|
||||
case 'icon':
|
||||
return 'h-24 h-24';
|
||||
case 'rect':
|
||||
return 'h-full w-full';
|
||||
default:
|
||||
return 'w-full';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user