UNPKG

aura-glass

Version:

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

318 lines (315 loc) 12.2 kB
'use client'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { GlassButton } from '../button/GlassButton.js'; import { GlassTooltip } from '../modal/GlassPopover.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { ChevronRight, ChevronLeft } from 'lucide-react'; import React, { forwardRef, useRef, useState, useLayoutEffect, useEffect, createContext, useContext } from 'react'; import { createPortal } from 'react-dom'; 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 { MotionFramer } from '../../primitives/motion/MotionFramer.js'; const SidebarContext = /*#__PURE__*/createContext(undefined); const useSidebarContext = () => { const context = useContext(SidebarContext); if (!context) { throw new Error("Sidebar components must be used within GlassSidebar"); } return context; }; /** * GlassSidebar component * A glassmorphism sidebar navigation with advanced features */ const GlassSidebar = /*#__PURE__*/forwardRef(({ items, activeId, variant = "default", width = "md", collapsible = true, collapsed = false, onCollapsedChange, open = true, onOpenChange, header, footer, logo, userInfo, onNavigate, renderItem, className, ...props }, ref) => { const containerRef = useRef(null); const [edgeX, setEdgeX] = useState(null); const [internalCollapsed, setInternalCollapsed] = useState(collapsed); const isCollapsed = collapsed ?? internalCollapsed; const widthClasses = { sm: isCollapsed ? "w-16" : "w-48", md: isCollapsed ? "w-16" : "w-64", lg: isCollapsed ? "w-20" : "w-72", xl: isCollapsed ? "w-20" : "w-80" }; const variantClasses = { default: "border-r border-border/20", compact: "border-r border-border/10", floating: "glass-mx-4 glass-my-4 squiricle border border-border/20", overlay: "border-r border-border/20 shadow-xl" }; const handleToggleCollapse = () => { const newCollapsed = !isCollapsed; setInternalCollapsed(newCollapsed); onCollapsedChange?.(newCollapsed); }; const handleItemClick = item => { if (item?.disabled) return; onNavigate?.(item); if (variant === "overlay") { onOpenChange?.(false); } }; const contextValue = { collapsed: isCollapsed, activeId, onNavigate: handleItemClick, renderItem }; if (variant === "overlay" && !open) { return null; } // Map width to px for fixed toggle placement const widthPxMap = { sm: 192, md: 256, lg: 288, xl: 320 }; const collapsedWidthPxMap = { sm: 64, md: 64, lg: 80, xl: 80 }; // Measure actual sidebar right edge in viewport coordinates useLayoutEffect(() => { const el = containerRef.current; if (!el) return; const update = () => { const rect = el.getBoundingClientRect(); setEdgeX(rect.left + rect.width); }; update(); let ro = null; if (typeof window !== "undefined" && "ResizeObserver" in window) { ro = new ResizeObserver(() => update()); ro.observe(el); } window.addEventListener("resize", update); window.addEventListener("scroll", update, true); const rafLoop = () => { update(); rafId = requestAnimationFrame(rafLoop); }; let rafId = requestAnimationFrame(rafLoop); return () => { if (ro) ro.disconnect(); window.removeEventListener("resize", update); window.removeEventListener("scroll", update, true); cancelAnimationFrame(rafId); }; }, [isCollapsed]); return jsxs(SidebarContext.Provider, { "data-glass-component": true, value: contextValue, children: [jsx(MotionFramer, { preset: variant === "overlay" ? "slideRight" : "none", className: cn(variant === "overlay" && "fixed inset-y-0 left-0 z-50", "relative overflow-visible z-[60]"), style: { overflow: "visible" }, children: jsxs(OptimizedGlassCore, { ref: node => { containerRef.current = node; if (typeof ref === "function") ref(node);else if (ref && typeof ref === "object") ref.current = node; }, intent: "primary", elevation: variant === "floating" ? "level2" : "level1", intensity: "medium", depth: variant === "floating" ? 3 : 2, tint: "lavender", border: variant === "floating" ? "gradient" : "subtle", animation: "none", performanceMode: "medium", className: cn("h-screen max-h-screen flex flex-col transition-colors overflow-visible", widthClasses?.[width], variantClasses?.[variant], variant === "floating" ? "squiricle" : "", className), style: { overflow: "visible" }, ...props, children: [(header || logo) && jsx("div", { className: "glass-flex-shrink-0 glass-p-4 glass-border-b glass-border-glass-border/20", children: header || jsx("div", { className: "glass-flex glass-items-center glass-justify-center glass-w-full", children: logo && jsx("div", { className: cn("flex items-center glass-gap-3 transition-all duration-300", isCollapsed ? "scale-75 opacity-70" : "scale-100 opacity-100"), children: logo }) }) }), userInfo && !isCollapsed && jsx("div", { className: "glass-flex-shrink-0 glass-p-4 glass-border-b glass-border-glass-border/10", children: userInfo }), jsx("div", { className: cn("flex-1 glass-px-4 glass-py-2", isCollapsed ? "overflow-hidden" : "overflow-y-auto overflow-x-visible"), children: jsx(SidebarNavigation, { items: items }) }), footer && jsx("div", { className: "glass-flex-shrink-0 glass-p-4 glass-border-t glass-border-glass-border/20", children: footer })] }) }), collapsible && (() => { // Render toggle into document.body to avoid any local stacking/overflow issues const [mounted, setMounted] = function useMounted() { const [m, setM] = useState(false); useEffect(() => setM(true), []); return [m, setM]; }(); if (!mounted) return null; const fallbackX = (isCollapsed ? collapsedWidthPxMap : widthPxMap)[width]; const centerX = edgeX ?? fallbackX; // Render inside a fullscreen, pointer-events-none layer; toggle itself is pointer-events-auto return /*#__PURE__*/createPortal( // Seam overlay: narrow, full-height, click-through slice at the exact sidebar edge jsx("div", { className: 'fixed top-0 h-screen z-[2147483647]', style: { left: Math.max(0, centerX - 24), width: 48, pointerEvents: "auto" }, children: jsx("button", { type: "button", onClick: handleToggleCollapse, title: isCollapsed ? "Expand sidebar" : "Collapse sidebar", className: cn("absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 glass-radius-full", "flex items-center justify-center", // Frosted surface "glass-glass-backdrop-blur-md2xl bg-white/55 border border-white/80", "glass-focus glass-touch-target glass-contrast-guard", "shadow-[0_6px_18px_rgba(var(--glass-color-black) / var(--glass-opacity-35)),inset_0_1px_2px_var(--glass-bg-active)] ring-1 ring-white/40", "hover:bg-white/65 active:bg-white/70"), "aria-label": isCollapsed ? "Expand sidebar" : "Collapse sidebar", children: isCollapsed ? jsx(ChevronRight, { className: 'w-5 h-5 text-primary drop-shadow-[0_1px_8px_var(--glass-text-secondary-dark)]', strokeWidth: 3 }) : jsx(ChevronLeft, { className: 'w-5 h-5 text-primary drop-shadow-[0_1px_8px_var(--glass-text-secondary-dark)]', strokeWidth: 3 }) }) }), document.body); })(), variant === "overlay" && open && jsx("div", { className: 'fixed inset-0 glass-surface-dark/50 glass-glass-glass-backdrop-blur-sm z-40 glass-contrast-guard', onClick: e => onOpenChange?.(false) })] }); }); GlassSidebar.displayName = "GlassSidebar"; function SidebarNavigation({ items, level = 0 }) { return jsx("ul", { className: cn("glass-gap-2", level > 0 && "glass-ml-4"), children: (items || []).map(item => jsx(SidebarNavigationItem, { item: item, level: level }, item?.id)) }); } function SidebarNavigationItem({ item, level }) { const { collapsed, activeId, onNavigate, renderItem } = useSidebarContext(); // Only expand the most essential group by default const shouldExpandByDefault = level === 0 && item?.id === "home"; const [isExpanded, setIsExpanded] = useState(shouldExpandByDefault); const isActive = activeId === item?.id; const hasChildren = item?.children && item?.children.length > 0; if (renderItem) { return jsx("li", { children: renderItem(item, level) }); } const handleClick = () => { if (hasChildren && !collapsed) { setIsExpanded(!isExpanded); } else { onNavigate?.(item); item?.onClick?.(); } }; const buttonContent = jsxs(GlassButton, { variant: "ghost", onClick: handleClick, disabled: item?.disabled, className: cn("w-full flex items-center glass-gap-3 glass-radius-md transition-all duration-200 relative group", "hover:bg-white/10 focus:outline-none", collapsed ? "glass-px-2 glass-py-2.5 justify-center" : "glass-px-3 glass-py-2", "glass-text-sm font-medium", isActive ? 'bg-primary/15 text-primary shadow-[0_0_0_2px_${glassStyles.borderColor || "var(--glass-color-primary, 0.2)"}]' : "", !isActive && "glass-text-secondary hover:text-foreground", item?.disabled && "opacity-50 cursor-not-allowed"), children: [item?.icon && jsx("span", { className: 'glass-flex-shrink-0 w-6 h-6 glass-flex glass-items-center glass-justify-center glass-text-lg', children: /*#__PURE__*/React.cloneElement(item?.icon, { className: cn((item?.icon).props?.className, collapsed ? "w-6 h-6" : "w-5 h-5") }) }), !collapsed && jsxs(Fragment, { children: [jsx("span", { className: 'glass-flex-1 text-left whitespace-nowrap truncate leading-5', children: item?.label }), item?.badge && jsx("span", { className: cn("flex-shrink-0 glass-px-2 glass-py-1 glass-text-xs glass-radius-full font-medium transition-all duration-200", "bg-black/15 border border-black/20 shadow-md glass-backdrop-blur-md", "group-hover:bg-black/20 group-hover:border-black/25 group-hover:shadow-lg", "relative z-10" // Ensure badge stays above any background effects ), style: { color: "var(--glass-white)" // white text on dark background }, children: item?.badge }), hasChildren && jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: cn("flex-shrink-0 transition-transform duration-200 glass-text-primary/50", isExpanded ? "rotate-90" : "rotate-0"), children: jsx("polyline", { points: "9 18 15 12 9 6" }) })] })] }); return jsxs("li", { children: [collapsed && item?.label ? jsx(GlassTooltip, { content: item?.label, placement: "right", zIndex: 9999, showDelay: 300, children: buttonContent }) : buttonContent, hasChildren && !collapsed && isExpanded && jsx(MotionFramer, { preset: "slideDown", className: "glass-mt-1", children: jsx("div", { className: 'glass-ml-2 pl-4 glass-border-l glass-border-glass-border/20', children: jsx(SidebarNavigation, { items: item?.children, level: level + 1 }) }) })] }); } export { GlassSidebar }; //# sourceMappingURL=GlassSidebar.js.map