UNPKG

aura-glass

Version:

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

241 lines (238 loc) 7.69 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useImperativeHandle, useEffect } 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 '../../primitives/motion/MotionFramer.js'; // Easing functions const easingFunctions = { linear: t => t, easeIn: t => t * t, easeOut: t => t * (2 - t), easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t }; const GlassAnimatedNumber = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for table cells, list items, badges, card titles, and other text content for WCAG AA compliance value = 0, from = 0, duration = 1000, easing = 'easeOut', decimals = 0, separator = false, prefix = '', suffix = '', formatter, animateOnChange = true, className = '', size = 'md', variant = 'count' }, ref) => { const [displayValue, setDisplayValue] = useState(from); const [isAnimating, setIsAnimating] = useState(false); const animationRef = useRef(); const startTimeRef = useRef(); const startValueRef = useRef(from); const elementRef = useRef(null); // Forward ref useImperativeHandle(ref, () => elementRef.current); const sizeClasses = { sm: 'glass-text-lg', md: 'glass-text-2xl', lg: 'glass-text-4xl', xl: 'glass-text-6xl' }; const sizeClassName = sizeClasses[size] ?? sizeClasses.md; // Format number with separators and decimals const formatNumber = num => { if (formatter) { return formatter(num); } let formatted = num.toFixed(decimals); if (separator) { // Add comma separators for thousands formatted = formatted.replace(/\B(?=(\d{3})+(?!\d))/g, ','); } return `${prefix}${formatted}${suffix}`; }; // Animation loop const animate = timestamp => { if (!startTimeRef.current) { startTimeRef.current = timestamp; } const elapsed = timestamp - startTimeRef.current; const progress = Math.min(elapsed / duration, 1); const easedProgress = easingFunctions[easing](progress); const currentValue = startValueRef.current + (value - startValueRef.current) * easedProgress; setDisplayValue(currentValue); if (progress < 1) { animationRef.current = requestAnimationFrame(animate); } else { setDisplayValue(value); setIsAnimating(false); } }; // Start animation when value changes useEffect(() => { if (!animateOnChange || value === startValueRef.current) { setDisplayValue(value); return; } setIsAnimating(true); startValueRef.current = displayValue; startTimeRef.current = undefined; // Cancel any existing animation if (animationRef.current) { cancelAnimationFrame(animationRef.current); } // Start new animation animationRef.current = requestAnimationFrame(animate); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [value, animateOnChange]); // Cleanup on unmount useEffect(() => { return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, []); const getVariantStyles = () => { switch (variant) { case 'scale': return { transform: isAnimating ? 'scale(1.1)' : 'scale(1)', transition: 'transform 0.2s ease' }; case 'glow': return { textShadow: isAnimating ? '0 0 20px var(--glass-border-hover), 0 0 40px ${glassStyles.borderColor || "var(--glass-bg-hover)"}' : 'none', transition: 'text-shadow 0.3s ease' }; case 'count': default: return {}; } }; return jsx(OptimizedGlassCore, { "data-glass-component": true, ref: elementRef, className: cn('glass-inline-flex glass-items-center glass-justify-center glass-font-mono glass-font-bold glass-text-primary', sizeClassName, className), style: getVariantStyles(), elevation: "level1", interactive: false, children: jsx("span", { className: cn('glass-tabular-nums'), children: formatNumber(displayValue) }) }); }); GlassAnimatedNumber.displayName = 'GlassAnimatedNumber'; // Compound component for animated counter with label const GlassAnimatedCounter = ({ value, label, from = 0, duration = 1500, size = 'lg', className = '' }) => { return jsxs("div", { className: cn('glass-flex glass-flex-col glass-items-center glass-gap-2', className), children: [jsx(GlassAnimatedNumber, { value: value, from: from, duration: duration, size: size, variant: "scale", separator: true }), label && jsx(OptimizedGlassCore, { className: cn('glass-text-sm glass-text-primary-70 glass-font-medium'), elevation: "level1", children: label })] }); }; // Compound component for animated progress/stat const GlassAnimatedStat = ({ value, total, label, showPercentage = false, duration = 1000, className = '' }) => { const percentage = total ? value / total * 100 : value; return jsxs("div", { className: cn('glass-flex glass-flex-col glass-gap-2', className), children: [jsxs("div", { className: cn('glass-flex glass-items-baseline glass-gap-2'), children: [jsx(GlassAnimatedNumber, { value: value, duration: duration, size: "lg", separator: true }), showPercentage && total && jsx(GlassAnimatedNumber, { value: percentage, duration: duration, decimals: 1, suffix: "%", size: "md", className: cn('glass-text-primary-80') })] }), label && jsx(OptimizedGlassCore, { className: cn('glass-text-sm glass-text-primary-70'), elevation: "level1", children: label }), total && jsx(OptimizedGlassCore, { className: cn('glass-h-2 glass-w-full glass-radius-full glass-overflow-hidden'), elevation: "level1", children: jsx("div", { className: cn('glass-h-full glass-bg-gradient-to-r glass-from-blue-500 glass-to-purple-500 glass-radius-full glass-transition-all glass-duration-1000 glass-ease-out'), style: { width: `${percentage}%` } }) })] }); }; // Utility hook for managing animated numbers const useAnimatedNumber = (targetValue, options = {}) => { const [currentValue, setCurrentValue] = useState(targetValue); const [isAnimating, setIsAnimating] = useState(false); const animateTo = newValue => { if (newValue === currentValue) return; setIsAnimating(true); const startValue = currentValue; const startTime = Date.now(); const duration = options.duration || 1000; const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easedProgress = easingFunctions[options.easing || 'easeOut'](progress); const current = startValue + (newValue - startValue) * easedProgress; setCurrentValue(current); if (progress < 1) { requestAnimationFrame(animate); } else { setCurrentValue(newValue); setIsAnimating(false); } }; requestAnimationFrame(animate); }; return { value: currentValue, isAnimating, animateTo }; }; export { GlassAnimatedCounter, GlassAnimatedNumber, GlassAnimatedStat, useAnimatedNumber }; //# sourceMappingURL=GlassAnimatedNumber.js.map