UNPKG

aura-glass

Version:

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

410 lines (407 loc) 16.6 kB
'use client'; import { jsxs } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useCallback, useEffect } from 'react'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { cn } from '../../lib/utilsComprehensive.js'; 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'; import { useGlassTheme } from '../../hooks/useGlassTheme.js'; // Helper to convert to CSS dimension const toCssDimension = value => { if (value === undefined) return '0'; return typeof value === 'number' ? `${value}px` : value; }; // Helper to map blur strength to elevation level const mapBlurToElevation = blurStrength => { if (blurStrength <= 8) return 'level1'; if (blurStrength <= 12) return 'level2'; if (blurStrength <= 16) return 'level3'; return 'level4'; }; // Helper to map opacity to glass intensity const mapOpacityToIntensity = opacity => { if (opacity <= 0.15) return 'subtle'; if (opacity <= 0.25) return 'medium'; if (opacity <= 0.35) return 'strong'; return 'intense'; }; /** * ContextAwareGlass Component * * A glass container that adapts its appearance based on content and background. * Now optimized with OptimizedGlass architecture for better performance. */ const ContextAwareGlass = /*#__PURE__*/forwardRef((props, ref) => { const { children, adaptationMode = 'auto', contentType, backgroundType, baseBlurStrength = 10, baseOpacity = 0.15, baseBorderOpacity = 0.2, enableEdgeHighlight = true, enableGlow = false, glowColor = '#6366F1', // Primary color enableContinuousAdaptation = false, adaptationInterval = 1000, onAdaptationChange, backgroundElementId, borderRadius = '8px', className, padding = '16px', debug = false, forceLightMode = false, forceDarkMode = false, minBlurStrength = 5, maxBlurStrength = 20, minOpacity = 0.1, maxOpacity = 0.4, ...rest } = props; // Get theme information const { theme } = useGlassTheme(); const themeIsDarkMode = theme === 'dark'; const isDarkMode = forceDarkMode || themeIsDarkMode && !forceLightMode; const prefersReducedMotion = useReducedMotion(); // Refs and state const containerRef = useRef(null); const adaptationTimerRef = useRef(null); // Default state for glass settings and detection const [detectedContentType, setDetectedContentType] = useState(contentType || 'unknown'); const [detectedBackgroundType, setDetectedBackgroundType] = useState(backgroundType || 'unknown'); const [blurStrength, setBlurStrength] = useState(baseBlurStrength); const [opacity, setOpacity] = useState(baseOpacity); const [borderOpacity, setBorderOpacity] = useState(baseBorderOpacity); // Convert borderRadius and padding to CSS dimensions const borderRadiusValue = toCssDimension(borderRadius); const paddingValue = toCssDimension(padding); // Function to detect content type const detectContentType = useCallback(() => { if (contentType) return contentType; if (!containerRef.current) return 'unknown'; const container = containerRef.current; // Check if container has form elements const hasFormElements = container.querySelectorAll('input, select, textarea, button').length > 0; if (hasFormElements) return 'form'; // Check if container has images const hasImages = container.querySelectorAll('img, svg, canvas').length > 0; // Check if container has video or audio elements const hasMultimedia = container.querySelectorAll('video, audio, iframe').length > 0; if (hasMultimedia) return 'multimedia'; // Check if container has interactive elements const hasInteractive = container.querySelectorAll('button, a, [role="button"]').length > 0; // Check text content length const textContent = container.textContent || ''; const hasSignificantText = textContent.length > 50; // Determine content type based on findings if (hasImages && hasSignificantText) return 'mixed'; if (hasImages) return 'image'; if (hasInteractive) return 'interactive'; if (hasSignificantText) return 'text'; // Default to unknown if can't determine return 'unknown'; }, [contentType, containerRef]); // Function to detect background type const detectBackgroundType = useCallback(() => { if (backgroundType) return backgroundType; let background = null; // Try to get specified background element if (backgroundElementId) { background = document.getElementById(backgroundElementId); } // If no specified element or not found, use parent of container if (!background && containerRef.current) { background = containerRef.current.parentElement; } if (!background) return 'unknown'; // Get computed style of background element const style = window.getComputedStyle(background); // Check for background image or gradient const backgroundImage = style.backgroundImage; if (backgroundImage && backgroundImage !== 'none') { if (backgroundImage.includes('gradient')) return 'gradient'; if (backgroundImage.includes('url')) return 'image'; return 'pattern'; } // Check if element is or contains a video if (background.tagName === 'VIDEO' || background.querySelector('video')) { return 'video'; } // Check background color for light/dark const backgroundColor = style.backgroundColor; if (backgroundColor && backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)') { // Parse the color to determine if it's light or dark const isLight = isLightColor(backgroundColor); return isLight ? 'light' : 'dark'; } // If we have multiple backgrounds or complex setup if (style.background && style.background !== style.backgroundColor) { return 'complex'; } return 'unknown'; }, [backgroundType, backgroundElementId, containerRef]); // Function to determine if a color is light or dark const isLightColor = color => { // Handle rgba/rgb format let r = 0, g = 0, b = 0; if (color.startsWith('rgba') || color.startsWith('rgb')) { const match = color.match(/(\d+),\s*(\d+),\s*(\d+)/); if (match) { r = parseInt(match[1], 10); g = parseInt(match[2], 10); b = parseInt(match[3], 10); } } else if (color.startsWith('#')) { // Handle hex format const hex = color.substring(1); r = parseInt(hex.substr(0, 2), 16); g = parseInt(hex.substr(2, 2), 16); b = parseInt(hex.substr(4, 2), 16); } // Calculate relative luminance const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; // Return true if luminance is greater than 0.5 (light color) return luminance > 0.5; }; // Function to adjust glass settings based on content and background const adjustGlassSettings = useCallback(() => { const currentContentType = detectContentType(); const currentBackgroundType = detectBackgroundType(); setDetectedContentType(currentContentType); setDetectedBackgroundType(currentBackgroundType); // Helper function to clamp a value between min and max const clamp = (value, min, max) => Math.min(Math.max(value, min), max); // Calculate adjusted values based on adaptation mode and detected types let adjustedBlurStrength = baseBlurStrength; let adjustedOpacity = baseOpacity; let adjustedBorderOpacity = baseBorderOpacity; switch (adaptationMode) { case 'content': // Adjust based on content type switch (currentContentType) { case 'text': // For text increase opacity and decrease blur for better readability adjustedBlurStrength = baseBlurStrength * 0.8; adjustedOpacity = baseOpacity * 1.2; break; case 'image': // For images decrease opacity and increase blur for better visibility adjustedBlurStrength = baseBlurStrength * 1.2; adjustedOpacity = baseOpacity * 0.8; break; case 'interactive': // For interactive elements make more visible adjustedBlurStrength = baseBlurStrength; adjustedOpacity = baseOpacity * 1.1; adjustedBorderOpacity = baseBorderOpacity * 1.2; break; case 'form': // For forms maximize readability and contrast adjustedBlurStrength = baseBlurStrength * 0.9; adjustedOpacity = baseOpacity * 1.3; adjustedBorderOpacity = baseBorderOpacity * 1.3; break; case 'multimedia': // For multimedia maximize transparency adjustedBlurStrength = baseBlurStrength * 1.3; adjustedOpacity = baseOpacity * 0.7; break; } break; case 'background': // Adjust based on background type switch (currentBackgroundType) { case 'light': // For light backgrounds decrease opacity in light mode, increase in dark mode adjustedOpacity = isDarkMode ? baseOpacity * 1.2 : baseOpacity * 0.9; break; case 'dark': // For dark backgrounds increase opacity in light mode, decrease in dark mode adjustedOpacity = isDarkMode ? baseOpacity * 0.9 : baseOpacity * 1.2; break; case 'image': case 'complex': case 'pattern': // For busy backgrounds increase blur and opacity for better isolation adjustedBlurStrength = baseBlurStrength * 1.4; adjustedOpacity = baseOpacity * 1.3; break; case 'video': // For video backgrounds maximize blur for better isolation adjustedBlurStrength = baseBlurStrength * 1.6; adjustedOpacity = baseOpacity * 1.2; break; case 'gradient': // For gradients moderate adjustments adjustedBlurStrength = baseBlurStrength * 1.2; adjustedOpacity = baseOpacity * 1.1; break; } break; case 'hybrid': // Combine both content and background adaptations // First apply content-based adjustments switch (currentContentType) { case 'text': adjustedBlurStrength = baseBlurStrength * 0.8; adjustedOpacity = baseOpacity * 1.1; break; case 'image': adjustedBlurStrength = baseBlurStrength * 1.1; adjustedOpacity = baseOpacity * 0.9; break; } // Then apply background-based adjustments switch (currentBackgroundType) { case 'image': case 'complex': case 'video': adjustedBlurStrength *= 1.2; adjustedOpacity *= 1.1; break; case 'light': if (isDarkMode) adjustedOpacity *= 1.1; break; case 'dark': if (!isDarkMode) adjustedOpacity *= 1.1; break; } break; case 'contrast': // Prioritize contrast and readability adjustedBlurStrength = baseBlurStrength * 0.7; adjustedOpacity = baseOpacity * 1.4; adjustedBorderOpacity = baseBorderOpacity * 1.5; break; case 'immersive': // Prioritize immersion and aesthetic adjustedBlurStrength = baseBlurStrength * 1.5; adjustedOpacity = baseOpacity * 0.8; break; case 'fixed': // Use base values without adaptation break; case 'dynamic': case 'auto': default: // For auto/dynamic use a combination of approaches based on content and background if (currentContentType === 'text' || currentContentType === 'form') { // Prioritize readability for text/forms adjustedBlurStrength = baseBlurStrength * 0.8; adjustedOpacity = baseOpacity * 1.2; } else if (currentContentType === 'image' || currentContentType === 'multimedia') { // Prioritize visibility for images/multimedia adjustedBlurStrength = baseBlurStrength * 1.2; adjustedOpacity = baseOpacity * 0.9; } // Further adjust based on background if (currentBackgroundType === 'complex' || currentBackgroundType === 'video') { adjustedBlurStrength *= 1.3; adjustedOpacity *= 1.2; } else if (currentBackgroundType === 'light' && isDarkMode) { adjustedOpacity *= 1.2; } else if (currentBackgroundType === 'dark' && !isDarkMode) { adjustedOpacity *= 1.2; } break; } // For reduced motion preferences, use more subtle effects if (prefersReducedMotion) { adjustedBlurStrength = Math.min(adjustedBlurStrength, baseBlurStrength); } // Clamp final values within valid ranges const finalBlurStrength = clamp(adjustedBlurStrength, minBlurStrength, maxBlurStrength); const finalOpacity = clamp(adjustedOpacity, minOpacity, maxOpacity); const finalBorderOpacity = clamp(adjustedBorderOpacity, 0.1, 0.5); // Update state with adjusted values setBlurStrength(finalBlurStrength); setOpacity(finalOpacity); setBorderOpacity(finalBorderOpacity); // Call onAdaptationChange callback if provided if (onAdaptationChange) { onAdaptationChange({ blurStrength: finalBlurStrength, opacity: finalOpacity, borderOpacity: finalBorderOpacity, contentType: currentContentType, backgroundType: currentBackgroundType }); } }, [adaptationMode, baseBlurStrength, baseOpacity, baseBorderOpacity, detectContentType, detectBackgroundType, isDarkMode, prefersReducedMotion, minBlurStrength, maxBlurStrength, minOpacity, maxOpacity, onAdaptationChange]); // Effect to adjust settings on mount and when dependencies change useEffect(() => { adjustGlassSettings(); }, [adaptationMode, contentType, backgroundType, baseBlurStrength, baseOpacity, baseBorderOpacity, isDarkMode, adjustGlassSettings]); // Effect for continuous adaptation useEffect(() => { if (enableContinuousAdaptation && adaptationMode !== 'fixed') { // Set up periodic check adaptationTimerRef.current = setInterval(() => { adjustGlassSettings(); }, adaptationInterval); // Clean up on unmount return () => { if (adaptationTimerRef.current) { clearInterval(adaptationTimerRef.current); } }; } }, [enableContinuousAdaptation, adaptationMode, adaptationInterval, adjustGlassSettings]); // Map values to OptimizedGlass props const elevation = mapBlurToElevation(blurStrength); const intensity = mapOpacityToIntensity(opacity); return jsxs(OptimizedGlassCore, { ref: node => { containerRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref && 'current' in ref) { ref.current = node; } }, elevation: elevation, intensity: intensity, intent: "neutral", tier: "high", interactive: enableEdgeHighlight, glow: enableGlow, glowColor: glowColor, className: cn('relative overflow-hidden', className), style: { borderRadius: borderRadiusValue, padding: paddingValue, ...(enableGlow && !prefersReducedMotion && { filter: `drop-shadow(0 0 20px ${glowColor})` }), ...(enableEdgeHighlight && { boxShadow: isDarkMode ? '0 0 20px var(--glass-bg-default)' : '0 0 20px rgba(var(--glass-color-black) / var(--glass-opacity-10))' }) }, ...rest, children: [debug && jsxs("div", { className: cn('absolute top-0 right-0 z-10 pointer-events-none', 'glass-text-xs glass-p-1 rounded-bl font-mono', isDarkMode ? 'bg-black/70 glass-text-primary' : 'bg-white/70 text-black'), children: [jsxs("div", { children: ["Content: ", detectedContentType] }), jsxs("div", { children: ["Background: ", detectedBackgroundType] }), jsxs("div", { children: ["Blur: ", blurStrength.toFixed(1), "px"] }), jsxs("div", { children: ["Opacity: ", opacity.toFixed(2)] })] }), children] }); }); ContextAwareGlass.displayName = 'ContextAwareGlass'; export { ContextAwareGlass, ContextAwareGlass as default }; //# sourceMappingURL=ContextAwareGlass.js.map