UNPKG

aura-glass

Version:

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

294 lines (291 loc) 10.7 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useImperativeHandle, useEffect, useMemo } from 'react'; 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 { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useA11yId } from '../../utils/a11y.js'; // Color utility functions const getTabColors = (color, isDarkMode, highContrast) => { const baseColors = { primary: { light: "#4B66EA", dark: "#6366F1" }, secondary: { light: "#8B5CF6", dark: "#A855F7" }, accent: { light: "#EC4899", dark: "#F472B6" }, light: { light: "var(--glass-gray-50)", dark: "var(--glass-gray-100)" }, dark: { light: "var(--glass-gray-800)", dark: "var(--glass-gray-900)" } }; const selectedColor = baseColors?.[color || "primary"][isDarkMode ? "dark" : "light"]; return { activeColor: selectedColor, activeBg: isDarkMode ? `${selectedColor}22` : `${selectedColor}11`, activeText: highContrast ? isDarkMode ? "var(--glass-white)" : "var(--glass-black)" : selectedColor, inactiveText: isDarkMode ? highContrast ? "rgba(var(--glass-color-white) / var(--glass-opacity-80))" : "rgba(var(--glass-color-white) / var(--glass-opacity-60))" : highContrast ? "var(--glass-text-secondary-dark)" : "var(--glass-text-tertiary-dark)", hoverBg: isDarkMode ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)", disabledText: isDarkMode ? "var(--glass-bg-hover)" : "rgba(var(--glass-color-black) / var(--glass-opacity-30))" }; }; // Utility to detect dark mode preference const useIsDarkMode = () => { const [isDark, setIsDark] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); setIsDark(mediaQuery.matches); const handleChange = e => setIsDark(e.matches); mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, []); return isDark; }; /** * EnhancedGlassTabs Component */ const EnhancedGlassTabs = /*#__PURE__*/forwardRef(({ tabs, activeTab, onChange, variant = "default", size = "medium", color = "primary", highContrast = false, indicatorAnimation = "slide", fullWidth = false, defaultTab, physicsEnabled = true, showIndicator = true, textAlign = "center", className, style, respectMotionPreference = true }, ref) => { // Check for reduced motion preference const prefersReducedMotion = useReducedMotion(); const shouldReduceMotion = respectMotionPreference && prefersReducedMotion; const isDarkMode = useIsDarkMode(); // Generate unique IDs for accessibility const tablistId = useA11yId("tablist"); const tabIdPrefix = useA11yId("tab"); // Refs for tab elements const tabRefs = useRef({}); // Ref for the container element const containerRef = useRef(null); // State for currently active tab const [currentTab, setCurrentTab] = useState(activeTab || defaultTab || ((tabs?.length || 0) > 0 ? tabs[0].id : "")); // State for indicator position const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0, height: 2, bottom: 0 }); // Handle tab change const handleTabChange = tabId => { if (currentTab !== tabId) { setCurrentTab(tabId); if (onChange) { onChange(tabId); } } }; // --- Imperative Handle (Moved After Handlers) --- useImperativeHandle(ref, () => ({ getContainerElement: () => containerRef.current, setActiveTab: tabId => { if (tabs.some(tab => tab.id === tabId)) { handleTabChange(tabId); } }, getActiveTab: () => currentTab, getTabElement: tabId => tabRefs.current?.[tabId] || null }), [containerRef, currentTab, tabs, handleTabChange, // Dependency tabRefs // Dependency ]); // Update active tab when controlled prop changes useEffect(() => { if (activeTab !== undefined && activeTab !== currentTab) { setCurrentTab(activeTab); } }, [activeTab]); // Update indicator position when active tab changes useEffect(() => { // Function to update the indicator position based on current tab const updateIndicatorPosition = () => { const activeTabElement = tabRefs.current?.[currentTab]; if (activeTabElement && containerRef.current) { const { left, width } = activeTabElement.getBoundingClientRect(); const containerLeft = containerRef.current.getBoundingClientRect().left || 0; const relativeLeft = left - containerLeft; const finalHeight = size === "small" ? 2 : size === "large" ? 4 : 3; const finalBottom = 0; setIndicatorStyle({ left: relativeLeft, width, height: finalHeight, bottom: finalBottom }); } }; // Run once immediately updateIndicatorPosition(); // Set up resize observer to update indicator on tab resize const resizeObserver = new ResizeObserver(() => { updateIndicatorPosition(); }); // Observe the container and current tab element if (containerRef.current) { resizeObserver.observe(containerRef.current); } const activeTabElement = tabRefs.current?.[currentTab]; if (activeTabElement) { resizeObserver.observe(activeTabElement); } // Add window resize listener window.addEventListener("resize", updateIndicatorPosition); // Clean up return () => { resizeObserver.disconnect(); window.removeEventListener("resize", updateIndicatorPosition); }; }, [currentTab, size]); // Only re-run when tab or size changes // Get color scheme for styling const colors = useMemo(() => getTabColors(color, isDarkMode, highContrast), [color, isDarkMode, highContrast]); // Size-based styling const sizeClasses = { small: { padding: "glass-py-2 glass-px-4", text: "glass-text-sm", indicatorHeight: 2 }, medium: { padding: "glass-py-3 px-5", text: "glass-text-base", indicatorHeight: 3 }, large: { padding: "glass-py-4 glass-px-6", text: "glass-text-lg", indicatorHeight: 4 } }; const sizeConfig = sizeClasses[size]; // Text alignment classes const alignmentClasses = { left: "justify-start", center: "justify-center", right: "justify-end" }; return jsxs(OptimizedGlassCore, { ref: containerRef, intent: "neutral", elevation: variant === "elevated" ? "level2" : "level1", tier: "medium", intensity: "medium", depth: 2, tint: "neutral", border: variant === "outlined" ? "subtle" : "none", animation: shouldReduceMotion ? "none" : "gentle", performanceMode: "medium", className: cn("flex relative overflow-hidden w-full glass-radius-lg", { "bg-transparent": variant === "text" }, className), style: style, children: [jsx("div", { className: cn("flex w-full relative", { "[&>*]:flex-1": fullWidth }), role: "tablist", "aria-orientation": "horizontal", id: tablistId, children: tabs.map(tab => { const isActive = currentTab === tab.id; return jsxs("button", { ref: element => { if (tabRefs.current) tabRefs.current[tab.id] = element; }, role: "tab", id: `${tabIdPrefix}-${tab.id}`, tabIndex: tab.disabled ? -1 : isActive ? 0 : -1, "aria-selected": isActive, "aria-controls": `tabpanel-${tab.id}`, disabled: tab.disabled, className: cn("relative flex items-center glass-gap-2 whitespace-nowrap border-none cursor-pointer", "outline-none transition-all duration-200 ease-out", "glass-focus glass-touch-target glass-contrast-guard", "focus-visible:ring-2 focus-visible:ring-offset-2", sizeConfig.padding, sizeConfig.text, alignmentClasses[textAlign], { "cursor-not-allowed opacity-50": tab.disabled, "bg-transparent": !isActive, "font-semibold": isActive, "font-medium": !isActive }), style: { color: tab.disabled ? colors.disabledText : isActive ? colors.activeText : colors.inactiveText, backgroundColor: isActive ? colors.activeBg : "transparent" }, onMouseEnter: e => { if (!tab.disabled && !isActive) { e.currentTarget.style.backgroundColor = colors.hoverBg; e.currentTarget.style.color = colors.activeText; } }, onMouseLeave: e => { if (!tab.disabled && !isActive) { e.currentTarget.style.backgroundColor = "transparent"; e.currentTarget.style.color = colors.inactiveText; } }, onClick: () => !tab.disabled && handleTabChange(tab.id), children: [tab.icon && jsx("span", { "aria-hidden": "true", children: tab.icon }), jsx("span", { children: tab.label }), tab.badgeCount !== undefined && tab.badgeCount > 0 && jsx("span", { className: 'glass-inline-flex glass-items-center glass-justify-center min-w-[18px] h-[18px] glass-px-1.5 glass-text-xs font-semibold text-primary glass-radius-full', style: { backgroundColor: colors.activeColor }, children: tab.badgeCount > 99 ? "99+" : tab.badgeCount })] }, tab.id); }) }), showIndicator && currentTab && jsx("div", { className: cn("absolute pointer-events-none", { "transition-all duration-300 ease-out": indicatorAnimation === "slide" && !shouldReduceMotion, "transition-opacity duration-200 ease-out": indicatorAnimation === "fade" && !shouldReduceMotion }), style: { left: `${indicatorStyle.left}px`, width: `${indicatorStyle.width}px`, height: `${sizeConfig.indicatorHeight}px`, bottom: `${indicatorStyle.bottom}px`, backgroundColor: colors.activeColor, borderRadius: `${sizeConfig.indicatorHeight / 2}px`, boxShadow: "var(--glass-elev-2)" } })] }); }); // Add displayName EnhancedGlassTabs.displayName = "EnhancedGlassTabs"; export { EnhancedGlassTabs, EnhancedGlassTabs as default }; //# sourceMappingURL=EnhancedGlassTabs.js.map