aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
318 lines (315 loc) • 12.2 kB
JavaScript
'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