UNPKG

aura-glass

Version:

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

431 lines (428 loc) 17.1 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useMemo, useCallback } from 'react'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; 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 { useA11yId } from '../../utils/a11y.js'; const GlassHeatmap = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for table cells, list items, badges, card titles, and other text content for WCAG AA compliance data: incomingData = [], xAxis, yAxis, colorScale = { min: "var(--glass-color-primary)", max: "var(--glass-color-danger)" }, cellSize = 20, cellGap = 1, showValues = false, showGrid = true, showTooltips = true, selectable = false, selectedCells: incomingSelectedCells = [], onSelectionChange, onCellClick, onCellHover, renderCell, renderTooltip, zoomable = false, zoomLevel = 1, onZoomChange, showLegend = true, legendPosition = "right", animated = true, animationDuration = 500, respectMotionPreference = true, className, ...props }, ref) => { const data = Array.isArray(incomingData) ? incomingData : []; const selectedCells = Array.isArray(incomingSelectedCells) ? incomingSelectedCells : []; const { prefersReducedMotion } = useMotionPreferenceContext(); const heatmapId = useA11yId("glass-heatmap"); const [hoveredCell, setHoveredCell] = useState(null); const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); const [internalZoomLevel, setInternalZoomLevel] = useState(zoomLevel); const containerRef = useRef(null); const heatmapRef = useRef(null); // Process data into normalized format const processedData = useMemo(() => { let cells = []; let minValue = Infinity; let maxValue = -Infinity; if (!Array.isArray(data) || data.length === 0) { return { cells: [], minValue: 0, maxValue: 0, range: 1 }; } const firstEntry = data[0]; if (Array.isArray(firstEntry)) { // Handle 2D array format const matrix = data; matrix.forEach((row, rowIndex) => { (Array.isArray(row) ? row : []).forEach((value, colIndex) => { minValue = Math.min(minValue, value); maxValue = Math.max(maxValue, value); cells.push({ row: rowIndex, col: colIndex, value, normalizedValue: 0 // Will be calculated after we know min/max }); }); }); } else { // Handle data points format const points = data; points.forEach(point => { if (typeof point?.value !== "number") return; minValue = Math.min(minValue, point.value); maxValue = Math.max(maxValue, point.value); cells.push({ row: point.y ?? 0, col: point.x ?? 0, value: point.value, normalizedValue: 0, // Will be calculated after we know min/max label: point.label, metadata: point.metadata }); }); } if (cells.length === 0) { return { cells: [], minValue: 0, maxValue: 0, range: 1 }; } // Normalize values const range = maxValue - minValue || 1; cells = cells.map(cell => ({ ...cell, normalizedValue: (cell.value - minValue) / range })); return { cells, minValue, maxValue, range }; }, [data]); const hasCells = processedData.cells.length > 0; // Get grid dimensions const gridDimensions = useMemo(() => { if (!processedData.cells.length) { return { rows: 0, cols: 0 }; } const maxRow = Math.max(...processedData.cells.map(c => c.row)) + 1; const maxCol = Math.max(...processedData.cells.map(c => c.col)) + 1; return { rows: maxRow, cols: maxCol }; }, [processedData.cells]); // Generate color from normalized value const getColor = useCallback(normalizedValue => { const { min, mid, max, steps = 100 } = colorScale; if (mid) { // Three-color gradient if (normalizedValue <= 0.5) { return interpolateColor(min, mid, normalizedValue * 2); } else { return interpolateColor(mid, max, (normalizedValue - 0.5) * 2); } } else { // Two-color gradient return interpolateColor(min, max, normalizedValue); } }, [colorScale]); // Color interpolation utility const interpolateColor = useCallback((color1, color2, factor) => { const hex1 = color1.replace("#", ""); const hex2 = color2.replace("#", ""); const r1 = parseInt(hex1.substr(0, 2), 16); const g1 = parseInt(hex1.substr(2, 2), 16); const b1 = parseInt(hex1.substr(4, 2), 16); const r2 = parseInt(hex2.substr(0, 2), 16); const g2 = parseInt(hex2.substr(2, 2), 16); const b2 = parseInt(hex2.substr(4, 2), 16); const r = Math.round(r1 + factor * (r2 - r1)); const g = Math.round(g1 + factor * (g2 - g1)); const b = Math.round(b1 + factor * (b2 - b1)); return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; }, []); // Handle cell interactions const handleCellClick = useCallback((cell, event) => { if (selectable) { const cellId = { row: cell.row, col: cell.col }; const isSelected = selectedCells.some(c => c.row === cell.row && c.col === cell.col); let newSelection; if (event.ctrlKey || event.metaKey) { // Multi-select newSelection = isSelected ? selectedCells.filter(c => !(c.row === cell.row && c.col === cell.col)) : [...selectedCells, cellId]; } else { // Single select newSelection = isSelected ? [] : [cellId]; } onSelectionChange?.(newSelection); } onCellClick?.(cell); }, [selectable, selectedCells, onSelectionChange, onCellClick]); const handleCellHover = useCallback((cell, event) => { setHoveredCell(cell); onCellHover?.(cell); if (cell && event && showTooltips) { const rect = containerRef.current?.getBoundingClientRect(); if (rect) { setTooltipPosition({ x: event.clientX - rect.left, y: event.clientY - rect.top }); } } }, [onCellHover, showTooltips]); // Handle zoom const handleZoom = useCallback((delta, event) => { if (!zoomable) return; event.preventDefault(); const newZoom = Math.max(0.5, Math.min(3, internalZoomLevel + delta)); setInternalZoomLevel(newZoom); onZoomChange?.(newZoom); }, [zoomable, internalZoomLevel, onZoomChange]); // Check if cell is selected const isCellSelected = useCallback((row, col) => { return selectedCells.some(c => c.row === row && c.col === col); }, [selectedCells]); // Default cell renderer const defaultRenderCell = useCallback(cell => { const isSelected = isCellSelected(cell.row, cell.col); const isHovered = hoveredCell?.row === cell.row && hoveredCell?.col === cell.col; const cellColor = getColor(cell.normalizedValue); return jsx(OptimizedGlassCore, { elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-heatmap-cell flex items-center justify-center glass-text-xs font-medium transition-all", "cursor-pointer hover:scale-110 hover:z-10", isSelected && "ring-2 ring-primary ring-offset-1", isHovered && "shadow-lg scale-110 z-20", showGrid && "border border-border/20"), style: { backgroundColor: cellColor, color: cell.normalizedValue > 0.5 ? "var(--glass-white)" : "var(--glass-black)", width: cellSize * internalZoomLevel, height: cellSize * internalZoomLevel, fontSize: `${Math.max(8, cellSize * internalZoomLevel * 0.4)}px` }, onClick: e => handleCellClick(cell, e), onMouseEnter: e => handleCellHover(cell, e), onMouseLeave: () => handleCellHover(null), children: showValues && jsx("span", { className: 'select-none', children: typeof cell.value === "number" ? cell.value.toFixed(1) : cell.value }) }); }, [isCellSelected, hoveredCell, getColor, cellSize, internalZoomLevel, showGrid, showValues, handleCellClick, handleCellHover]); // Default tooltip renderer const defaultRenderTooltip = useCallback(cell => { return jsx(OptimizedGlassCore, { elevation: "level3", intensity: "strong", depth: 3, tint: "neutral", border: "strong", className: "glass-heatmap-tooltip glass-p-3 glass-radius-lg glass-shadow-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard", children: jsxs("div", { className: "glass-text-sm glass-gap-1", children: [jsx("div", { className: 'font-semibold', children: cell.label || `Cell (${cell.col}, ${cell.row})` }), jsxs("div", { children: ["Value: ", cell.value] }), xAxis?.labels?.[cell.col] && jsxs("div", { children: ["X: ", xAxis.labels[cell.col]] }), yAxis?.labels?.[cell.row] && jsxs("div", { children: ["Y: ", yAxis.labels[cell.row]] }), cell.metadata && Object.entries(cell.metadata).map(([key, value]) => jsxs("div", { children: [key, ": ", String(value)] }, key))] }) }); }, [xAxis, yAxis]); // Render legend const renderLegend = useCallback(() => { if (!showLegend) return null; const legendSteps = 20; const isHorizontal = legendPosition === "top" || legendPosition === "bottom"; return jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-heatmap-legend glass-p-3 glass-radius-lg glass-backdrop-blur-md border border-border/20", isHorizontal ? "flex items-center glass-gap-3" : "flex flex-col glass-gap-3"), children: [jsx("div", { className: 'glass-text-sm font-medium text-primary', children: "Legend" }), jsxs("div", { className: cn("flex", isHorizontal ? "flex-row items-center glass-gap-1" : "flex-col glass-gap-1"), children: [jsx("div", { className: "glass-text-xs glass-text-secondary", children: processedData.minValue.toFixed(1) }), jsx("div", { className: cn("flex", isHorizontal ? "flex-row" : "flex-col"), children: Array.from({ length: legendSteps }, (_, i) => jsx("div", { className: cn("flex-1", isHorizontal ? "w-3 h-6" : "w-6 h-3"), style: { backgroundColor: getColor(i / (legendSteps - 1)) } }, i)) }), jsx("div", { className: "glass-text-xs glass-text-secondary", children: processedData.maxValue.toFixed(1) })] })] }); }, [showLegend, legendPosition, processedData, getColor]); // Create grid matrix for rendering const gridMatrix = useMemo(() => { const matrix = Array(gridDimensions.rows).fill(null).map(() => Array(gridDimensions.cols).fill(null)); processedData.cells.forEach(cell => { if (cell.row < gridDimensions.rows && cell.col < gridDimensions.cols) { matrix[cell.row][cell.col] = cell; } }); return matrix; }, [processedData.cells, gridDimensions]); return jsx(OptimizedGlassCore, { ref: ref, id: heatmapId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-heatmap glass-radius-lg glass-backdrop-blur-md border border-border/20 overflow-hidden", className), ...props, children: jsxs(MotionFramer, { preset: !prefersReducedMotion && respectMotionPreference && animated ? "fadeIn" : "none", className: 'relative', children: [jsxs("div", { ref: containerRef, className: cn("flex", legendPosition === "left" && "flex-row-reverse", legendPosition === "right" && "flex-row", legendPosition === "top" && "flex-col-reverse", legendPosition === "bottom" && "flex-col"), children: [showLegend && jsx("div", { className: "glass-flex-shrink-0 glass-p-4", children: renderLegend() }), jsx("div", { className: 'glass-flex-1 glass-p-6 overflow-auto', children: !hasCells ? jsx("div", { className: "glass-text-sm glass-text-secondary text-center glass-p-10", children: "No heatmap data available." }) : jsxs("div", { className: "glass-flex", children: [yAxis && jsxs("div", { className: "glass-flex glass-flex-col glass-justify-between glass-mr-2", children: [yAxis.title && jsx("div", { className: 'glass-text-sm font-medium text-primary mb-2 writing-mode-vertical-lr transform rotate-180', children: yAxis.title }), jsx("div", { className: "glass-flex glass-flex-col glass-justify-between glass-h-full", children: yAxis.labels?.map((label, index) => jsx("div", { className: 'glass-text-xs glass-text-secondary text-right pr-2', children: label }, index)) })] }), jsxs("div", { ref: heatmapRef, className: 'relative', onWheel: e => handleZoom(e.deltaY > 0 ? -0.1 : 0.1, e), children: [xAxis && jsxs("div", { className: 'mb-2', children: [xAxis.title && jsx("div", { className: 'glass-text-sm font-medium text-primary text-center mb-2', children: xAxis.title }), jsx("div", { className: "glass-flex glass-justify-between", style: { width: (cellSize * internalZoomLevel + cellGap) * gridDimensions.cols - cellGap }, children: xAxis.labels?.map((label, index) => jsx("div", { className: 'glass-text-xs glass-text-secondary text-center', children: label }, index)) })] }), jsx("div", { className: "glass-grid glass-gap-px", style: { gridTemplateColumns: `repeat(${gridDimensions.cols}, ${cellSize * internalZoomLevel}px)`, gridTemplateRows: `repeat(${gridDimensions.rows}, ${cellSize * internalZoomLevel}px)`, gap: `${cellGap}px` }, children: gridMatrix.map((row, rowIndex) => row.map((cell, colIndex) => jsx(MotionFramer, { preset: !prefersReducedMotion && respectMotionPreference && animated ? "scaleIn" : "none", delay: animated ? (rowIndex * gridDimensions.cols + colIndex) * 10 : 0, children: cell ? renderCell ? renderCell(cell) : defaultRenderCell(cell) : jsx("div", { className: "glass-surface-overlay glass-border glass-border-dashed glass-border-glass-border/30", style: { width: cellSize * internalZoomLevel, height: cellSize * internalZoomLevel } }) }, `${rowIndex}-${colIndex}`))) })] })] }) })] }), showTooltips && hoveredCell && jsx("div", { className: 'absolute pointer-events-none z-50', style: { left: tooltipPosition.x + 10, top: tooltipPosition.y - 10, transform: "translateY(-100%)" }, children: renderTooltip ? renderTooltip(hoveredCell) : defaultRenderTooltip(hoveredCell) }), zoomable && jsxs("div", { className: 'absolute top-4 right-4 glass-flex glass-flex-col glass-gap-1', children: [jsx("button", { onClick: () => handleZoom(0.1, {}), className: 'w-8 h-8 glass-flex glass-items-center glass-justify-center glass-radius-md glass-text-sm font-bold transition-all hover:scale-105 glass-focus glass-touch-target glass-contrast-guard', children: "+" }), jsx("button", { onClick: () => handleZoom(-0.1, {}), className: 'w-8 h-8 glass-flex glass-items-center glass-justify-center glass-radius-md glass-text-sm font-bold transition-all hover:scale-105 glass-focus glass-touch-target glass-contrast-guard', children: "\u2212" })] })] }) }); }); GlassHeatmap.displayName = "GlassHeatmap"; export { GlassHeatmap, GlassHeatmap as default }; //# sourceMappingURL=GlassHeatmap.js.map