UNPKG

aura-glass

Version:

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

813 lines (809 loc) 27.1 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useContext, createContext, useRef, useState, useEffect, useCallback, useMemo, useId } from 'react'; import { THEME_VARIANTS, BLUR_STRENGTHS } from './themeConstants.js'; import { AURA_GLASS } from '../tokens/glass.js'; import { safeMatchMedia, isBrowser, getSafeWindow, getSafeDocument } from '../utils/env.js'; const ColorModeContext = /*#__PURE__*/createContext({ colorMode: "system", setColorMode: mode => { if (process.env.NODE_ENV === "development") { console.warn("setColorMode was called before ThemeProvider was initialized"); } }, isDarkMode: false, toggleColorMode: () => { if (process.env.NODE_ENV === "development") { console.warn("toggleColorMode was called before ThemeProvider was initialized"); } }, systemPrefersDark: false }); const ThemeVariantContext = /*#__PURE__*/createContext({ themeVariant: THEME_VARIANTS[0] , setThemeVariant: variant => { if (process.env.NODE_ENV === "development") { console.warn("setThemeVariant was called before ThemeProvider was initialized"); } }, availableThemes: [...THEME_VARIANTS] }); const StyleUtilsContext = /*#__PURE__*/createContext({ getColor: () => "", getSpacing: () => "", getShadow: () => "", getBorderRadius: () => "", getZIndex: () => 0, getTypography: () => ({}) }); const GlassEffectsContext = /*#__PURE__*/createContext({ qualityTier: "high", setQualityTier: tier => { if (process.env.NODE_ENV === "development") { console.warn("setQualityTier was called before ThemeProvider was initialized"); } }, getBlurStrength: () => "", getBackgroundOpacity: () => 0, getBorderOpacity: () => 0, getGlowIntensity: () => 0, createSurface: () => "", GlassSurface: () => null }); const PreferencesContext = /*#__PURE__*/createContext({ reducedMotion: false, reducedTransparency: false, highContrastMode: false, setPreference: (key, value) => { if (process.env.NODE_ENV === "development") { console.warn("setPreference was called before ThemeProvider was initialized"); } }, getUserPreference: () => false }); const ResponsiveContext = /*#__PURE__*/createContext({ breakpoints: { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920 }, currentBreakpoint: "md", isMobile: false, isTablet: false, isDesktop: true, mediaQuery: () => "" }); // ------ ThemeProvider Presence Context ------ const ThemeProviderPresenceContext = /*#__PURE__*/createContext(false); const DEFAULT_PERSONA_ID = "auraglass-default"; const useThemeProviderPresence = () => useContext(ThemeProviderPresenceContext); /** * Unified Theme Provider Component * * Provides a comprehensive theme context for Glass UI components. */ const UnifiedThemeProvider = ({ children, initialColorMode = "system", initialTheme = THEME_VARIANTS[0] , enableAutoDetection = true, respectSystemPreference = true, forceColorMode, disableTransitions = false, enableScrollOptimization = true, initialQualityTier = "high", isolateTheme = false, enableOptimizations = true, debug = false, performanceMonitoring = false, contextUpdateThrottle = 0, updateOnlyOnCommit = false, onColorModeChange, onThemeChange }) => { // ------ Color Mode State ------ const themeHostRef = useRef(null); const resolvedPersonaId = DEFAULT_PERSONA_ID; const [colorMode, setColorModeState] = useState(initialColorMode); // State for whether the system prefers dark mode const [systemPrefersDark, setSystemPrefersDark] = useState(() => safeMatchMedia("(prefers-color-scheme: dark)")?.matches ?? false); // ------ Theme Variant State ------ const [themeVariant, setThemeVariantState] = useState(initialTheme); // ------ Glass Effects State ------ const [qualityTier, setQualityTierState] = useState(initialQualityTier); // ------ Preferences State ------ const [preferences, setPreferences] = useState({ reducedMotion: false, reducedTransparency: false, highContrastMode: false }); // ------ Responsive State ------ const [currentBreakpoint, setCurrentBreakpoint] = useState("md"); // ------ Performance Tracking ------ const renderCount = useRef(0); const lastUpdateTime = useRef(Date.now()); const pendingUpdates = useRef({}); const commitTimerRef = useRef(null); // ------ Initialize System Preferences ------ useEffect(() => { if (!enableAutoDetection || !isBrowser()) return; const darkModeMediaQuery = safeMatchMedia("(prefers-color-scheme: dark)"); const motionMediaQuery = safeMatchMedia("(prefers-reduced-motion: reduce)"); const handleDarkModeChange = event => { setSystemPrefersDark(event.matches); }; const handleMotionChange = event => { setPreferences(prev => ({ ...prev, reducedMotion: event.matches })); }; if (darkModeMediaQuery) { setSystemPrefersDark(darkModeMediaQuery.matches); darkModeMediaQuery.addEventListener("change", handleDarkModeChange); } if (motionMediaQuery) { setPreferences(prev => ({ ...prev, reducedMotion: motionMediaQuery.matches })); motionMediaQuery.addEventListener("change", handleMotionChange); } return () => { if (darkModeMediaQuery) { darkModeMediaQuery.removeEventListener("change", handleDarkModeChange); } if (motionMediaQuery) { motionMediaQuery.removeEventListener("change", handleMotionChange); } }; }, [enableAutoDetection]); // ------ Load Saved Preferences ------ useEffect(() => { if (!isBrowser()) return; try { const storage = getSafeWindow()?.localStorage; if (!storage) return; const savedColorMode = storage.getItem("glass-ui-color-mode"); if (savedColorMode && !forceColorMode) { setColorModeState(savedColorMode); } const savedThemeVariant = storage.getItem("glass-ui-theme-variant"); if (savedThemeVariant) { setThemeVariantState(savedThemeVariant); } const savedQualityTier = storage.getItem("glass-ui-quality-tier"); if (savedQualityTier) { setQualityTierState(savedQualityTier); } const savedPreferences = storage.getItem("glass-ui-preferences"); if (savedPreferences) { try { const parsedPreferences = JSON.parse(savedPreferences); setPreferences(prev => ({ ...prev, ...parsedPreferences })); } catch (e) { if (process.env.NODE_ENV === "development") { console.error("Failed to parse saved preferences", e); } } } } catch (error) { if (process.env.NODE_ENV === "development") { console.warn("ThemeProvider: failed to load saved preferences", error); } } }, [forceColorMode]); // ------ Initialize Responsive Breakpoints ------ useEffect(() => { if (!isBrowser()) return; const win = getSafeWindow(); if (!win) return; const breakpoints = { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920 }; const handleResize = () => { const width = win.innerWidth; let newBreakpoint = "xs"; if (width >= breakpoints.xl) { newBreakpoint = "xl"; } else if (width >= breakpoints.lg) { newBreakpoint = "lg"; } else if (width >= breakpoints.md) { newBreakpoint = "md"; } else if (width >= breakpoints.sm) { newBreakpoint = "sm"; } setCurrentBreakpoint(newBreakpoint); }; win.addEventListener("resize", handleResize); handleResize(); // Initial calculation return () => { win.removeEventListener("resize", handleResize); }; }, []); // ------ Optimized Update Handlers ------ // Handle color mode change with throttling const setColorMode = useCallback(mode => { if (forceColorMode) return; // Don't change if force mode is active const storage = getSafeWindow()?.localStorage; if (contextUpdateThrottle > 0) { pendingUpdates.current.colorMode = mode; if (!commitTimerRef.current) { commitTimerRef.current = setTimeout(() => { setColorModeState(pendingUpdates.current.colorMode); if (onColorModeChange) onColorModeChange(pendingUpdates.current.colorMode); storage?.setItem("glass-ui-color-mode", pendingUpdates.current.colorMode); commitTimerRef.current = null; }, contextUpdateThrottle); } } else { setColorModeState(mode); if (onColorModeChange) onColorModeChange(mode); storage?.setItem("glass-ui-color-mode", mode); } }, [forceColorMode, contextUpdateThrottle, onColorModeChange]); // Toggle between light and dark mode const toggleColorMode = useCallback(() => { if (forceColorMode) return; // Don't toggle if force mode is active setColorMode(colorMode === "light" ? "dark" : colorMode === "dark" ? "light" : systemPrefersDark ? "light" : "dark"); }, [colorMode, forceColorMode, systemPrefersDark, setColorMode]); // Handle theme variant change const setThemeVariant = useCallback(variant => { setThemeVariantState(variant); if (onThemeChange) onThemeChange(variant); const storage = getSafeWindow()?.localStorage; storage?.setItem("glass-ui-theme-variant", variant); }, [onThemeChange]); // Handle quality tier change const setQualityTier = useCallback(tier => { setQualityTierState(tier); const storage = getSafeWindow()?.localStorage; storage?.setItem("glass-ui-quality-tier", tier); }, []); // Handle preference changes const setPreference = useCallback((key, value) => { setPreferences(prev => { const newPreferences = { ...prev, [key]: value }; const storage = getSafeWindow()?.localStorage; storage?.setItem("glass-ui-preferences", JSON.stringify(newPreferences)); return newPreferences; }); }, []); // Get user preference const getUserPreference = useCallback(key => { return preferences[key] || false; }, [preferences]); // ------ Theme Utilities ------ // Determine if dark mode is active based on all factors const isDarkMode = useMemo(() => { if (forceColorMode) { return forceColorMode === "dark"; } return colorMode === "dark" || colorMode === "system" && respectSystemPreference && systemPrefersDark; }, [colorMode, forceColorMode, respectSystemPreference, systemPrefersDark]); const resolvedMode = isDarkMode ? "dark" : "light"; useEffect(() => { if (isolateTheme) { const host = themeHostRef.current; if (host) { host.setAttribute("data-aura-theme", resolvedPersonaId); host.setAttribute("data-aura-mode", resolvedMode); } return; } const doc = getSafeDocument(); const el = doc?.documentElement; if (!el) { return; } const previousTheme = el.getAttribute("data-aura-theme"); const previousMode = el.getAttribute("data-aura-mode"); el.setAttribute("data-aura-theme", resolvedPersonaId); el.setAttribute("data-aura-mode", resolvedMode); return () => { if (previousTheme) { el.setAttribute("data-aura-theme", previousTheme); } else { el.removeAttribute("data-aura-theme"); } if (previousMode) { el.setAttribute("data-aura-mode", previousMode); } else { el.removeAttribute("data-aura-mode"); } }; }, [isolateTheme, resolvedPersonaId, resolvedMode]); // Create style utility functions const getColor = useCallback((path, fallback = "") => { const parts = path.split("."); let value = AURA_GLASS.surfaces; for (const part of parts) { if (value && typeof value === "object" && part in value) { value = value[part]; } else { return fallback; } } return typeof value === "string" ? value : fallback; }, []); // Create glass effect utilities const getBlurStrength = useCallback(strength => { if (typeof strength === "number") { return `${strength}px`; } return BLUR_STRENGTHS.includes(strength) ? strength : "standard"; }, []); const getBackgroundOpacity = useCallback(opacity => { if (typeof opacity === "number") { return Math.max(0, Math.min(1, opacity)); } const opacityMap = { transparent: 0, lightest: 0.05, light: 0.1, medium: 0.2, high: 0.5, solid: 1 }; return opacityMap[opacity] || 0.2; }, []); const getBorderOpacity = useCallback(opacity => { if (typeof opacity === "number") { return Math.max(0, Math.min(1, opacity)); } const opacityMap = { none: 0, minimal: 0.05, subtle: 0.1, medium: 0.2, high: 0.4 }; return opacityMap[opacity] || 0.2; }, []); const getGlowIntensity = useCallback(intensity => { if (typeof intensity === "number") { return Math.max(0, Math.min(1, intensity)); } const intensityMap = { minimal: 0.02, light: 0.05, medium: 0.1, strong: 0.15, extreme: 0.25 }; return intensityMap[intensity] || 0.1; }, []); // Helper to create glass surface styles const createSurface = useCallback(props => { const { variant = "standard", elevation: rawElevation = 1, interactive = false } = props; // Ensure elevation is a number const elevation = typeof rawElevation === "string" ? rawElevation === "level1" ? 1 : rawElevation === "level2" ? 2 : rawElevation === "level3" ? 3 : rawElevation === "level4" ? 4 : 1 : Number(rawElevation); // Get glass-specific color values const backgroundColor = isDarkMode ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.1)"; const borderColor = isDarkMode ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)"; const shadowColor = isDarkMode ? "rgba(0, 0, 0, 0.3)" : "rgba(0, 0, 0, 0.1)"; const glowColor = isDarkMode ? "var(--glass-color-primary)" : "#6366f1"; // Get opacity and blur values from qualityTier const bgOpacity = getBackgroundOpacity("medium"); const borderOpacityValue = getBorderOpacity("medium"); const blurValue = getBlurStrength("medium"); const glowValue = getGlowIntensity("medium"); // Build styles based on glass variant const baseStyles = ` position: relative; background-color: ${backgroundColor}; backdrop-filter: blur(${blurValue}); -webkit-backdrop-filter: blur(${blurValue}); border: 1px solid ${borderColor}; box-shadow: 0 4px 12px ${shadowColor}; transition: transform 0.2s ease, box-shadow 0.2s ease; `; // Add variant-specific styles let variantStyles = ""; // Variables for all cases let blurNumber; let bgOpacityAdjusted; let blurAdjusted; let dimBgOpacity; switch (variant) { case "frosted": // Parse blur value as number if it's a string blurNumber = typeof blurValue === "string" ? parseInt(blurValue.replace("px", ""), 10) : Number(blurValue); bgOpacityAdjusted = bgOpacity * 0.7; blurAdjusted = blurNumber * 1.5; variantStyles = ` background-color: rgba(255, 255, 255, ${bgOpacityAdjusted}); backdrop-filter: blur(${blurAdjusted}px); -webkit-backdrop-filter: blur(${blurAdjusted}px); border-width: 1px; `; break; case "crystal": dimBgOpacity = bgOpacity * 0.6; const dimElev2 = elevation * 2; const dimElev6 = elevation * 6; const dimElev05 = elevation * 0.5; const dimElev1 = elevation * 1; variantStyles = ` background-color: rgba(255, 255, 255, ${dimBgOpacity}); box-shadow: 0 ${dimElev2}px ${dimElev6}px ${shadowColor}, 0 ${dimElev05}px ${dimElev1}px rgba(0, 0, 0, 0.03), inset 0 0 0 1px rgba(255, 255, 255, ${borderOpacityValue}); border-width: 0; `; break; case "metallic": const accentColor = isDarkMode ? "var(--glass-color-primary)" : "#6366f1"; const bgOpacityTop = bgOpacity * 0.6; const bgOpacityBottom = bgOpacity * 0.4; const elevationDouble = elevation * 2; const elevationSix = elevation * 6; const elevationFive = elevation * 5; variantStyles = ` background: linear-gradient( 135deg, rgba(255, 255, 255, ${bgOpacityTop}) 0%, rgba(${accentColor}, ${bgOpacityBottom}) 100% ); box-shadow: 0 ${elevationDouble}px ${elevationSix}px ${shadowColor}, 0 0 ${elevationFive}px rgba(${accentColor}, ${glowValue}); `; break; case "standard": default: const stdElev2 = elevation * 2; const stdElev6 = elevation * 6; variantStyles = ` background-color: rgba(255, 255, 255, ${bgOpacity}); box-shadow: 0 ${stdElev2}px ${stdElev6}px ${shadowColor}, inset 0 0 0 1px rgba(255, 255, 255, ${borderOpacityValue}); `; break; } // Add interactive states if required const hoverElev3 = elevation * 3; const hoverElev8 = elevation * 8; const hoverElev2 = elevation * 2; const activeElev1 = elevation * 1; const activeElev3 = elevation * 3; const interactiveStyles = interactive ? ` &:hover { transform: translateY(-2px); box-shadow: 0 ${hoverElev3}px ${hoverElev8}px ${shadowColor}, 0 0 ${hoverElev2}px ${glowColor}; } &:active { transform: translateY(0); box-shadow: 0 ${activeElev1}px ${activeElev3}px ${shadowColor}; } ` : ""; return ` ${baseStyles} ${variantStyles} ${interactiveStyles} `; }, [isDarkMode, getBackgroundOpacity, getBorderOpacity, getBlurStrength, getGlowIntensity]); /** * GlassSurface Component - A component for rendering glass surfaces with configurable properties */ function GlassSurfaceComponent(props) { const { variant = "frosted", elevation = "level1", interactive = false, children, ...rest } = props; // Generate a unique ID for this surface const uniqueId = useId(); const surfaceId = useMemo(() => `glass-surface-${uniqueId.replace(/:/g, "-")}`, [uniqueId]); // Get the glass styles const cssString = createSurface({ variant, elevation, interactive }); return jsxs("div", { id: surfaceId, ...rest, children: [jsx("style", { dangerouslySetInnerHTML: { __html: ` #${surfaceId} { ${cssString} } ` } }), children] }); } // Use the component directly const GlassSurface = GlassSurfaceComponent; // ------ Create Responsive Utilities ------ const breakpoints = useMemo(() => { return { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920 }; }, []); const mediaQuery = useCallback(breakpoint => { const width = breakpoints[breakpoint] || 0; return `@media (min-width: ${width}px)`; }, [breakpoints]); const isMobile = useMemo(() => { return ["xs", "sm"].includes(currentBreakpoint); }, [currentBreakpoint]); const isTablet = useMemo(() => { return currentBreakpoint === "md"; }, [currentBreakpoint]); const isDesktop = useMemo(() => { return ["lg", "xl"].includes(currentBreakpoint); }, [currentBreakpoint]); // ------ Create Context Values ------ // ColorMode context const colorModeContextValue = useMemo(() => ({ colorMode: forceColorMode || colorMode, setColorMode, isDarkMode, toggleColorMode, systemPrefersDark }), [forceColorMode, colorMode, setColorMode, isDarkMode, toggleColorMode, systemPrefersDark]); // ThemeVariant context const themeVariantContextValue = useMemo(() => ({ themeVariant, setThemeVariant, availableThemes: [...THEME_VARIANTS] }), [themeVariant, setThemeVariant]); // Theme utility functions const getSpacing = useCallback(size => { const spacingMap = { xs: "0.25rem", sm: "0.5rem", md: "1rem", lg: "1.5rem", xl: "2rem" }; if (typeof size === "number") return `${size * 8}px`; return spacingMap[size] || "0"; }, []); const getShadow = useCallback((level, color) => { const shadowMap = { none: "none", sm: "0 1px 2px rgba(0,0,0,0.05)", md: "0 4px 6px rgba(0,0,0,0.07)", lg: "0 10px 15px rgba(0,0,0,0.1)" }; const key = level < 1 ? "none" : level === 1 ? "sm" : level === 2 ? "md" : "lg"; return shadowMap[key] || shadowMap.md; }, []); const getBorderRadius = useCallback(size => { const borderRadiusMap = { none: "0", sm: "0.25rem", md: "0.5rem", lg: "1rem" }; return borderRadiusMap[size] || "0"; }, []); const getZIndex = useCallback(layer => { const zIndexMap = { base: 0, modal: 1000, tooltip: 1100 }; return zIndexMap[layer] || 0; }, []); const getTypography = useCallback(variant => { const typographyMap = { h1: { fontSize: "2.5rem", fontWeight: 600 }, h2: { fontSize: "2rem", fontWeight: 600 }, h3: { fontSize: "1.5rem", fontWeight: 600 }, body: { fontSize: "1rem", fontWeight: 400 } }; return typographyMap[variant] || {}; }, []); // StyleUtils context const styleUtilsContextValue = useMemo(() => ({ getColor, getSpacing, getShadow, getBorderRadius, getZIndex, getTypography }), [getColor, getSpacing, getShadow, getBorderRadius, getZIndex, getTypography]); // GlassEffects context including the component const glassEffectsContextValue = useMemo(() => ({ qualityTier, setQualityTier, getBlurStrength, getBackgroundOpacity, getBorderOpacity, getGlowIntensity, createSurface, GlassSurface }), [qualityTier, setQualityTier, getBlurStrength, getBackgroundOpacity, getBorderOpacity, getGlowIntensity, createSurface, GlassSurface]); // Preferences context const preferencesContextValue = useMemo(() => ({ reducedMotion: preferences.reducedMotion, reducedTransparency: preferences.reducedTransparency, highContrastMode: preferences.highContrastMode, setPreference, getUserPreference }), [preferences, setPreference, getUserPreference]); // Responsive context const responsiveContextValue = useMemo(() => ({ breakpoints, currentBreakpoint, isMobile, isTablet, isDesktop, mediaQuery }), [breakpoints, currentBreakpoint, isMobile, isTablet, isDesktop, mediaQuery]); // Performance debugging useEffect(() => { if (debug && performanceMonitoring) { renderCount.current++; const renderTime = Date.now() - lastUpdateTime.current; if (process.env.NODE_ENV === "development") { console.log(`[ThemeProvider] Render #${renderCount.current} took ${renderTime}ms`); } lastUpdateTime.current = Date.now(); } }); // Prevent transitions during theme changes useEffect(() => { if (!disableTransitions) { return; } const doc = getSafeDocument(); if (!doc) { return; } doc.documentElement.classList.add("disable-transitions"); const timeout = setTimeout(() => { doc.documentElement.classList.remove("disable-transitions"); }, 100); return () => clearTimeout(timeout); }, [isDarkMode, themeVariant, disableTransitions]); // Apply scroll optimization useEffect(() => { if (!enableScrollOptimization || !isBrowser()) { return; } const doc = getSafeDocument(); const win = getSafeWindow(); if (!doc || !win) { return; } let scrollTimer = null; let isScrolling = false; const handleScroll = () => { if (!isScrolling) { isScrolling = true; doc.documentElement.classList.add("is-scrolling"); } if (scrollTimer) { clearTimeout(scrollTimer); } scrollTimer = setTimeout(() => { isScrolling = false; doc.documentElement.classList.remove("is-scrolling"); }, 150); }; win.addEventListener("scroll", handleScroll, { passive: true }); return () => { win.removeEventListener("scroll", handleScroll); if (scrollTimer) { clearTimeout(scrollTimer); } }; }, [enableScrollOptimization]); // Render multi-context provider const themedChildren = isolateTheme ? jsx("div", { ref: themeHostRef, "data-aura-theme": resolvedPersonaId, "data-aura-mode": resolvedMode, children: children }) : children; return jsx(ThemeProviderPresenceContext.Provider, { value: true, children: jsx(ColorModeContext.Provider, { value: colorModeContextValue, children: jsx(ThemeVariantContext.Provider, { value: themeVariantContextValue, children: jsx(StyleUtilsContext.Provider, { value: styleUtilsContextValue, children: jsx(GlassEffectsContext.Provider, { value: glassEffectsContextValue, children: jsx(PreferencesContext.Provider, { value: preferencesContextValue, children: jsx(ResponsiveContext.Provider, { value: responsiveContextValue, children: themedChildren }) }) }) }) }) }) }); }; /** * ThemeProvider component (exporting the unified provider directly for now). * * This component provides theme context. */ const ThemeProvider = UnifiedThemeProvider; // ------ Theme Hooks ------ /** * Returns the full theme object based on current settings. */ const useTheme = () => { const colorModeContext = useContext(ColorModeContext); const themeVariantContext = useContext(ThemeVariantContext); const styleUtilsContext = useContext(StyleUtilsContext); if (!colorModeContext || !themeVariantContext || !styleUtilsContext) { throw new Error("useTheme must be used within a ThemeProvider"); } return { isDark: colorModeContext.isDarkMode, currentColorMode: colorModeContext.colorMode, toggleColorMode: colorModeContext.toggleColorMode, setColorMode: colorModeContext.setColorMode, currentTheme: themeVariantContext.themeVariant, setTheme: themeVariantContext.setThemeVariant, availableThemes: themeVariantContext.availableThemes, ...styleUtilsContext }; }; /** * Hook for accessing only theme variant aspects. * More efficient than useTheme when only theme variant is needed. */ const useThemeVariant = () => { const context = useContext(ThemeVariantContext); if (!context) { throw new Error("useThemeVariant must be used within a ThemeProvider"); } return context; }; export { ThemeProvider, useTheme, useThemeProviderPresence, useThemeVariant }; //# sourceMappingURL=ThemeProvider.js.map