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