UNPKG

aura-glass

Version:

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

361 lines (358 loc) 14.5 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useMemo, useCallback, useEffect } from 'react'; import { useReducedMotion } from '../hooks/useReducedMotion.js'; import { cn } from '../lib/utilsComprehensive.js'; import { LIQUID_GLASS, liquidGlassUtils } from '../tokens/glass.js'; import { useContrastGuard, contrastGuard } from '../utils/contrastGuard.js'; /** * LiquidGlassMaterial - Core primitive for Apple Liquid Glass parity * * Features: * - IOR-based refraction physics * - Environmental content awareness * - Motion-responsive effects * - Automatic contrast enforcement * - GPU-accelerated rendering with fallbacks * - Performance-optimized across devices */ const LiquidGlassMaterial = /*#__PURE__*/forwardRef(({ as: asProp = "div", material = "liquid", variant = "regular", intent = "neutral", elevation = "level2", ior, thickness, sheen, tintMode = "adaptive", adaptToContent = true, adaptToMotion = true, contrastLevel = "AA", performanceLevel = "high", enableRefraction, enableReflection, enableParallax, enableMicroInteractions, tint, quality, environmentAdaptation, motionResponsive, className, radius = "lg", interactive = false, disabled = false, children, onContrastAdjustment, onBackdropAnalysis, contentProps, style, ...props }, ref) => { const elementRef = useRef(null); const [isHovered, setIsHovered] = useState(false); const [isPressed, setIsPressed] = useState(false); const [deviceTilt, setDeviceTilt] = useState({ x: 0, y: 0 }); const [backdropSample, setBackdropSample] = useState(null); const prefersReducedMotion = useReducedMotion(); const supportsAdvancedRendering = useMemo(() => { if (typeof document === "undefined") return true; try { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); return !!context; } catch { return false; } }, []); const qualityTier = quality || performanceLevel; const isMotionResponsiveEnabled = (motionResponsive ?? adaptToMotion) && !prefersReducedMotion; const isEnvironmentAdaptive = environmentAdaptation ?? adaptToContent; // Merge refs const combinedRef = useCallback(node => { elementRef.current = node; if (typeof ref === "function") { ref(node); } else if (ref && "current" in ref) { ref.current = node; } }, [ref]); // Get performance settings const performanceConfig = useMemo(() => LIQUID_GLASS.performance[performanceLevel], [performanceLevel]); // Determine effective feature flags based on performance and props const effectiveFlags = useMemo(() => { const allowAdvanced = supportsAdvancedRendering; return { refraction: allowAdvanced && (enableRefraction ?? (performanceConfig.enableRefraction && material === "liquid")), reflection: allowAdvanced && (enableReflection ?? (performanceConfig.enableReflection && material === "liquid")), parallax: allowAdvanced && (enableParallax ?? (performanceConfig.enableParallax && material === "liquid")), microInteractions: enableMicroInteractions ?? (performanceConfig.enableMicroInteractions && interactive) }; }, [enableRefraction, enableReflection, enableParallax, enableMicroInteractions, performanceConfig, material, interactive, supportsAdvancedRendering]); // Get material specification const materialSpec = useMemo(() => { const spec = liquidGlassUtils.getLiquidSurface(intent, elevation, material, variant); // Apply prop overrides if (ior !== undefined) spec.ior = ior; if (thickness !== undefined) spec.thickness = thickness; if (sheen !== undefined) spec.sheen = sheen; if (tintMode !== undefined) spec.tintMode = tintMode; return spec; }, [intent, elevation, material, variant, ior, thickness, sheen, tintMode]); // Use contrast guard for accessibility const contrastAdjustment = useContrastGuard(elementRef, { targetLevel: contrastLevel, material, variant, textColor: materialSpec.text.primary, onAdjustment: onContrastAdjustment }); // Device orientation tracking for motion effects useEffect(() => { if (!isMotionResponsiveEnabled || !effectiveFlags.microInteractions) { return; } const handleOrientation = event => { if (event.beta !== null && event.gamma !== null) { const { sensitivity } = LIQUID_GLASS.motionFluency.tilt; setDeviceTilt({ x: Math.max(-1, Math.min(1, event.gamma * sensitivity)), y: Math.max(-1, Math.min(1, event.beta * sensitivity)) }); } }; if (typeof DeviceOrientationEvent !== "undefined") { window.addEventListener("deviceorientation", handleOrientation); return () => window.removeEventListener("deviceorientation", handleOrientation); } }, [isMotionResponsiveEnabled, effectiveFlags.microInteractions]); // Backdrop sampling for environmental adaptation useEffect(() => { if (!isEnvironmentAdaptive || !elementRef.current || material !== "liquid") { return; } const element = elementRef.current; const sampleBackdrop = async () => { try { const sample = await contrastGuard.sampleBackdrop(element); setBackdropSample(sample); onBackdropAnalysis?.(sample); } catch (error) { console.warn("LiquidGlassMaterial: Backdrop sampling failed", error); } }; // Initial sample sampleBackdrop(); // Periodic updates (throttled) const interval = setInterval(sampleBackdrop, LIQUID_GLASS.tinting.adaptive.updateThrottle); return () => clearInterval(interval); }, [isEnvironmentAdaptive, material, onBackdropAnalysis]); // Generate dynamic styles based on current state const dynamicStyles = useMemo(() => { const baseStyles = liquidGlassUtils.buildLiquidGlassStyles(intent, elevation, material, variant, performanceLevel); let styles = { ...baseStyles }; // Apply contrast adjustments if (contrastAdjustment?.modifications) { const { modifications } = contrastAdjustment; if (modifications.opacity !== undefined) { styles.opacity = modifications.opacity; } if (modifications.tint && materialSpec.tintMode === "adaptive") { styles.background = `${styles.background}, ${modifications.tint}`; } if (modifications.backdropBlur !== undefined) { const currentBlur = materialSpec.backdropBlur.px; const adjustedBlur = currentBlur * modifications.backdropBlur; const backdropFilter = styles.backdropFilter?.replace(/blur\([\d.]+px\)/, `blur(${adjustedBlur}px)`); styles.backdropFilter = backdropFilter; styles.WebkitBackdropFilter = backdropFilter; } } // Apply IOR-enhanced backdrop filter if (effectiveFlags.refraction && materialSpec.ior > 1) { const iorMultiplier = 1 + (materialSpec.ior - 1) * 0.3; const saturation = 1.8 * iorMultiplier; const brightness = 1.15 + (materialSpec.ior - 1) * 0.1; const contrast = 1.08 + materialSpec.sheen * 0.02; const backdropFilter = `blur(${materialSpec.backdropBlur.px}px) saturate(${saturation}) brightness(${brightness}) contrast(${contrast})`; styles.backdropFilter = backdropFilter; styles.WebkitBackdropFilter = backdropFilter; } // Apply thickness-based enhancements if (materialSpec.thickness > 2) { const thicknessOpacity = 0.1 + materialSpec.sheen * 0.05; styles.boxShadow = `${styles.boxShadow}, inset 0 1px ${materialSpec.thickness}px rgba(255,255,255,${thicknessOpacity})`; } // Motion responsiveness if (effectiveFlags.microInteractions && !prefersReducedMotion) { const { hover, press } = LIQUID_GLASS.motionFluency; if (isHovered) { styles.transform = `${styles.transform || ""} translateY(-1px) scale(1.005)`.trim(); styles.transition = `all ${hover.duration}ms ${hover.easing}`; } if (isPressed) { styles.transform = `${styles.transform || ""} translateY(1px) scale(0.998)`.trim(); styles.transition = `all ${press.duration}ms ${press.easing}`; } // Device tilt responsiveness if (Math.abs(deviceTilt.x) > 0.1 || Math.abs(deviceTilt.y) > 0.1) { const tiltX = deviceTilt.x * LIQUID_GLASS.motionFluency.tilt.maxTilt; const tiltY = deviceTilt.y * LIQUID_GLASS.motionFluency.tilt.maxTilt; styles.transform = `${styles.transform || ""} rotateX(${tiltY}deg) rotateY(${tiltX}deg)`.trim(); } } // Parallax depth effects if (effectiveFlags.parallax && materialSpec.thickness > 3) { styles.transformStyle = "preserve-3d"; styles.transform = `${styles.transform || ""} translateZ(${materialSpec.thickness * 2}px)`.trim(); } // Border radius const radiusMap = { none: "0px", sm: "4px", md: "8px", lg: "12px", xl: "16px", "2xl": "20px", full: "9999px" }; styles.borderRadius = radiusMap[radius]; // Performance optimizations if (effectiveFlags.microInteractions || effectiveFlags.parallax) { styles.willChange = "transform, opacity, backdrop-filter"; styles.contain = "layout style paint"; } return styles; }, [intent, elevation, material, variant, performanceLevel, materialSpec, contrastAdjustment, effectiveFlags, isHovered, isPressed, deviceTilt, prefersReducedMotion, radius]); // Event handlers for micro-interactions const handleMouseEnter = useCallback(() => { if (effectiveFlags.microInteractions && !disabled) { setIsHovered(true); } }, [effectiveFlags.microInteractions, disabled]); const handleMouseLeave = useCallback(() => { if (effectiveFlags.microInteractions) { setIsHovered(false); } }, [effectiveFlags.microInteractions]); const handleMouseDown = useCallback(() => { if (effectiveFlags.microInteractions && !disabled) { setIsPressed(true); } }, [effectiveFlags.microInteractions, disabled]); const handleMouseUp = useCallback(() => { if (effectiveFlags.microInteractions) { setIsPressed(false); } }, [effectiveFlags.microInteractions]); // Build CSS classes const classes = cn( // Base glass foundation "liquid-glass-material", "relative", "overflow-hidden", // Material type material === "liquid" && "liquid-glass-advanced", // Variant `liquid-glass-variant-${variant}`, variant && `liquid-glass-${variant}`, qualityTier && `liquid-glass-quality-${qualityTier}`, // Intent intent !== "neutral" && `liquid-glass-${intent}`, // Interactive states interactive && "cursor-pointer", disabled && "pointer-events-none opacity-60", // Accessibility contrastAdjustment?.modifications.fallbackMode && "liquid-glass-contrast-fallback", !supportsAdvancedRendering && "liquid-glass-css-fallback", // Feature flags effectiveFlags.refraction && "liquid-glass-refraction", effectiveFlags.reflection && "liquid-glass-reflection", effectiveFlags.parallax && "liquid-glass-parallax", effectiveFlags.microInteractions && "liquid-glass-interactive", isMotionResponsiveEnabled && "liquid-glass-motion-responsive", isEnvironmentAdaptive && "liquid-glass-environment-adaptive", // Reduced motion prefersReducedMotion && "liquid-glass-reduced-motion", prefersReducedMotion && "liquid-glass-motion-reduced", // Custom classes className); const cssVariableStyles = useMemo(() => { const variables = {}; if (materialSpec.ior !== undefined) { variables["--liquid-glass-ior"] = materialSpec.ior.toString(); } if (materialSpec.thickness !== undefined) { variables["--liquid-glass-thickness"] = `${materialSpec.thickness}px`; } if (tint) { const { r, g, b, a } = tint; variables["--liquid-glass-tint"] = `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; } return variables; }, [materialSpec, tint]); // Combine custom styles const combinedStyles = { ...cssVariableStyles, ...dynamicStyles, ...style }; const { className: contentClassName, style: contentStyle, ...restContentProps } = contentProps ?? {}; const ComponentTag = asProp || "div"; return jsxs(ComponentTag, { ref: combinedRef, className: classes, style: combinedStyles, "data-liquid-glass-material": "true", "data-liquid-glass-quality": qualityTier || undefined, "data-liquid-glass-motion-responsive": isMotionResponsiveEnabled || undefined, "data-liquid-glass-environment-adaptive": isEnvironmentAdaptive || undefined, "data-liquid-glass-css-fallback": !supportsAdvancedRendering || undefined, onMouseEnter: interactive ? handleMouseEnter : undefined, onMouseLeave: interactive ? handleMouseLeave : undefined, onMouseDown: interactive ? handleMouseDown : undefined, onMouseUp: interactive ? handleMouseUp : undefined, "aria-disabled": disabled || undefined, ...props, children: [materialSpec.sheen > 0 && jsx("div", { className: 'liquid-glass-sheen absolute inset-0 pointer-events-none', style: { background: `radial-gradient(60% 60% at 50% -10%, rgba(255,255,255,${0.1 + materialSpec.sheen * 0.05}) 0%, transparent 60%)`, opacity: isHovered ? 1.2 : 1, transition: LIQUID_GLASS.motionFluency.hover.easing } }), jsx("div", { className: cn("liquid-glass-content relative z-10", contentClassName), style: contentStyle, ...restContentProps, children: children }), process.env.NODE_ENV === "development" && jsxs("div", { className: 'liquid-glass-debug absolute top-0 right-0 glass-text-xs opacity-50 pointer-events-none glass-surface-dark glass-text-primary glass-p-1 glass-radius', children: [jsxs("div", { children: ["Material: ", material] }), jsxs("div", { children: ["Variant: ", variant] }), jsxs("div", { children: ["IOR: ", materialSpec.ior.toFixed(2)] }), jsxs("div", { children: ["Thickness: ", materialSpec.thickness, "px"] }), jsxs("div", { children: ["Sheen: ", materialSpec.sheen] }), contrastAdjustment && jsxs("div", { children: ["Contrast: ", contrastAdjustment.adjustedContrast.toFixed(1), ":1"] })] })] }); }); LiquidGlassMaterial.displayName = "LiquidGlassMaterial"; export { LiquidGlassMaterial }; //# sourceMappingURL=LiquidGlassMaterial.js.map