import React, { useEffect, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { DURATION_MS, EASE, pulseKeyframes } from '@/shared/styles/motionTokens'; import { useReducedMotion } from '@/shared/hooks/useReducedMotion'; /** * Unified loading primitives. Three components, one aesthetic. * * * For full-component / full-page loads. Replaces decorative spinners. * * * For inline button states + OAuth waits. Spinner = "I'm doing it now". * * * For "nothing here yet" empty lists. Replaces ad-hoc "Loading..." text. * * `delayMs` (Skeleton + EmptyState): don't show until N ms have elapsed. * Prevents the flash-of-skeleton on fast loads (<100ms common case). */ interface SkeletonProps { variant?: 'card' | 'line' | 'circle' | 'custom'; width?: number | string; height?: number | string; /** Default 100ms; pass 0 to render immediately */ delayMs?: number; } export const Skeleton: React.FC = ({ variant = 'line', width, height, delayMs = 100, }) => { const c = useClaudeTokens(); const reduced = useReducedMotion(); const [show, setShow] = useState(delayMs === 0); useEffect(() => { if (delayMs === 0) return; const t = setTimeout(() => setShow(true), delayMs); return () => clearTimeout(t); }, [delayMs]); if (!show) return null; const dimensions: React.CSSProperties = { width: width ?? (variant === 'card' ? '100%' : variant === 'circle' ? 24 : '60%'), height: height ?? (variant === 'card' ? 80 : variant === 'circle' ? 24 : 12), }; const radius = variant === 'circle' ? '50%' : variant === 'card' ? 8 : 4; return ( ); }; interface InlineSpinnerProps { /** 14 / 16 / 18; defaults to 16 */ size?: 14 | 16 | 18 | 20; color?: string; } export const InlineSpinner: React.FC = ({ size = 16, color }) => { const c = useClaudeTokens(); return ; }; interface EmptyStateProps { icon?: React.ReactNode; title: string; hint?: string; /** Show after N ms — keeps "Loading..." flash off fast paths */ delayMs?: number; } export const EmptyState: React.FC = ({ icon, title, hint, delayMs = 100 }) => { const c = useClaudeTokens(); const [show, setShow] = useState(delayMs === 0); useEffect(() => { if (delayMs === 0) return; const t = setTimeout(() => setShow(true), delayMs); return () => clearTimeout(t); }, [delayMs]); if (!show) return null; return ( {icon && {icon}} {title} {hint && ( {hint} )} ); };