UNPKG

aura-glass

Version:

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

510 lines (507 loc) 16.5 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useContext, forwardRef, useState, useRef, useEffect, createContext } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId, announceToScreenReader } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; const MotionContext = /*#__PURE__*/createContext(undefined); const useMotionController = () => { const context = useContext(MotionContext); if (!context) { throw new Error('useMotionController must be used within a GlassMotionController'); } return context; }; // Easing functions const easings = { 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, bounce: t => { if (t < 1 / 2.75) return 7.5625 * t * t; if (t < 2 / 2.75) return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; if (t < 2.5 / 2.75) return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; }, elastic: t => { if (t === 0) return 0; if (t === 1) return 1; return -Math.pow(2, -10 * t) * Math.sin((t - 0.1) * 5 * Math.PI) + 1; } }; const GlassMotionController = /*#__PURE__*/forwardRef(({ enabled = true, speed = 1, reduceMotion = false, respectMotionPreference = true, children, className, 'aria-label': ariaLabel, ...props }, ref) => { const [isAnimating, setIsAnimating] = useState(false); const motionPreference = useMotionPreferenceContext(); const controllerId = useA11yId('motion-controller'); // Determine if motion should be reduced const shouldReduceMotion = respectMotionPreference ? reduceMotion || motionPreference.prefersReducedMotion : reduceMotion; const animate = async (element, config) => { if (!enabled || shouldReduceMotion) { // Still apply end state for reduced motion if (shouldReduceMotion) { applyEndState(element, config.type, config.direction || 'center'); announceToScreenReader(`Animation ${config.type} applied instantly due to reduced motion preference`, 'polite'); } return; } setIsAnimating(true); announceToScreenReader(`Starting ${config.type} animation`, 'polite'); return new Promise(resolve => { const { type, direction = 'center', duration = 1000, delay = 0, easing = 'easeOut', repeat = 0, yoyo = false, amplitude = 1, frequency = 1 } = config; const actualDuration = duration * speed; let startTime; let repeatCount = 0; const animateFrame = timestamp => { if (!startTime) startTime = timestamp; if (timestamp - startTime < delay) { requestAnimationFrame(animateFrame); return; } const elapsed = timestamp - startTime - delay; const progress = Math.min(elapsed / actualDuration, 1); const easedProgress = easings[easing](progress); applyAnimation(element, type, direction, easedProgress, amplitude, frequency); if (progress < 1) { requestAnimationFrame(animateFrame); } else { repeatCount++; if (repeatCount <= repeat) { startTime = timestamp; if (yoyo) { // For yoyo effect, we need to reverse the animation const reverseAnimateFrame = createReverseAnimationFrame(); requestAnimationFrame(reverseAnimateFrame); return; } } else { setIsAnimating(false); announceToScreenReader(`Animation ${config.type} completed`, 'polite'); resolve(); } } }; requestAnimationFrame(animateFrame); }); }; const batchAnimate = async animations => { if (!enabled || shouldReduceMotion) { // Apply end states for reduced motion if (shouldReduceMotion) { animations.forEach(({ element, config }) => { applyEndState(element, config.type, config.direction || 'center'); }); announceToScreenReader(`${animations.length} animations applied instantly due to reduced motion preference`, 'polite'); } return; } setIsAnimating(true); announceToScreenReader(`Starting batch animation of ${animations.length} elements`, 'polite'); const promises = animations.map(({ element, config }) => animate(element, config)); await Promise.all(promises); setIsAnimating(false); announceToScreenReader('Batch animation completed', 'polite'); }; const createReverseAnimationFrame = (element, type, direction, duration, easing, amplitude, frequency) => { return timestamp => { // Reverse animation logic would go here // For simplicity, we'll just run the normal animation again }; }; const applyAnimation = (element, type, direction, progress, amplitude, frequency) => { const styles = {}; switch (type) { case 'fadeIn': case 'fadeOut': styles.opacity = type === 'fadeIn' ? progress.toString() : (1 - progress).toString(); break; case 'slideIn': case 'slideOut': const slideDistance = 100; const slideProgress = type === 'slideIn' ? 1 - progress : progress; switch (direction) { case 'up': styles.transform = `translateY(${slideDistance * slideProgress}px)`; break; case 'down': styles.transform = `translateY(-${slideDistance * slideProgress}px)`; break; case 'left': styles.transform = `translateX(${slideDistance * slideProgress}px)`; break; case 'right': styles.transform = `translateX(-${slideDistance * slideProgress}px)`; break; } break; case 'scaleIn': case 'scaleOut': const scaleStart = type === 'scaleIn' ? 0 : 1; const scaleEnd = type === 'scaleIn' ? 1 : 0; const scaleProgress = scaleStart + (scaleEnd - scaleStart) * progress; styles.transform = `scale(${scaleProgress})`; break; case 'bounce': const bounceHeight = amplitude * 50; const bounceProgress = easings.bounce(progress); styles.transform = `translateY(${bounceHeight * (1 - bounceProgress)}px)`; break; case 'shake': const shakeIntensity = amplitude * 10; const shakeOffset = Math.sin(progress * frequency * Math.PI * 2) * shakeIntensity; styles.transform = `translateX(${shakeOffset}px)`; break; case 'pulse': const pulseScale = 1 + Math.sin(progress * frequency * Math.PI * 2) * amplitude * 0.1; styles.transform = `scale(${pulseScale})`; break; case 'rotate': const rotation = progress * 360 * frequency; styles.transform = `rotate(${rotation}deg)`; break; case 'flip': const flipRotation = progress * 180; styles.transform = `rotateY(${flipRotation}deg)`; break; } // Apply styles Object.assign(element.style, styles); }; // Apply end state without animation for reduced motion const applyEndState = (element, type, direction) => { const styles = {}; switch (type) { case 'fadeIn': styles.opacity = '1'; break; case 'fadeOut': styles.opacity = '0'; break; case 'slideIn': case 'scaleIn': styles.transform = 'none'; styles.opacity = '1'; break; case 'slideOut': case 'scaleOut': styles.opacity = '0'; break; default: // For other animations, ensure element is visible styles.opacity = '1'; styles.transform = 'none'; break; } Object.assign(element.style, styles); }; return jsx("div", { ref: ref, className: className, id: controllerId, "aria-label": ariaLabel || (isAnimating ? 'Animation in progress' : 'Animation controller'), "aria-busy": isAnimating, role: "region", ...props, children: jsx(MotionContext.Provider, { value: { enabled: enabled && !shouldReduceMotion, speed, reduceMotion: shouldReduceMotion, animate, batchAnimate }, children: children }) }); }); GlassMotionController.displayName = 'GlassMotionController'; const GlassAnimated = /*#__PURE__*/forwardRef(({ animation, children, className = '', trigger = 'mount', respectMotionPreference = true, 'aria-label': ariaLabel, ...props }, ref) => { const { animate, enabled, reduceMotion } = useMotionController(); const elementRef = useRef(null); const [hasAnimated, setHasAnimated] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const motionPreference = useMotionPreferenceContext(); const animatedId = useA11yId('animated'); // Combine refs const combinedRef = node => { elementRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }; const shouldReduceMotion = respectMotionPreference ? reduceMotion || motionPreference.prefersReducedMotion : reduceMotion; useEffect(() => { if (!enabled || !animation || !elementRef.current || hasAnimated) return; if (trigger === 'mount') { setIsAnimating(true); animate(elementRef.current, animation).then(() => { setHasAnimated(true); setIsAnimating(false); }).catch(() => { setIsAnimating(false); }); } }, [animate, animation, enabled, trigger, hasAnimated]); const handleTrigger = async () => { if (!enabled || !animation || !elementRef.current) return; if (trigger === 'click') { setIsAnimating(true); try { await animate(elementRef.current, animation); } finally { setIsAnimating(false); } } }; const handleHover = async () => { if (!enabled || !animation || !elementRef.current || trigger !== 'hover') return; setIsAnimating(true); try { await animate(elementRef.current, animation); } finally { setIsAnimating(false); } }; const handleKeyDown = event => { if (trigger === 'click' && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); handleTrigger(); } }; return jsxs("div", { ref: combinedRef, id: animatedId, className: className, onClick: trigger === 'click' ? handleTrigger : undefined, onMouseEnter: trigger === 'hover' ? handleHover : undefined, onKeyDown: trigger === 'click' ? handleKeyDown : undefined, tabIndex: trigger === 'click' ? 0 : undefined, role: trigger === 'click' ? 'button' : undefined, "aria-label": ariaLabel || (trigger === 'click' ? 'Animated interactive element' : undefined), "aria-busy": isAnimating, "aria-describedby": shouldReduceMotion ? `${animatedId}-motion-notice` : undefined, ...props, children: [children, shouldReduceMotion && jsx("div", { id: `${animatedId}-motion-notice`, className: cn("glass-sr-only"), children: "Motion animations are disabled due to accessibility preferences" })] }); }); GlassAnimated.displayName = 'GlassAnimated'; const GlassAnimationSequence = /*#__PURE__*/forwardRef(({ children, staggerDelay = 100, className = '', respectMotionPreference = true, 'aria-label': ariaLabel, ...props }, ref) => { const { batchAnimate, enabled, reduceMotion } = useMotionController(); const containerRef = useRef(null); const [isAnimating, setIsAnimating] = useState(false); const motionPreference = useMotionPreferenceContext(); const sequenceId = useA11yId('animation-sequence'); // Combine refs const combinedRef = node => { containerRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }; const shouldReduceMotion = respectMotionPreference ? reduceMotion || motionPreference.prefersReducedMotion : reduceMotion; useEffect(() => { if (!enabled || !containerRef.current) return; const elements = containerRef.current.children; const animations = Array.from(elements).map((element, index) => ({ element: element, config: { type: 'fadeIn', direction: 'up', duration: 600, delay: shouldReduceMotion ? 0 : index * staggerDelay, easing: 'easeOut' } })); setIsAnimating(true); batchAnimate(animations).then(() => setIsAnimating(false)).catch(() => setIsAnimating(false)); }, [batchAnimate, enabled, staggerDelay, shouldReduceMotion]); return jsxs("div", { ref: combinedRef, id: sequenceId, className: className, role: "region", "aria-label": ariaLabel || 'Animation sequence', "aria-busy": isAnimating, "aria-describedby": shouldReduceMotion ? `${sequenceId}-motion-notice` : undefined, ...props, children: [children, shouldReduceMotion && jsx("div", { id: `${sequenceId}-motion-notice`, className: cn("glass-sr-only"), children: "Sequential animations are disabled due to accessibility preferences" })] }); }); GlassAnimationSequence.displayName = 'GlassAnimationSequence'; // Preset animations const animationPresets = { fadeInUp: { type: 'fadeIn', direction: 'up', duration: 600, easing: 'easeOut' }, fadeInDown: { type: 'fadeIn', direction: 'down', duration: 600, easing: 'easeOut' }, slideInLeft: { type: 'slideIn', direction: 'left', duration: 500, easing: 'easeOut' }, slideInRight: { type: 'slideIn', direction: 'right', duration: 500, easing: 'easeOut' }, scaleIn: { type: 'scaleIn', duration: 400, easing: 'easeOut' }, bounceIn: { type: 'bounce', duration: 800, easing: 'bounce' }, shake: { type: 'shake', duration: 500, amplitude: 1, frequency: 5 }, pulse: { type: 'pulse', duration: 1000, amplitude: 1, frequency: 2, repeat: Infinity } }; const GlassAnimationTimeline = /*#__PURE__*/forwardRef(({ timeline, children, className = '', respectMotionPreference = true, 'aria-label': ariaLabel, ...props }, ref) => { const { animate, enabled, reduceMotion } = useMotionController(); const containerRef = useRef(null); const [isAnimating, setIsAnimating] = useState(false); const motionPreference = useMotionPreferenceContext(); const timelineId = useA11yId('animation-timeline'); // Combine refs const combinedRef = node => { containerRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }; const shouldReduceMotion = respectMotionPreference ? reduceMotion || motionPreference.prefersReducedMotion : reduceMotion; useEffect(() => { if (!enabled || !containerRef.current) return; setIsAnimating(true); let animationPromises = []; timeline.forEach(({ selector, animation, startTime = 0 }) => { const element = containerRef.current?.querySelector(selector); if (element) { const promise = new Promise(resolve => { setTimeout(() => { animate(element, animation).then(resolve).catch(resolve); }, shouldReduceMotion ? 0 : startTime); }); animationPromises.push(promise); } }); Promise.all(animationPromises).then(() => setIsAnimating(false)).catch(() => setIsAnimating(false)); }, [animate, enabled, timeline, shouldReduceMotion]); return jsxs("div", { ref: combinedRef, id: timelineId, className: className, role: "region", "aria-label": ariaLabel || 'Animation timeline', "aria-busy": isAnimating, "aria-describedby": shouldReduceMotion ? `${timelineId}-motion-notice` : undefined, ...props, children: [children, shouldReduceMotion && jsx("div", { id: `${timelineId}-motion-notice`, className: cn("glass-sr-only"), children: "Timeline animations are disabled due to accessibility preferences" })] }); }); GlassAnimationTimeline.displayName = 'GlassAnimationTimeline'; export { GlassAnimated, GlassAnimationSequence, GlassAnimationTimeline, GlassMotionController, animationPresets, useMotionController }; //# sourceMappingURL=GlassMotionController.js.map