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}
)}
);
};