UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

255 lines (254 loc) 14.3 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as React from "react"; import { ChevronRight, ChevronLeft } from "lucide-react"; import { motion, useInView } from "framer-motion"; import { twMerge } from "tailwind-merge"; import clsx from "clsx"; // Re-implementing the 'cn' utility function directly for self-containment function cn(...inputs) { return twMerge(clsx(inputs)); } const SidebarContext = React.createContext(undefined); export function SidebarProvider({ defaultExpanded = true, expanded: controlledExpanded, onExpandedChange, children, }) { const [expanded, setExpanded] = React.useState(defaultExpanded); const [activeMenuItem, setActiveMenuItem] = React.useState(null); const menuItemPosition = React.useRef({ left: 0, width: 0, top: 0, height: 0, }); const menuItemRefs = React.useRef(new Map()); const menuRef = React.useRef(null); // NEW: State to force re-evaluation when menuItemRefs content might have changed const [menuRefsVersion, setMenuRefsVersion] = React.useState(0); const isControlled = controlledExpanded !== undefined; const actualExpanded = isControlled ? controlledExpanded : expanded; const onExpandedChangeRef = React.useRef(onExpandedChange); React.useEffect(() => { onExpandedChangeRef.current = onExpandedChange; }, [onExpandedChange]); const handleExpandedChange = React.useCallback((value) => { if (!isControlled) { setExpanded(value); } onExpandedChangeRef.current?.(value); }, [isControlled]); // NEW: Callback to increment the version when a menu item ref is added/removed const notifyMenuItemRefChange = React.useCallback(() => { setMenuRefsVersion((prev) => prev + 1); }, []); // Helper function to encapsulate indicator positioning logic const updateIndicatorPosition = React.useCallback((id) => { const indicator = menuRef.current?.querySelector(".sidebar-menu-indicator"); if (id && menuRef.current) { const selectedItem = menuItemRefs.current.get(id); if (selectedItem) { const menuRect = menuRef.current.getBoundingClientRect(); const rect = selectedItem.getBoundingClientRect(); menuItemPosition.current = { left: rect.left - menuRect.left, width: rect.width, top: rect.top - menuRect.top, height: rect.height, }; if (indicator) { indicator.style.left = `${menuItemPosition.current.left}px`; indicator.style.width = `${menuItemPosition.current.width}px`; indicator.style.top = `${menuItemPosition.current.top}px`; indicator.style.height = `${menuItemPosition.current.height}px`; indicator.style.opacity = "1"; } } else { // If selectedItem is not found (e.g., not yet mounted or invalid ID) // Ensure the indicator is hidden until the item is ready if (indicator) { indicator.style.opacity = "0"; } } } else { // If no active ID, hide the indicator if (indicator) { indicator.style.opacity = "0"; } } }, [menuItemRefs, menuRef, menuItemPosition]); // Effect to set active menu item from URL React.useEffect(() => { const url = new URL(window.location.href); const searchParams = url.searchParams; const path = url.pathname; let potentialMenuItemValue = null; if (searchParams.has("component")) { potentialMenuItemValue = searchParams.get("component"); } else { const pathSegments = path.split("/").filter((segment) => segment); if (pathSegments.length > 0) { potentialMenuItemValue = pathSegments[pathSegments.length - 1]; } } setActiveMenuItem(potentialMenuItemValue); // No need to call updateIndicatorPosition directly here. // The useLayoutEffect below, which depends on menuRefsVersion, will handle it. }, [window.location.pathname, window.location.search]); // Primary useLayoutEffect for synchronous indicator updates React.useLayoutEffect(() => { // This effect runs whenever activeMenuItem changes OR when menuRefsVersion increments. // By depending on menuRefsVersion, we ensure that if an item registers its ref // AFTER activeMenuItem is set (e.g., on initial load/navigation), // this effect will // re-run and find the newly available ref. updateIndicatorPosition(activeMenuItem); }, [activeMenuItem, menuRefsVersion, menuRef, updateIndicatorPosition]); // Effect to re-adjust on window resize/layout changes React.useEffect(() => { const handleResize = () => { if (activeMenuItem) { updateIndicatorPosition(activeMenuItem); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [activeMenuItem, updateIndicatorPosition]); return (_jsx(SidebarContext.Provider, { value: { expanded: actualExpanded, onChange: handleExpandedChange, activeMenuItem, setActiveMenuItem, menuItemPosition, menuItemRefs, menuRef, updateIndicatorPosition, notifyMenuItemRefChange, // Expose the new notification function }, children: children })); } export function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider"); } return context; } export function Sidebar({ className, children, ...props }) { const { expanded } = useSidebar(); return (_jsx("div", { className: cn("h-full min-h-screen z-40 w-56 relative", // expanded ? "" : "w-16", "bg-background border-r shadow-sm", "fixed lg:sticky top-0 md:top-0", expanded ? "left-0" : "md:left-0 -left-full", className), role: "complementary", "data-collapsed": !expanded, ...props, children: children })); } export function SidebarTrigger({ className, ...props }) { const { expanded, onChange } = useSidebar(); return (_jsxs("button", { type: "button", className: cn("inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "fixed md:static z-50 left-4 top-20", className), onClick: () => onChange(!expanded), "aria-label": expanded ? "Close sidebar" : "Open sidebar", ...props, children: [_jsx("span", { className: "sr-only", children: expanded ? "Close sidebar" : "Open sidebar" }), expanded ? (_jsx(ChevronLeft, { className: "h-4 w-4" })) : (_jsx(ChevronRight, { className: "h-4 w-4" }))] })); } export function SidebarHeader({ className, children, ...props }) { const { expanded } = useSidebar(); return (_jsx("div", { className: cn("flex h-16 items-center border-b px-4", expanded ? "justify-between" : "justify-center", className), ...props, children: children })); } export function SidebarContent({ className, children, ...props }) { const scrollRef = React.useRef(null); return (_jsx("div", { className: cn("flex-1 overflow-hidden h-[calc(100vh-4rem)] space-y-4 ", className), ...props, children: _jsx("div", { ref: scrollRef, className: "h-full pb-12 overflow-auto scrollbar-hide ", children: children }) })); } export function SidebarGroup({ className, children, ...props }) { return (_jsx("div", { className: cn("px-2 py-4", className), ...props, children: children })); } export function SidebarGroupLabel({ className, children, ...props }) { const { expanded } = useSidebar(); if (!expanded) { return null; } return (_jsx("div", { className: cn("mb-2 px-2 text-md md:text-sm font-semibold md:font-bold tracking-tight", className), ...props, children: children })); } export function SidebarGroupContent({ className, children, ...props }) { return (_jsx("div", { className: cn("space-y-1", className), ...props, children: children })); } export function SidebarFooter({ className, children, ...props }) { const { expanded } = useSidebar(); return (_jsx("div", { className: cn("flex border-t p-4", expanded ? "flex-row items-center justify-between" : "flex-col justify-center", className), ...props, children: children })); } export function SidebarMenu({ className, children, ...props }) { const { menuRef } = useSidebar(); return ( // In your SidebarMenu component's div for the indicator: _jsxs("div", { ref: menuRef, className: cn("relative", className), ...props, children: [_jsx("div", { className: "sidebar-menu-indicator opacity-0 absolute ease-in-out \r\n rounded-md bg-primarylw/10 dark:bg-greedy/10 border border-primarylw dark:border-greedy" }), _jsx("div", { className: "sidebar-menu-indicator/10 opacity-0 absolute \r\n ease-in-out \r\n rounded-md bg-primarylw/10 dark:bg-greedy/10" }), " ", children] })); } export function SidebarMenuItem({ className, children, value, ...props }) { const itemRef = React.useRef(null); // NEW: Get notifyMenuItemRefChange from context const { activeMenuItem, menuItemRefs, notifyMenuItemRefChange } = useSidebar(); const menuItemId = value || React.useId(); const isActive = activeMenuItem === menuItemId; const isInView = useInView(itemRef, { once: false, amount: 0.5 }); // Register this menu item when it mounts // and NOTIFY the provider about the change React.useEffect(() => { if (itemRef.current) { menuItemRefs.current.set(menuItemId, itemRef.current); // Notify the provider that a ref has been added, potentially triggering // the useLayoutEffect if this item is the active one. notifyMenuItemRefChange(); } return () => { menuItemRefs.current.delete(menuItemId); // Also notify when a ref is removed (component unmounts) notifyMenuItemRefChange(); }; }, [menuItemRefs, menuItemId, notifyMenuItemRefChange]); // Added notifyMenuItemRefChange to deps return (_jsx(motion.div, { ref: itemRef, className: cn("mb-1 scrollbar-hide", className), "data-value": menuItemId, "data-state": isActive ? "active" : "inactive", initial: { scale: 1, opacity: 0.5, x: -0 }, animate: { scale: isInView ? 1 : 0.6, opacity: isInView ? 1 : 0.5, x: isInView ? 0 : -60, }, transition: { duration: 0.4, ease: "easeOut" }, ...props, children: children })); } export function SidebarMenuButton({ className, children, asChild = false, value, ...props }) { const { expanded, activeMenuItem, setActiveMenuItem, updateIndicatorPosition, } = useSidebar(); const menuItemId = value || React.useId(); const isActive = activeMenuItem === menuItemId; const handleClick = React.useCallback(() => { setActiveMenuItem(menuItemId); // Explicitly call updateIndicatorPosition immediately on click. // This provides immediate visual feedback for direct clicks, overriding // any potential slight delay from the useLayoutEffect waiting for version update. updateIndicatorPosition(menuItemId); if (props.onClick && typeof props.onClick === "function") { const dummyEvent = { currentTarget: {}, target: {}, preventDefault: () => { }, stopPropagation: () => { }, }; props.onClick(dummyEvent); } }, [menuItemId, setActiveMenuItem, updateIndicatorPosition, props.onClick]); const sharedClassName = "flex cursor-pointer items-center rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring "; if (!expanded) { if (asChild) { return (_jsx("div", { className: className, "data-active": isActive ? "true" : "false", onClick: handleClick, ...props, children: React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { ...child.props, className: cn(sharedClassName, "justify-center p-2", "hover:bg-primary/10 hover:scale-110", isActive ? "text-primary font-medium" : "", child.props?.className), }); } return child; }) })); } return (_jsx("div", { className: cn(sharedClassName, "justify-center p-2", "hover:bg-primary/10 hover:scale-110", isActive ? "text-primary font-medium" : "", className), "data-active": isActive ? "true" : "false", onClick: handleClick, ...props, children: React.Children.toArray(children).filter((child) => React.isValidElement(child) && typeof child.type !== "string") })); } if (asChild) { return (_jsx("div", { className: className, "data-active": isActive ? "true" : "false", onClick: handleClick, ...props, children: React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { ...child.props, className: cn(sharedClassName, "justify-start gap-2", "hover:bg-primary/10 hover:translate-x-1", isActive ? "text-primary font-medium" : "", child.props?.className), }); } return child; }) })); } return (_jsx("div", { className: cn(sharedClassName, "justify-start gap-2", "hover:bg-primary/10 hover:translate-x-1", isActive ? "text-primary font-medium" : "", className), "data-active": isActive ? "true" : "false", onClick: handleClick, ...props, children: children })); } export { Sidebar as SidebarRoot };