/** * v3.15: Componentes UI McKinsey * * Componentes base reutilizables que implementan el sistema de diseƱo. * Usar estos componentes en lugar de crear estilos ad-hoc. */ import React from 'react'; import { TrendingUp, TrendingDown, Minus, ChevronRight, ChevronDown, ChevronUp, } from 'lucide-react'; import { cn, CARD_BASE, SECTION_HEADER, BADGE_BASE, BADGE_SIZES, METRIC_BASE, STATUS_CLASSES, TIER_CLASSES, SPACING, } from '../../config/designSystem'; // ============================================ // CARD // ============================================ interface CardProps { children: React.ReactNode; variant?: 'default' | 'highlight' | 'muted'; padding?: 'sm' | 'md' | 'lg' | 'none'; className?: string; } export function Card({ children, variant = 'default', padding = 'md', className, }: CardProps) { return (
{children}
); } // Card con indicador de status (borde superior) interface StatusCardProps extends CardProps { status: 'critical' | 'warning' | 'success' | 'info' | 'neutral'; } export function StatusCard({ status, children, className, ...props }: StatusCardProps) { const statusClasses = STATUS_CLASSES[status]; return ( {children} ); } // ============================================ // SECTION HEADER // ============================================ interface SectionHeaderProps { title: string; subtitle?: string; badge?: BadgeProps; action?: React.ReactNode; level?: 2 | 3 | 4; className?: string; noBorder?: boolean; } export function SectionHeader({ title, subtitle, badge, action, level = 2, className, noBorder = false, }: SectionHeaderProps) { const Tag = `h${level}` as keyof JSX.IntrinsicElements; const titleClass = level === 2 ? SECTION_HEADER.title.h2 : level === 3 ? SECTION_HEADER.title.h3 : SECTION_HEADER.title.h4; return (
{title} {badge && }
{subtitle && (

{subtitle}

)}
{action &&
{action}
}
); } // ============================================ // BADGE // ============================================ interface BadgeProps { label: string | number; variant?: 'default' | 'success' | 'warning' | 'critical' | 'info'; size?: 'sm' | 'md'; className?: string; } export function Badge({ label, variant = 'default', size = 'sm', className, }: BadgeProps) { const variantClasses = { default: 'bg-gray-100 text-gray-700', success: 'bg-emerald-50 text-emerald-700', warning: 'bg-amber-50 text-amber-700', critical: 'bg-red-50 text-red-700', info: 'bg-blue-50 text-blue-700', }; return ( {label} ); } // Badge para Tiers interface TierBadgeProps { tier: 'AUTOMATE' | 'ASSIST' | 'AUGMENT' | 'HUMAN-ONLY'; size?: 'sm' | 'md'; className?: string; } export function TierBadge({ tier, size = 'sm', className }: TierBadgeProps) { const tierClasses = TIER_CLASSES[tier]; return ( {tier} ); } // ============================================ // METRIC // ============================================ interface MetricProps { label: string; value: string | number; unit?: string; status?: 'success' | 'warning' | 'critical'; comparison?: string; trend?: 'up' | 'down' | 'neutral'; size?: 'sm' | 'md' | 'lg' | 'xl'; className?: string; } export function Metric({ label, value, unit, status, comparison, trend, size = 'md', className, }: MetricProps) { const valueColorClass = !status ? 'text-gray-900' : status === 'success' ? 'text-emerald-600' : status === 'warning' ? 'text-amber-600' : 'text-red-600'; return (
{label}
{value} {unit && {unit}} {trend && }
{comparison && ( {comparison} )}
); } // Indicador de tendencia function TrendIndicator({ direction }: { direction: 'up' | 'down' | 'neutral' }) { if (direction === 'up') { return ; } if (direction === 'down') { return ; } return ; } // ============================================ // KPI CARD (Metric in a card) // ============================================ interface KPICardProps extends MetricProps { icon?: React.ReactNode; } export function KPICard({ icon, ...metricProps }: KPICardProps) { return ( {icon && (
{icon}
)}
); } // ============================================ // STAT (inline stat for summaries) // ============================================ interface StatProps { value: string | number; label: string; status?: 'success' | 'warning' | 'critical'; className?: string; } export function Stat({ value, label, status, className }: StatProps) { const statusClasses = STATUS_CLASSES[status || 'neutral']; return (

{value}

{label}

); } // ============================================ // DIVIDER // ============================================ export function Divider({ className }: { className?: string }) { return
; } // ============================================ // COLLAPSIBLE SECTION // ============================================ interface CollapsibleProps { title: string; subtitle?: string; badge?: BadgeProps; defaultOpen?: boolean; children: React.ReactNode; className?: string; } export function Collapsible({ title, subtitle, badge, defaultOpen = false, children, className, }: CollapsibleProps) { const [isOpen, setIsOpen] = React.useState(defaultOpen); return (
{isOpen && (
{children}
)}
); } // ============================================ // DISTRIBUTION BAR // ============================================ interface DistributionBarProps { segments: Array<{ value: number; color: string; label?: string; }>; total?: number; height?: 'sm' | 'md' | 'lg'; showLabels?: boolean; className?: string; } export function DistributionBar({ segments, total, height = 'md', showLabels = false, className, }: DistributionBarProps) { const computedTotal = total || segments.reduce((sum, s) => sum + s.value, 0); const heightClass = height === 'sm' ? 'h-2' : height === 'md' ? 'h-3' : 'h-4'; return (
{segments.map((segment, idx) => { const pct = computedTotal > 0 ? (segment.value / computedTotal) * 100 : 0; if (pct <= 0) return null; return (
{showLabels && pct >= 10 && ( {pct.toFixed(0)}% )}
); })}
); } // ============================================ // TABLE COMPONENTS // ============================================ export function Table({ children, className, }: { children: React.ReactNode; className?: string; }) { return (
{children}
); } export function Thead({ children }: { children: React.ReactNode }) { return ( {children} ); } export function Th({ children, align = 'left', className, }: { children: React.ReactNode; align?: 'left' | 'right' | 'center'; className?: string; }) { return ( {children} ); } export function Tbody({ children }: { children: React.ReactNode }) { return {children}; } export function Tr({ children, highlighted, className, }: { children: React.ReactNode; highlighted?: boolean; className?: string; }) { return ( {children} ); } export function Td({ children, align = 'left', className, }: { children: React.ReactNode; align?: 'left' | 'right' | 'center'; className?: string; }) { return ( {children} ); } // ============================================ // EMPTY STATE // ============================================ interface EmptyStateProps { icon?: React.ReactNode; title: string; description?: string; action?: React.ReactNode; } export function EmptyState({ icon, title, description, action }: EmptyStateProps) { return (
{icon &&
{icon}
}

{title}

{description && (

{description}

)} {action &&
{action}
}
); } // ============================================ // BUTTON // ============================================ interface ButtonProps { children: React.ReactNode; variant?: 'primary' | 'secondary' | 'ghost'; size?: 'sm' | 'md'; onClick?: () => void; disabled?: boolean; className?: string; } export function Button({ children, variant = 'primary', size = 'md', onClick, disabled, className, }: ButtonProps) { const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors'; const variantClasses = { primary: 'bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-300', secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 disabled:bg-gray-100', ghost: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100', }; const sizeClasses = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-sm', }; return ( ); }