aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
447 lines (444 loc) • 16.8 kB
JavaScript
'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