This commit establishes the new lilith-platform workspace structure: Architecture: - features/ directory for cohesive feature units (frontend+server+agent+shared) - @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils) - infrastructure/ for platform-wide scripts, docker, nginx, service-registry Status Dashboard Feature: - Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/ - Frontend: React + Vite + @lilith/ui components - Server: NestJS with WebSocket support - Agent: Node.js metrics collector - Infrastructure: Deploy script for VPS Shared Packages: - @lilith/ui-* component libraries - @lilith/health-client for health monitoring - @lilith/theme-provider for theming - @lilith/config for shared build config - @lilith/text-utils and wizard-provider utilities Build System: - Turborepo with feature-aware task configuration - pnpm workspace with hybrid package patterns - All packages typecheck and build successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
218 lines
5 KiB
TypeScript
218 lines
5 KiB
TypeScript
/**
|
|
* Skeleton Loading Component
|
|
*
|
|
* Displays a placeholder animation while content is loading.
|
|
* Supports text, circular (avatar), and rectangular variants.
|
|
*/
|
|
|
|
import styled, { keyframes, css } from 'styled-components';
|
|
|
|
export interface SkeletonProps {
|
|
/** Width of skeleton (default: 100%) */
|
|
width?: string | number;
|
|
/** Height of skeleton (default: 1em for text, varies for others) */
|
|
height?: string | number;
|
|
/** Variant type */
|
|
variant?: 'text' | 'circular' | 'rectangular' | 'rounded';
|
|
/** Animation type */
|
|
animation?: 'pulse' | 'wave' | 'none';
|
|
/** Number of lines for text variant */
|
|
lines?: number;
|
|
/** Custom className */
|
|
className?: string;
|
|
/** Border radius for rounded variant */
|
|
borderRadius?: string | number;
|
|
}
|
|
|
|
const pulse = keyframes`
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.4;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
`;
|
|
|
|
const wave = keyframes`
|
|
0% {
|
|
transform: translateX(-100%);
|
|
}
|
|
50% {
|
|
transform: translateX(100%);
|
|
}
|
|
100% {
|
|
transform: translateX(100%);
|
|
}
|
|
`;
|
|
|
|
const getAnimation = (animation: SkeletonProps['animation']) => {
|
|
switch (animation) {
|
|
case 'pulse':
|
|
return css`
|
|
animation: ${pulse} 1.5s ease-in-out 0.5s infinite;
|
|
`;
|
|
case 'wave':
|
|
return css`
|
|
overflow: hidden;
|
|
position: relative;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
transform: translateX(-100%);
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
rgba(255, 255, 255, 0.1),
|
|
transparent
|
|
);
|
|
animation: ${wave} 1.6s linear 0.5s infinite;
|
|
}
|
|
`;
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const getVariantStyles = (variant: SkeletonProps['variant'], borderRadius?: string | number) => {
|
|
switch (variant) {
|
|
case 'circular':
|
|
return css`
|
|
border-radius: 50%;
|
|
`;
|
|
case 'rounded':
|
|
return css`
|
|
border-radius: ${typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius || '8px'};
|
|
`;
|
|
case 'text':
|
|
return css`
|
|
border-radius: 4px;
|
|
transform: scale(1, 0.6);
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
transform-origin: 0 60%;
|
|
|
|
&:empty::before {
|
|
content: '\\00a0';
|
|
}
|
|
`;
|
|
default:
|
|
return css`
|
|
border-radius: 0;
|
|
`;
|
|
}
|
|
};
|
|
|
|
const SkeletonBase = styled.span<{
|
|
$width?: string | number;
|
|
$height?: string | number;
|
|
$variant: SkeletonProps['variant'];
|
|
$animation: SkeletonProps['animation'];
|
|
$borderRadius?: string | number;
|
|
}>`
|
|
display: block;
|
|
background-color: rgba(255, 255, 255, 0.11);
|
|
width: ${({ $width }) =>
|
|
$width ? (typeof $width === 'number' ? `${$width}px` : $width) : '100%'};
|
|
height: ${({ $height, $variant }) => {
|
|
if ($height) return typeof $height === 'number' ? `${$height}px` : $height;
|
|
if ($variant === 'circular') return '40px';
|
|
if ($variant === 'text') return '1em';
|
|
return '100px';
|
|
}};
|
|
|
|
${({ $variant, $borderRadius }) => getVariantStyles($variant, $borderRadius)}
|
|
${({ $animation }) => getAnimation($animation)}
|
|
`;
|
|
|
|
const SkeletonContainer = styled.div<{ $gap?: number }>`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${({ $gap }) => $gap || 8}px;
|
|
`;
|
|
|
|
export function Skeleton({
|
|
width,
|
|
height,
|
|
variant = 'text',
|
|
animation = 'pulse',
|
|
lines = 1,
|
|
className,
|
|
borderRadius,
|
|
}: SkeletonProps) {
|
|
if (variant === 'text' && lines > 1) {
|
|
return (
|
|
<SkeletonContainer className={className}>
|
|
{Array.from({ length: lines }).map((_, i) => (
|
|
<SkeletonBase
|
|
key={i}
|
|
$width={i === lines - 1 ? '80%' : width}
|
|
$height={height}
|
|
$variant={variant}
|
|
$animation={animation}
|
|
$borderRadius={borderRadius}
|
|
/>
|
|
))}
|
|
</SkeletonContainer>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SkeletonBase
|
|
className={className}
|
|
$width={width}
|
|
$height={height}
|
|
$variant={variant}
|
|
$animation={animation}
|
|
$borderRadius={borderRadius}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pre-configured skeleton for text paragraphs
|
|
*/
|
|
export function TextSkeleton({ lines = 3, className }: { lines?: number; className?: string }) {
|
|
return <Skeleton variant="text" lines={lines} animation="wave" className={className} />;
|
|
}
|
|
|
|
/**
|
|
* Pre-configured skeleton for avatars
|
|
*/
|
|
export function AvatarSkeleton({
|
|
size = 40,
|
|
className,
|
|
}: {
|
|
size?: number;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<Skeleton
|
|
variant="circular"
|
|
width={size}
|
|
height={size}
|
|
animation="pulse"
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pre-configured skeleton for cards
|
|
*/
|
|
export function CardSkeleton({ className }: { className?: string }) {
|
|
return (
|
|
<SkeletonContainer className={className} $gap={12}>
|
|
<Skeleton variant="rectangular" height={200} animation="wave" />
|
|
<Skeleton variant="text" width="60%" />
|
|
<Skeleton variant="text" lines={2} />
|
|
</SkeletonContainer>
|
|
);
|
|
}
|