spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: client/src/components/ui/sidebar.tsx6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617"use client"1819import * as React from "react"20import { Slot } from "@radix-ui/react-slot"21import { cva, VariantProps } from "class-variance-authority"22import { PanelLeftIcon } from "lucide-react"2324import { useIsMobile } from "@/hooks/use-mobile"25import { cn } from "@/lib/utils"26import { Button } from "@/components/ui/button"27import { Input } from "@/components/ui/input"28import { Separator } from "@/components/ui/separator"29import {30 Sheet,31 SheetContent,32 SheetDescription,33 SheetHeader,34 SheetTitle,35} from "@/components/ui/sheet"36import { Skeleton } from "@/components/ui/skeleton"37import {38 Tooltip,39 TooltipContent,40 TooltipProvider,41 TooltipTrigger,42} from "@/components/ui/tooltip"4344const SIDEBAR_COOKIE_NAME = "sidebar_state"45const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 746const SIDEBAR_WIDTH = "16rem"47const SIDEBAR_WIDTH_MOBILE = "18rem"48const SIDEBAR_WIDTH_ICON = "3rem"49const SIDEBAR_KEYBOARD_SHORTCUT = "b"5051type SidebarContextProps = {52 state: "expanded" | "collapsed"53 open: boolean54 setOpen: (open: boolean) => void55 openMobile: boolean56 setOpenMobile: (open: boolean) => void57 isMobile: boolean58 toggleSidebar: () => void59}6061const SidebarContext = React.createContext<SidebarContextProps | null>(null)6263function useSidebar() {64 const context = React.useContext(SidebarContext)65 if (!context) {66 throw new Error("useSidebar must be used within a SidebarProvider.")67 }6869 return context70}7172function SidebarProvider({73 defaultOpen = true,74 open: openProp,75 onOpenChange: setOpenProp,76 className,77 style,78 children,79 ...props80}: React.ComponentProps<"div"> & {81 defaultOpen?: boolean82 open?: boolean83 onOpenChange?: (open: boolean) => void84}) {85 const isMobile = useIsMobile()86 const [openMobile, setOpenMobile] = React.useState(false)8788 // This is the internal state of the sidebar.89 // We use openProp and setOpenProp for control from outside the component.90 const [_open, _setOpen] = React.useState(defaultOpen)91 const open = openProp ?? _open92 const setOpen = React.useCallback(93 (value: boolean | ((value: boolean) => boolean)) => {94 const openState = typeof value === "function" ? value(open) : value95 if (setOpenProp) {96 setOpenProp(openState)97 } else {98 _setOpen(openState)99 }100101 // This sets the cookie to keep the sidebar state.102 document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`103 },104 [setOpenProp, open]105 )106107 // Helper to toggle the sidebar.108 const toggleSidebar = React.useCallback(() => {109 return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)110 }, [isMobile, setOpen, setOpenMobile])111112 // Adds a keyboard shortcut to toggle the sidebar.113 React.useEffect(() => {114 const handleKeyDown = (event: KeyboardEvent) => {115 if (116 event.key === SIDEBAR_KEYBOARD_SHORTCUT &&117 (event.metaKey || event.ctrlKey)118 ) {119 event.preventDefault()120 toggleSidebar()121 }122 }123124 window.addEventListener("keydown", handleKeyDown)125 return () => window.removeEventListener("keydown", handleKeyDown)126 }, [toggleSidebar])127128 // We add a state so that we can do data-state="expanded" or "collapsed".129 // This makes it easier to style the sidebar with Tailwind classes.130 const state = open ? "expanded" : "collapsed"131132 const contextValue = React.useMemo<SidebarContextProps>(133 () => ({134 state,135 open,136 setOpen,137 isMobile,138 openMobile,139 setOpenMobile,140 toggleSidebar,141 }),142 [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]143 )144145 return (146 <SidebarContext.Provider value={contextValue}>147 <TooltipProvider delayDuration={0}>148 <div149 data-slot="sidebar-wrapper"150 style={151 {152 "--sidebar-width": SIDEBAR_WIDTH,153 "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,154 ...style,155 } as React.CSSProperties156 }157 className={cn(158 "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",159 className160 )}161 {...props}162 >163 {children}164 </div>165 </TooltipProvider>166 </SidebarContext.Provider>167 )168}169170function Sidebar({171 side = "left",172 variant = "sidebar",173 collapsible = "offcanvas",174 className,175 children,176 ...props177}: React.ComponentProps<"div"> & {178 side?: "left" | "right"179 variant?: "sidebar" | "floating" | "inset"180 collapsible?: "offcanvas" | "icon" | "none"181}) {182 const { isMobile, state, openMobile, setOpenMobile } = useSidebar()183184 if (collapsible === "none") {185 return (186 <div187 data-slot="sidebar"188 className={cn(189 "bg-sidebar text-sidebar-foreground flex h-full w-[var(--sidebar-width)] flex-col",190 className191 )}192 {...props}193 >194 {children}195 </div>196 )197 }198199 if (isMobile) {200 return (201 <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>202 <SheetContent203 data-sidebar="sidebar"204 data-slot="sidebar"205 data-mobile="true"206 className="bg-sidebar text-sidebar-foreground w-[var(--sidebar-width)] p-0 [&>button]:hidden"207 style={208 {209 "--sidebar-width": SIDEBAR_WIDTH_MOBILE,210 } as React.CSSProperties211 }212 side={side}213 >214 <SheetHeader className="sr-only">215 <SheetTitle>Sidebar</SheetTitle>216 <SheetDescription>Displays the mobile sidebar.</SheetDescription>217 </SheetHeader>218 <div className="flex h-full w-full flex-col">{children}</div>219 </SheetContent>220 </Sheet>221 )222 }223224 return (225 <div226 className="group peer text-sidebar-foreground hidden md:block"227 data-state={state}228 data-collapsible={state === "collapsed" ? collapsible : ""}229 data-variant={variant}230 data-side={side}231 data-slot="sidebar"232 >233 {/* This is what handles the sidebar gap on desktop */}234 <div235 data-slot="sidebar-gap"236 className={cn(237 "relative w-[var(--sidebar-width)] bg-transparent transition-[width] duration-200 ease-linear",238 "group-data-[collapsible=offcanvas]:w-0",239 "group-data-[side=right]:rotate-180",240 variant === "floating" || variant === "inset"241 ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--spacing-4))]"242 : "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]"243 )}244 />245 <div246 data-slot="sidebar-container"247 className={cn(248 "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] duration-200 ease-linear md:flex",249 side === "left"250 ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"251 : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",252 // Adjust the padding for floating and inset variants.253 variant === "floating" || variant === "inset"254 ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--spacing-4)+2px)]"255 : "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l",256 className257 )}258 {...props}259 >260 <div261 data-sidebar="sidebar"262 data-slot="sidebar-inner"263 className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"264 >265 {children}266 </div>267 </div>268 </div>269 )270}271272function SidebarTrigger({273 className,274 onClick,275 ...props276}: React.ComponentProps<typeof Button>) {277 const { toggleSidebar } = useSidebar()278279 return (280 <Button281 data-sidebar="trigger"282 data-slot="sidebar-trigger"283 variant="ghost"284 size="icon"285 className={cn("h-7 w-7", className)}286 onClick={(event) => {287 onClick?.(event)288 toggleSidebar()289 }}290 {...props}291 >292 <PanelLeftIcon />293 <span className="sr-only">Toggle Sidebar</span>294 </Button>295 )296}297298function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {299 const { toggleSidebar } = useSidebar()300301 // Note: Tailwind v3.4 doesn't support "in-" selectors. So the rail won't work perfectly.302 return (303 <button304 data-sidebar="rail"305 data-slot="sidebar-rail"306 aria-label="Toggle Sidebar"307 tabIndex={-1}308 onClick={toggleSidebar}309 title="Toggle Sidebar"310 className={cn(311 "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",312 "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",313 "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",314 "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",315 "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",316 "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",317 className318 )}319 {...props}320 />321 )322}323324function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {325 return (326 <main327 data-slot="sidebar-inset"328 className={cn(329 "bg-background relative flex w-full flex-1 flex-col",330 "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",331 className332 )}333 {...props}334 />335 )336}337338function SidebarInput({339 className,340 ...props341}: React.ComponentProps<typeof Input>) {342 return (343 <Input344 data-slot="sidebar-input"345 data-sidebar="input"346 className={cn("bg-background h-8 w-full shadow-none", className)}347 {...props}348 />349 )350}351352function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {353 return (354 <div355 data-slot="sidebar-header"356 data-sidebar="header"357 className={cn("flex flex-col gap-2 p-2", className)}358 {...props}359 />360 )361}362363function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {364 return (365 <div366 data-slot="sidebar-footer"367 data-sidebar="footer"368 className={cn("flex flex-col gap-2 p-2", className)}369 {...props}370 />371 )372}373374function SidebarSeparator({375 className,376 ...props377}: React.ComponentProps<typeof Separator>) {378 return (379 <Separator380 data-slot="sidebar-separator"381 data-sidebar="separator"382 className={cn("bg-sidebar-border mx-2 w-auto", className)}383 {...props}384 />385 )386}387388function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {389 return (390 <div391 data-slot="sidebar-content"392 data-sidebar="content"393 className={cn(394 "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",395 className396 )}397 {...props}398 />399 )400}401402function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {403 return (404 <div405 data-slot="sidebar-group"406 data-sidebar="group"407 className={cn("relative flex w-full min-w-0 flex-col p-2", className)}408 {...props}409 />410 )411}412413function SidebarGroupLabel({414 className,415 asChild = false,416 ...props417}: React.ComponentProps<"div"> & { asChild?: boolean }) {418 const Comp = asChild ? Slot : "div"419420 return (421 <Comp422 data-slot="sidebar-group-label"423 data-sidebar="group-label"424 className={cn(425 "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0",426 "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",427 className428 )}429 {...props}430 />431 )432}433434function SidebarGroupAction({435 className,436 asChild = false,437 ...props438}: React.ComponentProps<"button"> & { asChild?: boolean }) {439 const Comp = asChild ? Slot : "button"440441 return (442 <Comp443 data-slot="sidebar-group-action"444 data-sidebar="group-action"445 className={cn(446 "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",447 // Increases the hit area of the button on mobile.448 "after:absolute after:-inset-2 md:after:hidden",449 "group-data-[collapsible=icon]:hidden",450 className451 )}452 {...props}453 />454 )455}456457function SidebarGroupContent({458 className,459 ...props460}: React.ComponentProps<"div">) {461 return (462 <div463 data-slot="sidebar-group-content"464 data-sidebar="group-content"465 className={cn("w-full text-sm", className)}466 {...props}467 />468 )469}470471function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {472 return (473 <ul474 data-slot="sidebar-menu"475 data-sidebar="menu"476 className={cn("flex w-full min-w-0 flex-col gap-1", className)}477 {...props}478 />479 )480}481482function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {483 return (484 <li485 data-slot="sidebar-menu-item"486 data-sidebar="menu-item"487 className={cn("group/menu-item relative", className)}488 {...props}489 />490 )491}492493const sidebarMenuButtonVariants = cva(494 "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:w-8! group-data-[collapsible=icon]:h-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",495 {496 variants: {497 variant: {498 default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",499 outline:500 "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",501 },502 size: {503 default: "h-8 text-sm",504 sm: "h-7 text-xs",505 lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",506 },507 },508 defaultVariants: {509 variant: "default",510 size: "default",511 },512 }513)514515function SidebarMenuButton({516 asChild = false,517 isActive = false,518 variant = "default",519 size = "default",520 tooltip,521 className,522 ...props523}: React.ComponentProps<"button"> & {524 asChild?: boolean525 isActive?: boolean526 tooltip?: string | React.ComponentProps<typeof TooltipContent>527} & VariantProps<typeof sidebarMenuButtonVariants>) {528 const Comp = asChild ? Slot : "button"529 const { isMobile, state } = useSidebar()530531 const button = (532 <Comp533 data-slot="sidebar-menu-button"534 data-sidebar="menu-button"535 data-size={size}536 data-active={isActive}537 className={cn(sidebarMenuButtonVariants({ variant, size }), className)}538 {...props}539 />540 )541542 if (!tooltip) {543 return button544 }545546 if (typeof tooltip === "string") {547 tooltip = {548 children: tooltip,549 }550 }551552 return (553 <Tooltip>554 <TooltipTrigger asChild>{button}</TooltipTrigger>555 <TooltipContent556 side="right"557 align="center"558 hidden={state !== "collapsed" || isMobile}559 {...tooltip}560 />561 </Tooltip>562 )563}564565function SidebarMenuAction({566 className,567 asChild = false,568 showOnHover = false,569 ...props570}: React.ComponentProps<"button"> & {571 asChild?: boolean572 showOnHover?: boolean573}) {574 const Comp = asChild ? Slot : "button"575576 return (577 <Comp578 data-slot="sidebar-menu-action"579 data-sidebar="menu-action"580 className={cn(581 "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",582 // Increases the hit area of the button on mobile.583 "after:absolute after:-inset-2 md:after:hidden",584 "peer-data-[size=sm]/menu-button:top-1",585 "peer-data-[size=default]/menu-button:top-1.5",586 "peer-data-[size=lg]/menu-button:top-2.5",587 "group-data-[collapsible=icon]:hidden",588 showOnHover &&589 "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",590 className591 )}592 {...props}593 />594 )595}596597function SidebarMenuBadge({598 className,599 ...props600}: React.ComponentProps<"div">) {601 return (602 <div603 data-slot="sidebar-menu-badge"604 data-sidebar="menu-badge"605 className={cn(606 "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",607 "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",608 "peer-data-[size=sm]/menu-button:top-1",609 "peer-data-[size=default]/menu-button:top-1.5",610 "peer-data-[size=lg]/menu-button:top-2.5",611 "group-data-[collapsible=icon]:hidden",612 className613 )}614 {...props}615 />616 )617}618619function SidebarMenuSkeleton({620 className,621 showIcon = false,622 ...props623}: React.ComponentProps<"div"> & {624 showIcon?: boolean625}) {626 // Random width between 50 to 90%.627 const width = React.useMemo(() => {628 return `${Math.floor(Math.random() * 40) + 50}%`629 }, [])630631 return (632 <div633 data-slot="sidebar-menu-skeleton"634 data-sidebar="menu-skeleton"635 className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}636 {...props}637 >638 {showIcon && (639 <Skeleton640 className="size-4 rounded-md"641 data-sidebar="menu-skeleton-icon"642 />643 )}644 <Skeleton645 className="h-4 max-w-[var(--skeleton-width)] flex-1"646 data-sidebar="menu-skeleton-text"647 style={648 {649 "--skeleton-width": width,650 } as React.CSSProperties651 }652 />653 </div>654 )655}656657function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {658 return (659 <ul660 data-slot="sidebar-menu-sub"661 data-sidebar="menu-sub"662 className={cn(663 "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",664 "group-data-[collapsible=icon]:hidden",665 className666 )}667 {...props}668 />669 )670}671672function SidebarMenuSubItem({673 className,674 ...props675}: React.ComponentProps<"li">) {676 return (677 <li678 data-slot="sidebar-menu-sub-item"679 data-sidebar="menu-sub-item"680 className={cn("group/menu-sub-item relative", className)}681 {...props}682 />683 )684}685686function SidebarMenuSubButton({687 asChild = false,688 size = "md",689 isActive = false,690 className,691 ...props692}: React.ComponentProps<"a"> & {693 asChild?: boolean694 size?: "sm" | "md"695 isActive?: boolean696}) {697 const Comp = asChild ? Slot : "a"698699 return (700 <Comp701 data-slot="sidebar-menu-sub-button"702 data-sidebar="menu-sub-button"703 data-size={size}704 data-active={isActive}705 className={cn(706 "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline outline-2 outline-transparent outline-offset-2 focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",707 "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",708 size === "sm" && "text-xs",709 size === "md" && "text-sm",710 "group-data-[collapsible=icon]:hidden",711 className712 )}713 {...props}714 />715 )716}717718export {719 Sidebar,720 SidebarContent,721 SidebarFooter,722 SidebarGroup,723 SidebarGroupAction,724 SidebarGroupContent,725 SidebarGroupLabel,726 SidebarHeader,727 SidebarInput,728 SidebarInset,729 SidebarMenu,730 SidebarMenuAction,731 SidebarMenuBadge,732 SidebarMenuButton,733 SidebarMenuItem,734 SidebarMenuSkeleton,735 SidebarMenuSub,736 SidebarMenuSubButton,737 SidebarMenuSubItem,738 SidebarProvider,739 SidebarRail,740 SidebarSeparator,741 SidebarTrigger,742 useSidebar,743}744