import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; // ============================================================================ // TYPES // ============================================================================ export type WidgetVariant = 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info' | 'indigo' | 'blue' | 'teal' | 'purple' | 'pink' | 'orange'; export interface StatWidgetProps { /** Widget title/label */ title: string; /** Main value to display */ value: string | number; /** Icon class name (e.g., 'icon-pointer') */ icon?: string; /** Icon element (alternative to icon class) */ iconElement?: React.ReactNode; /** Color variant */ variant?: WidgetVariant; /** Icon position */ iconPosition?: 'left' | 'right'; /** Whether to use solid background color */ solid?: boolean; /** Optional trend indicator */ trend?: { value: number; direction: 'up' | 'down'; label?: string; }; /** Click handler */ onClick?: () => void; /** Additional CSS class */ className?: string; } export interface ProgressWidgetProps { /** Widget title */ title: string; /** Subtitle/description */ subtitle?: string; /** Progress value (0-100) */ progress: number; /** Progress label */ progressLabel?: string; /** Progress sublabel */ progressSublabel?: string; /** Icon class name */ icon?: string; /** Icon element (alternative to icon class) */ iconElement?: React.ReactNode; /** Color variant */ variant?: WidgetVariant; /** Progress bar height */ progressHeight?: number; /** Additional CSS class */ className?: string; } export interface ChartWidgetProps { /** Widget title */ title?: string; /** Subtitle/description */ subtitle?: string; /** Chart type */ type: 'area' | 'bar' | 'line' | 'donut' | 'pie' | 'sparkline'; /** Chart data */ data: ChartDataPoint[]; /** Chart height in pixels */ height?: number; /** Color for the chart */ color?: string; /** Multiple colors for pie/donut charts */ colors?: string[]; /** Whether to animate on load */ animate?: boolean; /** Animation duration in ms */ animationDuration?: number; /** Show tooltip on hover */ showTooltip?: boolean; /** Tooltip formatter */ tooltipFormatter?: (value: number, label?: string) => string; /** Header content */ header?: React.ReactNode; /** Footer content */ footer?: React.ReactNode; /** Additional CSS class */ className?: string; } export interface ChartDataPoint { label?: string; value: number; date?: string | Date; color?: string; } export interface ContentWidgetProps { /** Widget title */ title?: string; /** Header actions */ headerActions?: React.ReactNode; /** Widget content */ children: React.ReactNode; /** Footer content */ footer?: React.ReactNode; /** Whether to remove padding */ noPadding?: boolean; /** Additional CSS class */ className?: string; } export interface UserWidgetProps { /** User name */ name: string; /** User role/title */ role?: string; /** User avatar URL */ avatar?: string; /** Background image or color */ background?: string; /** Background color variant */ variant?: WidgetVariant; /** User details */ details?: Array<{ label: string; value: string | React.ReactNode }>; /** Social links */ socialLinks?: Array<{ icon: string; href: string }>; /** Action buttons */ actions?: React.ReactNode; /** Additional CSS class */ className?: string; } export interface MessageListItem { id: string; avatar?: string; name: string; message: string; time: string; badge?: number; online?: boolean; } export interface MessageListWidgetProps { /** Widget title */ title?: string; /** Header extra content (e.g., trend indicator) */ headerExtra?: React.ReactNode; /** Messages to display */ messages: MessageListItem[]; /** Tab configuration for multiple views */ tabs?: Array<{ id: string; label: string; messages: MessageListItem[] }>; /** Chart to display above messages */ chart?: ChartWidgetProps; /** Message click handler */ onMessageClick?: (message: MessageListItem) => void; /** Additional CSS class */ className?: string; } // ============================================================================ // UTILITY HOOKS // ============================================================================ /** * Hook for responsive container width tracking */ export function useContainerWidth(ref: React.RefObject): number { const [width, setWidth] = useState(0); useEffect(() => { if (!ref.current) return; const observer = new ResizeObserver((entries) => { for (const entry of entries) { setWidth(entry.contentRect.width); } }); observer.observe(ref.current); setWidth(ref.current.getBoundingClientRect().width); return () => observer.disconnect(); }, [ref]); return width; } /** * Format large numbers with abbreviations */ export function formatNumber(num: number): string { if (num >= 1000000) { return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; } return num.toLocaleString(); } // ============================================================================ // STAT WIDGET // ============================================================================ export const StatWidget: React.FC = ({ title, value, icon, iconElement, variant = 'default', iconPosition = 'left', solid = false, trend, onClick, className = '', }) => { const variantClass = variant !== 'default' ? `ll-stat-widget--${variant}` : ''; const solidClass = solid ? 'll-stat-widget--solid' : ''; const clickableClass = onClick ? 'll-stat-widget--clickable' : ''; const positionClass = iconPosition === 'right' ? 'll-stat-widget--icon-right' : ''; const iconContent = iconElement || (icon && ); return (
{iconPosition === 'left' && iconContent && (
{iconContent}
)}

{typeof value === 'number' ? formatNumber(value) : value}

{title}
{iconPosition === 'right' && iconContent && (
{iconContent}
)}
{trend && (
{trend.value}% {trend.label && {trend.label}}
)}
); }; // ============================================================================ // PROGRESS WIDGET // ============================================================================ export const ProgressWidget: React.FC = ({ title, subtitle, progress, progressLabel, progressSublabel, icon, iconElement, variant = 'primary', progressHeight = 2, className = '', }) => { const variantClass = `ll-progress-widget--${variant}`; const iconContent = iconElement || (icon && ); return (
{title}
{subtitle && {subtitle}}
{iconContent &&
{iconContent}
}
{(progressLabel || progressSublabel) && (
{progressSublabel && {progressSublabel}} {progressLabel && {progressLabel}}
)}
); }; // ============================================================================ // CHART WIDGET - Pure React/SVG Charts (No jQuery) // ============================================================================ export const ChartWidget: React.FC = ({ title, subtitle, type, data, height = 200, color = '#26A69A', colors, animate = true, animationDuration = 1000, showTooltip = true, tooltipFormatter, header, footer, className = '', }) => { const containerRef = useRef(null); const width = useContainerWidth(containerRef); const [animationProgress, setAnimationProgress] = useState(animate ? 0 : 1); const [hoveredIndex, setHoveredIndex] = useState(null); const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); // Animation effect useEffect(() => { if (!animate) return; const startTime = Date.now(); let animationFrame: number; const updateAnimation = () => { const elapsed = Date.now() - startTime; const progress = Math.min(1, elapsed / animationDuration); setAnimationProgress(progress); if (progress < 1) { animationFrame = requestAnimationFrame(updateAnimation); } }; animationFrame = requestAnimationFrame(updateAnimation); return () => cancelAnimationFrame(animationFrame); }, [animate, animationDuration, data]); // Calculate scales const maxValue = useMemo(() => Math.max(...data.map((d) => d.value)), [data]); const minValue = useMemo(() => Math.min(...data.map((d) => d.value)), [data]); // Default tooltip formatter const defaultFormatter = useCallback( (value: number, label?: string) => `${label ? label + ': ' : ''}${formatNumber(value)}`, [] ); const formatter = tooltipFormatter || defaultFormatter; // Render different chart types const renderChart = () => { if (width === 0) return null; switch (type) { case 'area': return renderAreaChart(); case 'line': return renderLineChart(); case 'bar': return renderBarChart(); case 'sparkline': return renderSparklineChart(); case 'donut': case 'pie': return renderPieChart(); default: return null; } }; const renderAreaChart = () => { const padding = { top: 10, right: 10, bottom: 10, left: 10 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; const xScale = (i: number) => padding.left + (i / (data.length - 1)) * chartWidth; const yScale = (v: number) => padding.top + chartHeight - ((v - minValue) / (maxValue - minValue || 1)) * chartHeight; const pathData = data .map((d, i) => { const x = xScale(i); const y = yScale(d.value * animationProgress); return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; }) .join(' '); const areaPath = `${pathData} L ${xScale(data.length - 1)} ${height - padding.bottom} L ${padding.left} ${height - padding.bottom} Z`; return ( {showTooltip && data.map((d, i) => ( { setHoveredIndex(i); setTooltipPosition({ x: e.clientX, y: e.clientY }); }} onMouseLeave={() => setHoveredIndex(null)} /> ))} ); }; const renderLineChart = () => { const padding = { top: 10, right: 10, bottom: 10, left: 10 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; const xScale = (i: number) => padding.left + (i / (data.length - 1)) * chartWidth; const yScale = (v: number) => padding.top + chartHeight - ((v - minValue) / (maxValue - minValue || 1)) * chartHeight; const pathData = data .map((d, i) => { const x = xScale(i); const y = yScale(d.value * animationProgress); return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; }) .join(' '); return ( {showTooltip && data.map((d, i) => ( { setHoveredIndex(i); setTooltipPosition({ x: e.clientX, y: e.clientY }); }} onMouseLeave={() => setHoveredIndex(null)} /> ))} ); }; const renderBarChart = () => { const padding = { top: 10, right: 10, bottom: 10, left: 10 }; const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; const barGap = 0.3; const barWidth = (chartWidth / data.length) * (1 - barGap); const barSpacing = chartWidth / data.length; return ( {data.map((d, i) => { const barHeight = ((d.value / maxValue) * chartHeight * animationProgress); const x = padding.left + i * barSpacing + (barSpacing - barWidth) / 2; const y = padding.top + chartHeight - barHeight; return ( { setHoveredIndex(i); setTooltipPosition({ x: e.clientX, y: e.clientY }); }} onMouseLeave={() => setHoveredIndex(null)} /> ); })} ); }; const renderSparklineChart = () => { const chartHeight = height; const xScale = (i: number) => (i / (data.length - 1)) * width; const yScale = (v: number) => chartHeight - ((v - minValue) / (maxValue - minValue || 1)) * chartHeight; const pathData = data .map((d, i) => { const x = xScale(i); const y = yScale(d.value * animationProgress); return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; }) .join(' '); return ( ); }; const renderPieChart = () => { const centerX = width / 2; const centerY = height / 2; const radius = Math.min(centerX, centerY) - 10; const innerRadius = type === 'donut' ? radius * 0.6 : 0; const total = data.reduce((sum, d) => sum + d.value, 0); let currentAngle = -Math.PI / 2; const defaultColors = [ '#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4', '#FF5722', '#795548', '#607D8B', '#3F51B5', ]; const chartColors = colors || defaultColors; const slices = data.map((d, i) => { const sliceAngle = (d.value / total) * 2 * Math.PI * animationProgress; const startAngle = currentAngle; const endAngle = currentAngle + sliceAngle; currentAngle = endAngle; const x1 = centerX + radius * Math.cos(startAngle); const y1 = centerY + radius * Math.sin(startAngle); const x2 = centerX + radius * Math.cos(endAngle); const y2 = centerY + radius * Math.sin(endAngle); const ix1 = centerX + innerRadius * Math.cos(startAngle); const iy1 = centerY + innerRadius * Math.sin(startAngle); const ix2 = centerX + innerRadius * Math.cos(endAngle); const iy2 = centerY + innerRadius * Math.sin(endAngle); const largeArc = sliceAngle > Math.PI ? 1 : 0; const path = type === 'donut' ? `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L ${ix2} ${iy2} A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${ix1} ${iy1} Z` : `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`; return ( { setHoveredIndex(i); setTooltipPosition({ x: e.clientX, y: e.clientY }); }} onMouseLeave={() => setHoveredIndex(null)} /> ); }); return ( {slices} ); }; return (
{(title || subtitle || header) && (
{header || ( <> {title &&
{title}
} {subtitle && {subtitle}} )}
)}
{renderChart()} {showTooltip && hoveredIndex !== null && (
{formatter(data[hoveredIndex].value, data[hoveredIndex].label)}
)}
{footer &&
{footer}
}
); }; // ============================================================================ // CONTENT WIDGET // ============================================================================ export const ContentWidget: React.FC = ({ title, headerActions, children, footer, noPadding = false, className = '', }) => { return (
{(title || headerActions) && (
{title &&
{title}
} {headerActions &&
{headerActions}
}
)}
{children}
{footer &&
{footer}
}
); }; // ============================================================================ // USER WIDGET // ============================================================================ export const UserWidget: React.FC = ({ name, role, avatar, background, variant = 'primary', details, socialLinks, actions, className = '', }) => { const variantClass = `ll-user-widget--${variant}`; const hasBackground = background?.startsWith('http') || background?.startsWith('/') || background?.startsWith('data:'); return (
{avatar ? ( {name} ) : (
{name.charAt(0).toUpperCase()}
)} {actions &&
{actions}
}
{name}
{role && {role}} {socialLinks && socialLinks.length > 0 && (
{socialLinks.map((link, i) => ( ))}
)}
{details && details.length > 0 && (
{details.map((detail, i) => (
{detail.label} {detail.value}
))}
)}
); }; // ============================================================================ // MESSAGE LIST WIDGET // ============================================================================ export const MessageListWidget: React.FC = ({ title, headerExtra, messages, tabs, chart, onMessageClick, className = '', }) => { const [activeTab, setActiveTab] = useState(tabs?.[0]?.id || ''); const currentMessages = tabs ? tabs.find((t) => t.id === activeTab)?.messages || [] : messages; return (
{(title || headerExtra) && (
{title &&
{title}
} {headerExtra &&
{headerExtra}
}
)} {chart && (
)} {tabs && tabs.length > 0 && (
{tabs.map((tab) => ( ))}
)}
{currentMessages.map((msg) => (
onMessageClick?.(msg)} >
{msg.avatar ? ( {msg.name} ) : (
{msg.name.charAt(0).toUpperCase()}
)} {msg.badge !== undefined && msg.badge > 0 && ( {msg.badge} )} {msg.online !== undefined && ( )}
{msg.name} {msg.time}

{msg.message}

))}
); }; // ============================================================================ // QUICK STATS GRID // ============================================================================ export interface QuickStatsProps { stats: Array<{ label: string; value: string | number; icon?: string; variant?: WidgetVariant; trend?: { value: number; direction: 'up' | 'down' }; }>; columns?: 2 | 3 | 4; className?: string; } export const QuickStats: React.FC = ({ stats, columns = 4, className = '', }) => { return (
{stats.map((stat, i) => ( ))}
); }; // Default export export default { StatWidget, ProgressWidget, ChartWidget, ContentWidget, UserWidget, MessageListWidget, QuickStats, useContainerWidth, formatNumber, };