UNPKG

aura-glass

Version:

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

259 lines (256 loc) 7.99 kB
'use client'; import { useState, useRef, useCallback, useEffect } from 'react'; import { useEnhancedPerformance } from './useEnhancedPerformance.js'; /** * Enhanced intersection observer hook with performance optimizations */ function useGlassIntersection(options = {}) { const { threshold = 0.1, rootMargin = '0px', root = null, triggerOnce = false, skip = false, delay = 0 } = options; const [state, setState] = useState({ isIntersecting: false, intersectionRatio: 0, boundingClientRect: null, rootBounds: null, target: null, hasIntersected: false }); const ref = useRef(null); const observerRef = useRef(); const timeoutRef = useRef(); const { performanceMode } = useEnhancedPerformance(); // Performance-aware threshold adjustment const adaptiveThreshold = Array.isArray(threshold) ? threshold : performanceMode === 'low' ? Math.max(threshold, 0.25) // Higher threshold for low-end devices : threshold; // Performance-aware root margin adjustment const adaptiveRootMargin = performanceMode === 'low' ? '0px' // No preloading for low-end devices : rootMargin; const handleIntersection = useCallback(entries => { const entry = entries[0]; const updateState = () => { setState(prevState => { const newState = { isIntersecting: entry.isIntersecting, intersectionRatio: entry.intersectionRatio, boundingClientRect: entry.boundingClientRect, rootBounds: entry.rootBounds, target: entry.target, hasIntersected: prevState.hasIntersected || entry.isIntersecting }; return newState; }); // Disconnect observer if triggerOnce and has intersected if (triggerOnce && entry.isIntersecting && observerRef.current) { observerRef.current.disconnect(); } }; // Apply delay if specified if (delay > 0 && entry.isIntersecting) { timeoutRef.current = setTimeout(updateState, delay); } else { updateState(); } }, [triggerOnce, delay]); // Set up intersection observer useEffect(() => { if (skip || !ref.current) return; // Clean up existing observer if (observerRef.current) { observerRef.current.disconnect(); } // Create new observer try { observerRef.current = new IntersectionObserver(handleIntersection, { threshold: adaptiveThreshold, rootMargin: adaptiveRootMargin, root }); observerRef.current.observe(ref.current); } catch (error) { if (process.env.NODE_ENV === 'development') { console.warn('Failed to create IntersectionObserver:', error); } // Fallback: assume element is visible setState(prev => ({ ...prev, isIntersecting: true, hasIntersected: true })); } return () => { if (observerRef.current) { observerRef.current.disconnect(); } if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [skip, handleIntersection, adaptiveThreshold, adaptiveRootMargin, root]); // Cleanup on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return [ref, state]; } /** * Hook for lazy loading images with intersection observer */ function useGlassLazyImage(src, options = {}) { const { placeholder, lowQualitySrc, webpSrc, avifSrc, ...intersectionOptions } = options; const [elementRef, { isIntersecting, hasIntersected }] = useGlassIntersection({ triggerOnce: true, ...intersectionOptions }); const [currentSrc, setCurrentSrc] = useState(placeholder || lowQualitySrc || ''); const [isLoading, setIsLoading] = useState(false); const [isLoaded, setIsLoaded] = useState(false); const [error, setError] = useState(null); const { performanceMode, metrics } = useEnhancedPerformance(); const imageRef = useRef(null); // Determine best image format based on performance const getBestImageSrc = useCallback(() => { const networkSpeed = metrics?.networkSpeed || '4g'; // Use low quality for slow networks or low performance mode if (performanceMode === 'low' || networkSpeed === '2g' || networkSpeed === 'slow-2g') { return lowQualitySrc || src; } // Use modern formats for fast networks and high performance if (performanceMode === 'high' && (networkSpeed === '4g' || networkSpeed === '5g')) { if (avifSrc && CSS.supports('image-format', 'avif')) { return avifSrc; } if (webpSrc && CSS.supports('image-format', 'webp')) { return webpSrc; } } return src; }, [src, lowQualitySrc, webpSrc, avifSrc, performanceMode, metrics]); // Load image when intersecting useEffect(() => { if (!isIntersecting && !hasIntersected) return; const loadImage = async () => { setIsLoading(true); setError(null); try { const targetSrc = getBestImageSrc(); const img = new Image(); await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = () => reject(new Error('Image failed to load')); img.src = targetSrc; }); setCurrentSrc(targetSrc); setIsLoaded(true); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load image'); setCurrentSrc(src); // Fallback to original } finally { setIsLoading(false); } }; loadImage(); }, [isIntersecting, hasIntersected, getBestImageSrc, src]); return { ref: imageRef, currentSrc, isLoading, isLoaded, error }; } /** * Hook for animating elements on intersection */ function useGlassIntersectionAnimation(animationClass = 'animate-fade-in', options = {}) { const [ref, { isIntersecting, hasIntersected }] = useGlassIntersection({ threshold: 0.1, triggerOnce: true, ...options }); const shouldAnimate = isIntersecting || hasIntersected; useEffect(() => { const element = ref.current; if (!element) return; if (shouldAnimate) { element.classList.add(animationClass); } else { element.classList.remove(animationClass); } }, [shouldAnimate, animationClass]); return [ref, shouldAnimate]; } /** * Hook for progressive content loading */ function useProgressiveLoading(items, options = {}) { const { batchSize = 10, delay = 100, threshold = 0.8 } = options; const [loadedCount, setLoadedCount] = useState(batchSize); const [isLoading, setIsLoading] = useState(false); const { performanceMode } = useEnhancedPerformance(); // Adjust batch size based on performance const adaptiveBatchSize = performanceMode === 'low' ? Math.max(1, Math.floor(batchSize / 2)) : performanceMode === 'high' ? batchSize * 2 : batchSize; const visibleItems = items.slice(0, loadedCount); const loadNext = useCallback(async () => { if (isLoading || loadedCount >= items.length) return; setIsLoading(true); // Simulate loading delay for better UX if (delay > 0) { await new Promise(resolve => setTimeout(resolve, delay)); } setLoadedCount(prev => Math.min(items.length, prev + adaptiveBatchSize)); setIsLoading(false); }, [isLoading, loadedCount, items.length, delay, adaptiveBatchSize]); const loadAll = useCallback(() => { setLoadedCount(items.length); setIsLoading(false); }, [items.length]); // Auto-load more when scrolled near bottom const [scrollRef] = useGlassIntersection({ threshold: threshold, rootMargin: '100px' }); return { visibleItems, isLoading, loadedCount, totalCount: items.length, loadNext, loadAll, scrollRef }; } export { useGlassIntersection, useGlassIntersectionAnimation, useGlassLazyImage, useProgressiveLoading }; //# sourceMappingURL=useGlassIntersection.js.map