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