From 089130b0bc963f9f7331cfcd343f00f59a346719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:54:29 +0000 Subject: [PATCH] Split sidebar.tsx (726 LOC) into 4 modular files Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/components/ui/sidebar-context.tsx | 132 +++++ src/components/ui/sidebar-core.tsx | 181 +++++++ src/components/ui/sidebar-menu.tsx | 318 +++++++++++ src/components/ui/sidebar-parts.tsx | 81 +++ src/components/ui/sidebar.tsx | 730 +------------------------- 5 files changed, 727 insertions(+), 715 deletions(-) create mode 100644 src/components/ui/sidebar-context.tsx create mode 100644 src/components/ui/sidebar-core.tsx create mode 100644 src/components/ui/sidebar-menu.tsx create mode 100644 src/components/ui/sidebar-parts.tsx diff --git a/src/components/ui/sidebar-context.tsx b/src/components/ui/sidebar-context.tsx new file mode 100644 index 0000000..7406539 --- /dev/null +++ b/src/components/ui/sidebar-context.tsx @@ -0,0 +1,132 @@ +"use client" + +import { CSSProperties, ComponentProps, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react" +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { TooltipProvider } from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +export const SIDEBAR_WIDTH = "16rem" +export const SIDEBAR_WIDTH_MOBILE = "18rem" +export const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +export type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = createContext(null) + +export function useSidebar() { + const context = useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +export function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = useState(defaultOpen) + const open = openProp ?? _open + const setOpen = useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) +} diff --git a/src/components/ui/sidebar-core.tsx b/src/components/ui/sidebar-core.tsx new file mode 100644 index 0000000..3679772 --- /dev/null +++ b/src/components/ui/sidebar-core.tsx @@ -0,0 +1,181 @@ +"use client" + +import { CSSProperties, ComponentProps } from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { useSidebar, SIDEBAR_WIDTH_MOBILE } from "./sidebar-context" +import PanelLeftIcon from "lucide-react/dist/esm/icons/panel-left" + +export function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +export function SidebarTrigger({ + className, + onClick, + ...props +}: ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +export function SidebarRail({ className, ...props }: ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( + - ) -} - -function SidebarRail({ className, ...props }: ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() - - return ( -