UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

447 lines (444 loc) 16.8 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { forwardRef, useState, useRef, useEffect, useMemo, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const GlassMetricsGrid = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for table cells, list items, badges, card titles, and other text content for WCAG AA compliance metrics, layout = { columns: 4, gap: 16 }, loading = false, animated = true, showTrends = true, showSparks = true, autoRefresh = false, refreshInterval = 30000, onRefresh, onMetricClick, renderMetric, filters, sort, searchable = false, searchQuery = "", onSearchChange, exportable = false, onExport, respectMotionPreference = true, className, ...props }, ref) => { const { prefersReducedMotion } = useMotionPreferenceContext(); const { play, feedback } = useGlassSound(); const metricsGridId = useA11yId("glass-metrics-grid"); const [internalSearchQuery, setInternalSearchQuery] = useState(searchQuery); const refreshIntervalRef = useRef(null); // Auto-refresh effect useEffect(() => { if (autoRefresh && onRefresh && refreshInterval > 0) { refreshIntervalRef.current = setInterval(() => { onRefresh(); feedback("notification"); }, refreshInterval); return () => { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); } }; } }, [autoRefresh, onRefresh, refreshInterval, feedback]); // Filter and search metrics const filteredMetrics = useMemo(() => { let result = [...metrics]; // Apply search if (internalSearchQuery.trim()) { const query = internalSearchQuery.toLowerCase(); result = result.filter(metric => metric.title.toLowerCase().includes(query) || metric.description?.toLowerCase().includes(query) || metric.category?.toLowerCase().includes(query)); } // Apply filters if (filters) { if (filters.categories && filters.categories.length > 0) { result = result.filter(metric => metric.category && filters.categories.includes(metric.category)); } if (filters.priorities && filters.priorities.length > 0) { result = result.filter(metric => metric.priority && filters.priorities.includes(metric.priority)); } if (filters.statuses && filters.statuses.length > 0) { result = result.filter(metric => metric.status && filters.statuses.includes(metric.status)); } } // Apply sorting if (sort) { result.sort((a, b) => { const aValue = a[sort.field]; const bValue = b[sort.field]; const direction = sort.direction === "asc" ? 1 : -1; // Handle null/undefined values if (aValue == null && bValue == null) return 0; if (aValue == null) return direction; if (bValue == null) return -direction; if (aValue < bValue) return -1 * direction; if (aValue > bValue) return 1 * direction; return 0; }); } return result; }, [metrics, internalSearchQuery, filters, sort]); // Status colors const statusColors = { success: "border-green-500/30 bg-green-500/5", warning: "border-yellow-500/30 bg-yellow-500/5", error: "border-red-500/30 bg-red-500/5", info: "border-blue-500/30 bg-blue-500/5", neutral: "border-border/30 bg-background/5" }; // Priority indicators const priorityIndicators = { low: "w-1 h-1", medium: "w-1.5 h-1.5", high: "w-2 h-2", critical: "w-2.5 h-2.5 animate-pulse" }; // Format metric value const formatValue = useCallback(value => { if (value.customFormatter) { return value.customFormatter(value.current); } const num = value.current; const unit = value.unit || ""; switch (value.format) { case "percentage": return `${num.toFixed(1)}%`; case "currency": return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(num); case "bytes": const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(num) / Math.log(1024)); return `${(num / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; case "time": const hours = Math.floor(num / 3600); const minutes = Math.floor(num % 3600 / 60); const seconds = Math.floor(num % 60); if (hours > 0) return `${hours}h ${minutes}m`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; default: return new Intl.NumberFormat().format(num) + (unit ? ` ${unit}` : ""); } }, []); // Calculate trend const calculateTrend = useCallback(metric => { if (!metric.trend || !showTrends) return null; const { direction, percentage, period } = metric.trend; const isPositive = direction === "up"; const isNegative = direction === "down"; return jsxs("div", { className: cn("flex items-center glass-gap-1 glass-text-xs", isPositive && "text-green-600", isNegative && "text-red-600", direction === "neutral" && "glass-text-secondary"), children: [jsx("span", { className: "glass-text-base", children: isPositive ? "↗" : isNegative ? "↘" : "→" }), jsxs("span", { children: [percentage.toFixed(1), "%"] }), period && jsxs("span", { className: "glass-text-secondary", children: ["vs ", period] })] }); }, [showTrends]); // Render spark line const renderSparkLine = useCallback(spark => { if (!showSparks) return null; const max = Math.max(...spark.data); const min = Math.min(...spark.data); const range = max - min || 1; const points = spark.data.map((value, index) => { const x = index / (spark.data.length - 1) * 100; const y = (max - value) / range * 100; return `${x},${y}`; }).join(" "); return jsx("div", { className: 'relative glass-w-full h-8 glass-mt-2', children: jsxs("svg", { className: 'absolute inset-0 glass-w-full glass-h-full', viewBox: "0 0 100 100", preserveAspectRatio: "none", children: [spark.showArea && jsx("polygon", { points: `0,100 ${points} 100,100`, fill: "currentColor", className: 'opacity-10', style: { color: spark.color || "currentColor" } }), jsx("polyline", { points: points, fill: "none", stroke: "currentColor", strokeWidth: "2", className: 'transition-all duration-300', style: { color: spark.color || "currentColor" } })] }) }); }, [showSparks]); // Default metric renderer const defaultRenderMetric = useCallback(metric => { const sizeConfig = { sm: { padding: "glass-p-3", title: "glass-text-sm", value: "glass-text-lg", description: "glass-text-xs" }, md: { padding: "glass-p-4", title: "glass-text-sm", value: "glass-text-xl", description: "glass-text-xs" }, lg: { padding: "glass-p-6", title: "glass-text-base", value: "glass-text-2xl", description: "glass-text-sm" }, xl: { padding: "p-8", title: "glass-text-lg", value: "text-3xl", description: "glass-text-base" } }; const config = sizeConfig[metric.size || "md"]; return jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 2, tint: "neutral", border: "subtle", className: cn("glass-metric-card relative transition-all duration-300", "glass-backdrop-blur-md border border-border/20 glass-radius-lg", config.padding, metric.status && statusColors[metric.status], metric.clickable && "cursor-pointer hover:scale-[1.02] hover:shadow-lg", loading && "animate-pulse opacity-50"), onClick: () => { if (metric.clickable && metric.onClick) { metric.onClick(); feedback("tap"); } else if (onMetricClick) { onMetricClick(metric); feedback("tap"); } }, role: metric.clickable ? "button" : "article", "aria-label": `Metric: ${metric.title}`, tabIndex: metric.clickable ? 0 : -1, children: [metric.priority && jsx("div", { className: cn("absolute top-2 right-2 glass-radius-full bg-current opacity-60", priorityIndicators[metric.priority]), style: { color: metric.color || "currentColor" } }), jsx("div", { className: 'glass-flex glass-items-start glass-justify-between mb-2', children: jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsxs("div", { className: 'glass-flex glass-items-center glass-gap-2 mb-1', children: [metric.icon && jsx("div", { className: "glass-flex-shrink-0", style: { color: metric.color || "currentColor" }, children: metric.icon }), jsx("h3", { className: cn("font-semibold text-foreground truncate", config.title), children: metric.title })] }), metric.description && jsx("p", { className: cn("glass-text-secondary line-clamp-2", config.description), children: metric.description })] }) }), jsx("div", { className: cn("font-bold text-foreground glass-mb-2", config.value), children: loading ? "---" : formatValue(metric.value) }), metric.value.target && jsxs("div", { className: 'mb-2', children: [jsxs("div", { className: 'glass-flex glass-justify-between glass-text-xs glass-text-secondary mb-1', children: [jsxs("span", { children: ["Target:", " ", formatValue({ ...metric.value, current: metric.value.target })] }), jsxs("span", { children: [(metric.value.current / metric.value.target * 100).toFixed(0), "%"] })] }), jsx("div", { className: 'glass-w-full glass-surface-overlay glass-radius-full h-2', children: jsx("div", { className: 'glass-surface-primary h-2 glass-radius-full transition-all duration-500', style: { width: `${Math.min(metric.value.current / metric.value.target * 100, 100)}%` } }) })] }), metric.trend && calculateTrend(metric), metric.spark && renderSparkLine(metric.spark), metric.customContent && jsx("div", { className: "glass-mt-2", children: metric.customContent }), metric.category && jsx("div", { className: 'absolute bottom-2 right-2', children: jsx("span", { className: "glass-px-2 glass-py-1 glass-text-xs glass-surface-overlay glass-text-secondary glass-radius-full", children: metric.category }) })] }); }, [loading, formatValue, calculateTrend, renderSparkLine, onMetricClick, feedback, statusColors, priorityIndicators]); // Handle search const handleSearch = useCallback(query => { setInternalSearchQuery(query); onSearchChange?.(query); }, [onSearchChange]); // Manual refresh const handleRefresh = useCallback(() => { if (onRefresh) { onRefresh(); feedback("notification"); } }, [onRefresh, feedback]); // Grid styles const gridStyle = useMemo(() => { const { columns, gap, responsive } = layout; return { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: `${gap}px`, ...(responsive && { "@media (max-width: 640px)": { gridTemplateColumns: `repeat(${responsive.sm || 1}, 1fr)` }, "@media (max-width: 768px)": { gridTemplateColumns: `repeat(${responsive.md || 2}, 1fr)` }, "@media (max-width: 1024px)": { gridTemplateColumns: `repeat(${responsive.lg || 3}, 1fr)` }, "@media (min-width: 1280px)": { gridTemplateColumns: `repeat(${responsive.xl || columns}, 1fr)` } }) }; }, [layout]); return jsx(OptimizedGlassCore, { ref: ref, id: metricsGridId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-metrics-grid glass-radius-lg glass-backdrop-blur-md border border-border/20", className), ...props, children: jsxs(MotionFramer, { preset: !prefersReducedMotion && respectMotionPreference && animated ? "fadeIn" : "none", className: "glass-p-6", children: [(searchable || exportable || autoRefresh) && jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-6', children: [jsx("div", { className: "glass-flex glass-items-center glass-gap-4", children: searchable && jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: 'relative', children: [jsx("input", { type: "text", placeholder: "Search metrics...", value: internalSearchQuery, onChange: e => handleSearch(e.target.value), className: cn("w-64 glass-px-4 glass-py-2 bg-transparent border-0 glass-radius-md", "placeholder:glass-text-secondary focus:outline-none focus:ring-2 focus:ring-primary/50", "glass-text-sm") }), jsx("div", { className: 'absolute right-3 glass-top-1/2 -translate-y-1/2 glass-text-secondary', children: "\uD83D\uDD0D" })] }) }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [autoRefresh && jsx(GlassButton, { onClick: handleRefresh, elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-px-3 glass-py-2 glass-text-sm transition-all", "hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary/50"), children: "\uD83D\uDD04 Refresh" }), exportable && onExport && jsx(GlassButton, { onClick: () => onExport("json"), elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-px-3 glass-py-2 glass-text-sm transition-all", "hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary/50"), children: "\uD83D\uDCCA Export" })] })] }), filteredMetrics.length === 0 ? jsx("div", { className: 'glass-flex glass-items-center glass-justify-center h-64 text-center', children: jsxs("div", { children: [jsx("div", { className: 'glass-text-4xl mb-4', children: "\uD83D\uDCCA" }), jsx("h3", { className: 'glass-text-lg font-semibold text-primary mb-2', children: "No Metrics Found" }), jsx("p", { className: "glass-text-secondary", children: internalSearchQuery ? "Try adjusting your search query" : "No metrics to display" })] }) }) : jsx("div", { className: "glass-grid", style: gridStyle, children: filteredMetrics.map((metric, index) => jsx(MotionFramer, { preset: !prefersReducedMotion && respectMotionPreference && animated ? "slideUp" : "none", delay: index * 100, children: renderMetric ? renderMetric(metric) : defaultRenderMetric(metric) }, metric.id)) })] }) }); }); GlassMetricsGrid.displayName = "GlassMetricsGrid"; export { GlassMetricsGrid, GlassMetricsGrid as default }; //# sourceMappingURL=GlassMetricsGrid.js.map