Sidebar
The app frame for the dashboard layout, with header, grouped nav, sub-items, badges and a user footer. Collapses to icons.
import { Separator } from "@/components/ui/separator";import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger,} from "@/components/ui/sidebar";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const platform = [ { title: "Dashboard", icon: ( <IconPlaceholder lucide="LayoutDashboardIcon" tabler="IconDashboard" hugeicons="DashboardSquare01Icon" phosphor="SquaresFourIcon" remixicon="RiDashboardLine" /> ), active: true, }, { title: "Explore", icon: ( <IconPlaceholder lucide="SearchIcon" tabler="IconSearch" hugeicons="SearchIcon" phosphor="MagnifyingGlassIcon" remixicon="RiSearchLine" /> ), }, { title: "Feed", icon: ( <IconPlaceholder lucide="NewspaperIcon" tabler="IconNews" hugeicons="News01Icon" phosphor="NewspaperIcon" remixicon="RiNewspaperLine" /> ), badge: "3", }, { title: "Leaderboard", icon: ( <IconPlaceholder lucide="TrophyIcon" tabler="IconTrophy" hugeicons="Award01Icon" phosphor="TrophyIcon" remixicon="RiTrophyLine" /> ), },];
export default function Demo() { return ( // translateZ makes the sidebar's fixed positioning relative to this frame. <div className="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <SidebarProvider className="h-full min-h-0"> <Sidebar collapsible="icon"> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" tooltip="stockbreak"> <IconPlaceholder lucide="HomeIcon" tabler="IconHome" hugeicons="HomeIcon" phosphor="HouseIcon" remixicon="RiHomeLine" /> <span className="font-semibold group-data-[collapsible=icon]:hidden"> stockbreak </span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Platform</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> {platform.map((item) => ( <SidebarMenuItem key={item.title}> <SidebarMenuButton isActive={item.active} tooltip={item.title} > {item.icon} <span>{item.title}</span> </SidebarMenuButton> {item.badge ? ( <SidebarMenuBadge>{item.badge}</SidebarMenuBadge> ) : null} </SidebarMenuItem> ))} </SidebarMenu> </SidebarGroupContent> </SidebarGroup> <SidebarGroup> <SidebarGroupLabel>You</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton tooltip="Create index"> <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="PlusSignIcon" phosphor="PlusIcon" remixicon="RiAddLine" /> <span>Create index</span> </SidebarMenuButton> </SidebarMenuItem> <SidebarMenuItem> <SidebarMenuButton tooltip="Portfolio"> <IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /> <span>Portfolio</span> </SidebarMenuButton> <SidebarMenuSub> <SidebarMenuSubItem> <SidebarMenuSubButton href="#positions"> Positions </SidebarMenuSubButton> </SidebarMenuSubItem> <SidebarMenuSubItem> <SidebarMenuSubButton href="#created"> Created </SidebarMenuSubButton> </SidebarMenuSubItem> </SidebarMenuSub> </SidebarMenuItem> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </SidebarContent> <SidebarFooter> <span className="px-2 text-xs text-muted-foreground group-data-[collapsible=icon]:hidden"> Dewi Lestari </span> </SidebarFooter> <SidebarRail /> </Sidebar> <SidebarInset> <header className="flex h-12 items-center gap-2 border-b border-border px-3"> <SidebarTrigger /> <Separator orientation="vertical" className="h-4" /> <span className="text-sm text-muted-foreground">Dashboard</span> </header> <div className="flex-1 p-4 text-sm text-muted-foreground"> Toggle with the button or Cmd/Ctrl+B. </div> </SidebarInset> </SidebarProvider> </div> );}<script setup lang="ts">import { Building2Icon, HomeIcon, LayoutDashboardIcon, BookOpenIcon, PlusIcon, SearchIcon, TargetIcon } from "@lucide/vue";import { Separator } from "@/components/ui/separator";import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger,} from "@/components/ui/sidebar";
const platform = [ { title: "Dashboard", icon: LayoutDashboardIcon, active: true }, { title: "Explore", icon: SearchIcon }, { title: "Feed", icon: BookOpenIcon, badge: "3" }, { title: "Leaderboard", icon: TargetIcon },];</script>
<template> <!-- translateZ makes the sidebar's fixed positioning relative to this frame. --> <div class="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full" > <SidebarProvider class="h-full min-h-0"> <Sidebar collapsible="icon"> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" tooltip="stockbreak"> <HomeIcon /> <span class="font-semibold group-data-[collapsible=icon]:hidden">stockbreak</span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Platform</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <SidebarMenuItem v-for="item in platform" :key="item.title"> <SidebarMenuButton :is-active="item.active" :tooltip="item.title"> <component :is="item.icon" /> <span>{{ item.title }}</span> </SidebarMenuButton> <SidebarMenuBadge v-if="item.badge">{{ item.badge }}</SidebarMenuBadge> </SidebarMenuItem> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> <SidebarGroup> <SidebarGroupLabel>You</SidebarGroupLabel> <SidebarGroupContent> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton tooltip="Create index"> <PlusIcon /> <span>Create index</span> </SidebarMenuButton> </SidebarMenuItem> <SidebarMenuItem> <SidebarMenuButton tooltip="Portfolio"> <Building2Icon /> <span>Portfolio</span> </SidebarMenuButton> <SidebarMenuSub> <SidebarMenuSubItem> <SidebarMenuSubButton href="#positions">Positions</SidebarMenuSubButton> </SidebarMenuSubItem> <SidebarMenuSubItem> <SidebarMenuSubButton href="#created">Created</SidebarMenuSubButton> </SidebarMenuSubItem> </SidebarMenuSub> </SidebarMenuItem> </SidebarMenu> </SidebarGroupContent> </SidebarGroup> </SidebarContent> <SidebarFooter> <span class="px-2 text-xs text-muted-foreground group-data-[collapsible=icon]:hidden">Dewi Lestari</span> </SidebarFooter> <SidebarRail /> </Sidebar> <SidebarInset> <header class="flex h-12 items-center gap-2 border-b border-border px-3"> <SidebarTrigger /> <Separator orientation="vertical" class="h-4" /> <span class="text-sm text-muted-foreground">Dashboard</span> </header> <div class="flex-1 p-4 text-sm text-muted-foreground">Toggle with the button or Cmd/Ctrl+B.</div> </SidebarInset> </SidebarProvider> </div></template><script lang="ts"> import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import { Separator } from "$lib/components/ui/separator"; import * as Sidebar from "$lib/components/ui/sidebar";
const platform = [ { title: "Dashboard", icon: "dashboard", active: true }, { title: "Explore", icon: "search" }, { title: "Feed", icon: "feed", badge: "3" }, { title: "Leaderboard", icon: "trophy" }, ];</script>
{#snippet icon(name: string)} {#if name === "dashboard"} <IconPlaceholder lucide="LayoutDashboardIcon" tabler="IconDashboard" hugeicons="DashboardSquare01Icon" phosphor="SquaresFourIcon" remixicon="RiDashboardLine" /> {:else if name === "search"} <IconPlaceholder lucide="SearchIcon" tabler="IconSearch" hugeicons="SearchIcon" phosphor="MagnifyingGlassIcon" remixicon="RiSearchLine" /> {:else if name === "feed"} <IconPlaceholder lucide="NewspaperIcon" tabler="IconNews" hugeicons="News01Icon" phosphor="NewspaperIcon" remixicon="RiNewspaperLine" /> {:else} <IconPlaceholder lucide="TrophyIcon" tabler="IconTrophy" hugeicons="Award01Icon" phosphor="TrophyIcon" remixicon="RiTrophyLine" /> {/if}{/snippet}
<!-- translateZ makes the sidebar's fixed positioning relative to this frame. --><div class="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <Sidebar.Provider class="h-full min-h-0"> <Sidebar.Root collapsible="icon"> <Sidebar.Header> <Sidebar.Menu> <Sidebar.MenuItem> <Sidebar.MenuButton size="lg" tooltipContent="stockbreak"> <IconPlaceholder lucide="HomeIcon" tabler="IconHome" hugeicons="HomeIcon" phosphor="HouseIcon" remixicon="RiHomeLine" /> <span class="font-semibold group-data-[collapsible=icon]:hidden">stockbreak</span> </Sidebar.MenuButton> </Sidebar.MenuItem> </Sidebar.Menu> </Sidebar.Header> <Sidebar.Content> <Sidebar.Group> <Sidebar.GroupLabel>Platform</Sidebar.GroupLabel> <Sidebar.GroupContent> <Sidebar.Menu> {#each platform as item (item.title)} <Sidebar.MenuItem> <Sidebar.MenuButton isActive={item.active} tooltipContent={item.title}> {@render icon(item.icon)} <span>{item.title}</span> </Sidebar.MenuButton> {#if item.badge} <Sidebar.MenuBadge>{item.badge}</Sidebar.MenuBadge> {/if} </Sidebar.MenuItem> {/each} </Sidebar.Menu> </Sidebar.GroupContent> </Sidebar.Group> <Sidebar.Group> <Sidebar.GroupLabel>You</Sidebar.GroupLabel> <Sidebar.GroupContent> <Sidebar.Menu> <Sidebar.MenuItem> <Sidebar.MenuButton tooltipContent="Create index"> <IconPlaceholder lucide="PlusIcon" tabler="IconPlus" hugeicons="PlusSignIcon" phosphor="PlusIcon" remixicon="RiAddLine" /> <span>Create index</span> </Sidebar.MenuButton> </Sidebar.MenuItem> <Sidebar.MenuItem> <Sidebar.MenuButton tooltipContent="Portfolio"> <IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /> <span>Portfolio</span> </Sidebar.MenuButton> <Sidebar.MenuSub> <Sidebar.MenuSubItem> <Sidebar.MenuSubButton href="#positions">Positions</Sidebar.MenuSubButton> </Sidebar.MenuSubItem> <Sidebar.MenuSubItem> <Sidebar.MenuSubButton href="#created">Created</Sidebar.MenuSubButton> </Sidebar.MenuSubItem> </Sidebar.MenuSub> </Sidebar.MenuItem> </Sidebar.Menu> </Sidebar.GroupContent> </Sidebar.Group> </Sidebar.Content> <Sidebar.Footer> <span class="px-2 text-xs text-muted-foreground group-data-[collapsible=icon]:hidden"> Dewi Lestari </span> </Sidebar.Footer> <Sidebar.Rail /> </Sidebar.Root> <Sidebar.Inset> <header class="flex h-12 items-center gap-2 border-b border-border px-3"> <Sidebar.Trigger /> <Separator orientation="vertical" class="h-4" /> <span class="text-sm text-muted-foreground">Dashboard</span> </header> <div class="flex-1 p-4 text-sm text-muted-foreground"> Toggle with the button or Cmd/Ctrl+B. </div> </Sidebar.Inset> </Sidebar.Provider></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/sidebarpnpm dlx shadcn@latest add @edmi-ui/sidebaryarn dlx shadcn@latest add @edmi-ui/sidebarbunx shadcn@latest add @edmi-ui/sidebarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/react class-variance-authorityTerminal window pnpm add @base-ui/react class-variance-authorityTerminal window yarn add @base-ui/react class-variance-authorityTerminal window bun add @base-ui/react class-variance-authorityAdd the Edmi components this one builds on:
button,input,separator,sheet,skeleton,tooltip,use-mobile.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/sidebarpnpm dlx shadcn-vue@latest add @edmi-ui/sidebaryarn dlx shadcn-vue@latest add @edmi-ui/sidebarbunx shadcn-vue@latest add @edmi-ui/sidebarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui class-variance-authority @vueuse/coreTerminal window pnpm add reka-ui class-variance-authority @vueuse/coreTerminal window yarn add reka-ui class-variance-authority @vueuse/coreTerminal window bun add reka-ui class-variance-authority @vueuse/coreAdd the Edmi components this one builds on:
button,input,separator,sheet,skeleton,tooltip.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sidebar.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sidebar.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sidebar.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sidebar.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,input,separator,sheet,skeleton,tooltip,use-mobile.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Sidebar, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider,} from "@/components/ui/sidebar";
<div className="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <SidebarProvider className="h-full min-h-0"> <Sidebar collapsible="icon"> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" tooltip="stockbreak"> <HouseIcon /> <span className="font-semibold group-data-[collapsible=icon]:hidden"> stockbreak </span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> {/* … */} </Sidebar> {/* … */} </SidebarProvider></div><script setup lang="ts">import { Sidebar, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider,} from "@/components/ui/sidebar";</script>
<template> <div class="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full" > <SidebarProvider class="h-full min-h-0"> <Sidebar collapsible="icon"> <SidebarHeader> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" tooltip="stockbreak"> <HomeIcon /> <span class="font-semibold group-data-[collapsible=icon]:hidden">stockbreak</span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarHeader> <!-- … --> </Sidebar> <!-- … --> </SidebarProvider> </div></template><script lang="ts"> import * as Sidebar from "$lib/components/ui/sidebar";</script>
<div class="relative h-[460px] w-full overflow-hidden rounded-xl border border-border [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <Sidebar.Provider class="h-full min-h-0"> <Sidebar.Root collapsible="icon"> <Sidebar.Header> <Sidebar.Menu> <Sidebar.MenuItem> <Sidebar.MenuButton size="lg" tooltipContent="stockbreak"> <HouseIcon /> <span class="font-semibold group-data-[collapsible=icon]:hidden">stockbreak</span> </Sidebar.MenuButton> </Sidebar.MenuItem> </Sidebar.Menu> </Sidebar.Header> <!-- … --> </Sidebar.Root> <!-- … --> </Sidebar.Provider></div>Component source
Section titled “Component source”"use client";
import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";import * as React from "react";import { SidebarIcon } from "@phosphor-icons/react";import { useIsMobile } from "@/registry/edmi/hooks/use-mobile";import { Button } from "@/registry/edmi/ui/button";import { Input } from "@/registry/edmi/ui/input";import { Separator } from "@/registry/edmi/ui/separator";import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle,} from "@/registry/edmi/ui/sheet";import { Skeleton } from "@/registry/edmi/ui/skeleton";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/registry/edmi/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;const SIDEBAR_WIDTH = "16rem";const SIDEBAR_WIDTH_MOBILE = "18rem";const SIDEBAR_WIDTH_ICON = "3rem";const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = { state: "expanded" | "collapsed"; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void;};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider."); }
return context;}
function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props}: React.ComponentProps<"div"> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void;}) { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; const setOpen = React.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 = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen]);
// Adds a keyboard shortcut to toggle the sidebar. React.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 = React.useMemo<SidebarContextProps>( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, }), [state, open, setOpen, isMobile, openMobile, toggleSidebar], );
return ( <SidebarContext.Provider value={contextValue}> <div data-slot="sidebar-wrapper" style={ { "--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, ...style, } as React.CSSProperties } className={cn( "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", className, )} {...props} > {children} </div> </SidebarContext.Provider> );}
function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, dir, ...props}: React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none";}) { const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") { return ( <div data-slot="sidebar" className={cn( "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", className, )} {...props} > {children} </div> ); }
if (isMobile) { return ( <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> <SheetContent dir={dir} data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" style={ { "--sidebar-width": SIDEBAR_WIDTH_MOBILE, } as React.CSSProperties } side={side} > <SheetHeader className="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div className="flex h-full w-full flex-col">{children}</div> </SheetContent> </Sheet> ); }
return ( <div className="group peer hidden text-sidebar-foreground md:block" data-state={state} data-collapsible={state === "collapsed" ? collapsible : ""} data-variant={variant} data-side={side} data-slot="sidebar" > {/* This is what handles the sidebar gap on desktop */} <div data-slot="sidebar-gap" className={cn( "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", )} /> <div data-slot="sidebar-container" data-side={side} className={cn( "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex", // Adjust the padding for floating and inset variants. variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", className, )} {...props} > <div data-sidebar="sidebar" data-slot="sidebar-inner" className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-xl group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border" > {children} </div> </div> </div> );}
function SidebarTrigger({ className, onClick, ...props}: React.ComponentProps<typeof Button>) { const { toggleSidebar } = useSidebar();
return ( <Button data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" size="icon-sm" className={cn(className)} onClick={(event) => { onClick?.(event); toggleSidebar(); }} {...props} > <SidebarIcon className="rtl:rotate-180" /> <span className="sr-only">Toggle Sidebar</span> </Button> );}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { const { toggleSidebar } = useSidebar();
return ( <button data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" tabIndex={-1} onClick={toggleSidebar} title="Toggle Sidebar" className={cn( "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className, )} {...props} /> );}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { return ( <main data-slot="sidebar-inset" className={cn( "relative flex w-full flex-1 flex-col bg-background 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]:border md:peer-data-[variant=inset]:border-border md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className, )} {...props} /> );}
function SidebarInput({ className, ...props}: React.ComponentProps<typeof Input>) { return ( <Input data-slot="sidebar-input" data-sidebar="input" className={cn("h-8 w-full", className)} {...props} /> );}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-header" data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} /> );}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-footer" data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} /> );}
function SidebarSeparator({ className, ...props}: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="sidebar-separator" data-sidebar="separator" className={cn("mx-2 w-auto bg-sidebar-border", className)} {...props} /> );}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-content" data-sidebar="content" className={cn( "no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className, )} {...props} /> );}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-group" data-sidebar="group" className={cn("relative flex w-full min-w-0 flex-col p-2", className)} {...props} /> );}
function SidebarGroupLabel({ className, render, ...props}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) { return useRender({ defaultTagName: "div", props: mergeProps<"div">( { className: cn( "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", className, ), }, props, ), render, state: { slot: "sidebar-group-label", sidebar: "group-label", }, });}
function SidebarGroupAction({ className, render, ...props}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) { return useRender({ defaultTagName: "button", props: mergeProps<"button">( { className: cn( "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", className, ), }, props, ), render, state: { slot: "sidebar-group-action", sidebar: "group-action", }, });}
function SidebarGroupContent({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} /> );}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { return ( <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-0", className)} {...props} /> );}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { return ( <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} /> );}
const sidebarMenuButtonVariants = cva( "peer/menu-button group/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-[7px] p-2 text-left text-[13.5px] ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground data-[active]:bg-sidebar-accent data-[active]:font-medium data-[active]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", { variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: "border border-sidebar-border bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", }, size: { default: "h-8 text-[13.5px]", sm: "h-7 text-xs", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", }, }, defaultVariants: { variant: "default", size: "default", }, },);
function SidebarMenuButton({ render, isActive = false, variant = "default", size = "default", tooltip, className, ...props}: useRender.ComponentProps<"button"> & React.ComponentProps<"button"> & { isActive?: boolean; tooltip?: string | React.ComponentProps<typeof TooltipContent>; } & VariantProps<typeof sidebarMenuButtonVariants>) { const { isMobile, state } = useSidebar(); const comp = useRender({ defaultTagName: "button", props: mergeProps<"button">( { className: cn(sidebarMenuButtonVariants({ variant, size }), className), }, props, ), render: !tooltip ? render : <TooltipTrigger render={render} />, state: { slot: "sidebar-menu-button", sidebar: "menu-button", size, active: isActive, }, });
if (!tooltip) { return comp; }
if (typeof tooltip === "string") { tooltip = { children: tooltip, }; }
return ( <Tooltip> {comp} <TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} /> </Tooltip> );}
function SidebarMenuAction({ className, render, showOnHover = false, ...props}: useRender.ComponentProps<"button"> & React.ComponentProps<"button"> & { showOnHover?: boolean; }) { return useRender({ defaultTagName: "button", props: mergeProps<"button">( { className: cn( "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", showOnHover && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active]/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0", className, ), }, props, ), render, state: { slot: "sidebar-menu-action", sidebar: "menu-action", }, });}
function SidebarMenuBadge({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" className={cn( "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--brand)_30%,var(--popover))] bg-brand-soft px-1.5 font-mono text-[11px] font-medium text-brand-text tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1", className, )} {...props} /> );}
function SidebarMenuSkeleton({ className, showIcon = false, ...props}: React.ComponentProps<"div"> & { showIcon?: boolean;}) { // Random width between 50 to 90%. const [width] = React.useState(() => { return `${Math.floor(Math.random() * 40) + 50}%`; });
return ( <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} {...props} > {showIcon && ( <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> )} <Skeleton className="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style={ { "--skeleton-width": width, } as React.CSSProperties } /> </div> );}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { return ( <ul data-slot="sidebar-menu-sub" data-sidebar="menu-sub" className={cn( "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden", className, )} {...props} /> );}
function SidebarMenuSubItem({ className, ...props}: React.ComponentProps<"li">) { return ( <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" className={cn("group/menu-sub-item relative", className)} {...props} /> );}
function SidebarMenuSubButton({ render, size = "md", isActive = false, className, ...props}: useRender.ComponentProps<"a"> & React.ComponentProps<"a"> & { size?: "sm" | "md"; isActive?: boolean; }) { return useRender({ defaultTagName: "a", props: mergeProps<"a">( { className: cn( "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-[7px] px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-[active]:bg-sidebar-accent data-[active]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", className, ), }, props, ), render, state: { slot: "sidebar-menu-sub-button", sidebar: "menu-sub-button", size, active: isActive, }, });}
export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar,};<script setup lang="ts">import type { SidebarProps } from "."import { cn } from "@/registry/edmi/lib/utils"import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/registry/edmi/ui/sheet"import { SIDEBAR_WIDTH_MOBILE, useSidebar } from "./utils"
defineOptions({ inheritAttrs: false,})
const props = withDefaults(defineProps<SidebarProps>(), { side: "left", variant: "sidebar", collapsible: "offcanvas",})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()</script>
<template> <div v-if="collapsible === 'none'" data-slot="sidebar" :class="cn('flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground', props.class)" v-bind="$attrs" > <slot /> </div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile"> <SheetContent data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" :side="side" class="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" :style="{ '--sidebar-width': SIDEBAR_WIDTH_MOBILE, }" > <SheetHeader class="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div class="flex h-full w-full flex-col"> <slot /> </div> </SheetContent> </Sheet>
<div v-else class="group peer hidden text-sidebar-foreground md:block" data-slot="sidebar" :data-state="state" :data-collapsible="state === 'collapsed' ? collapsible : ''" :data-variant="variant" :data-side="side" > <!-- This is what handles the sidebar gap on desktop --> <div :class="cn( 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear', 'group-data-[collapsible=offcanvas]:w-0', 'group-data-[side=right]:rotate-180', variant === 'floating' || variant === 'inset' ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)', )" /> <div :class="cn( 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex', side === 'left' ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', // Adjust the padding for floating and inset variants. variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', props.class, )" v-bind="$attrs" > <div data-sidebar="sidebar" data-slot="sidebar-inner" class="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-xl group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border" > <slot /> </div> </div> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-content" data-sidebar="content" :class="cn( `no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden`, props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-footer" data-sidebar="footer" :class="cn('flex flex-col gap-2 p-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-group" data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes["class"]}>()</script>
<template> <Primitive data-slot="sidebar-group-action" data-sidebar="group-action" :as="as" :as-child="asChild" :class="cn( `absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0`, props.class, )" > <slot /> </Primitive></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-group-content" data-sidebar="group-content" :class="cn('w-full text-sm', props.class)" > <slot /> </div></template><script setup lang="ts">import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes["class"]}>()</script>
<template> <Primitive data-slot="sidebar-group-label" data-sidebar="group-label" :as="as" :as-child="asChild" :class="cn( `flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0`, props.class)" > <slot /> </Primitive></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-header" data-sidebar="header" :class="cn('flex flex-col gap-2 p-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Input } from "@/registry/edmi/ui/input"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <Input data-slot="sidebar-input" data-sidebar="input" :class="cn( `h-8 w-full`, props.class, )" > <slot /> </Input></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <main data-slot="sidebar-inset" :class="cn( `relative flex w-full flex-1 flex-col bg-background 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]:border md:peer-data-[variant=inset]:border-border md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2`, props.class, )" > <slot /> </main></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <ul data-slot="sidebar-menu" data-sidebar="menu" :class="cn( `flex w-full min-w-0 flex-col gap-0`, props.class)" > <slot /> </ul></template><script setup lang="ts">import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<PrimitiveProps & { showOnHover?: boolean class?: HTMLAttributes["class"]}>(), { as: "button",})</script>
<template> <Primitive data-slot="sidebar-menu-action" data-sidebar="menu-action" :class="cn( `absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0`, showOnHover && `group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0`, props.class, )" :as="as" :as-child="asChild" > <slot /> </Primitive></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" :class="cn( `pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--brand)_30%,var(--popover))] bg-brand-soft px-1.5 font-mono text-[11px] font-medium text-brand-text tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1`, props.class, )" > <slot /> </div></template><script setup lang="ts">import type { Component } from "vue"import type { SidebarMenuButtonProps } from "./SidebarMenuButtonChild.vue"import { reactiveOmit } from "@vueuse/core"import { Tooltip, TooltipContent, TooltipTrigger } from "@/registry/edmi/ui/tooltip"import SidebarMenuButtonChild from "./SidebarMenuButtonChild.vue"import { useSidebar } from "./utils"
defineOptions({ inheritAttrs: false,})
const props = withDefaults(defineProps<SidebarMenuButtonProps & { tooltip?: string | Component}>(), { as: "button", variant: "default", size: "default",})
const { isMobile, state } = useSidebar()
const delegatedProps = reactiveOmit(props, "tooltip")</script>
<template> <SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }"> <slot /> </SidebarMenuButtonChild>
<Tooltip v-else> <TooltipTrigger as-child> <SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }"> <slot /> </SidebarMenuButtonChild> </TooltipTrigger> <TooltipContent side="right" align="center" :hidden="state !== 'collapsed' || isMobile" > <template v-if="typeof tooltip === 'string'"> {{ tooltip }} </template> <component :is="tooltip" v-else /> </TooltipContent> </Tooltip></template><script setup lang="ts">import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { SidebarMenuButtonVariants } from "."import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { sidebarMenuButtonVariants } from "."
export interface SidebarMenuButtonProps extends PrimitiveProps { variant?: SidebarMenuButtonVariants["variant"] size?: SidebarMenuButtonVariants["size"] isActive?: boolean class?: HTMLAttributes["class"]}
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), { as: "button", variant: "default", size: "default",})</script>
<template> <Primitive data-slot="sidebar-menu-button" data-sidebar="menu-button" :data-size="size" :data-active="isActive" :class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)" :as="as" :as-child="asChild" v-bind="$attrs" > <slot /> </Primitive></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <li data-slot="sidebar-menu-item" data-sidebar="menu-item" :class="cn('group/menu-item relative', props.class)" > <slot /> </li></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Skeleton } from "@/registry/edmi/ui/skeleton"
const props = defineProps<{ showIcon?: boolean class?: HTMLAttributes["class"]}>()
const width = computed(() => { return `${Math.floor(Math.random() * 40) + 50}%`})</script>
<template> <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" :class="cn( `flex h-8 items-center gap-2 rounded-md px-2`, props.class)" > <Skeleton v-if="showIcon" class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
<Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" :style="{ '--skeleton-width': width }" /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <ul data-slot="sidebar-menu-sub" data-sidebar="menu-badge" :class="cn( `mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden`, props.class, )" > <slot /> </ul></template><script setup lang="ts">import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<PrimitiveProps & { size?: "sm" | "md" isActive?: boolean class?: HTMLAttributes["class"]}>(), { as: "a", size: "md",})</script>
<template> <Primitive data-slot="sidebar-menu-sub-button" data-sidebar="menu-sub-button" :as="as" :as-child="asChild" :data-size="size" :data-active="isActive" :class="cn( `flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-[7px] px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground`, props.class, )" > <slot /> </Primitive></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" :class="cn('group/menu-sub-item relative', props.class)" > <slot /> </li></template><script setup lang="ts">import type { HTMLAttributes, Ref } from "vue"import { defaultDocument, useEventListener, useMediaQuery, useVModel } from "@vueuse/core"import { TooltipProvider } from "reka-ui"import { computed, ref } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from "./utils"
const props = withDefaults(defineProps<{ defaultOpen?: boolean open?: boolean class?: HTMLAttributes["class"]}>(), { defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`), open: undefined,})
const emits = defineEmits<{ "update:open": [open: boolean]}>()
const isMobile = useMediaQuery("(max-width: 768px)")const openMobile = ref(false)
const open = useVModel(props, "open", emits, { defaultValue: props.defaultOpen ?? false, passive: (props.open === undefined) as false,}) as Ref<boolean>
function setOpen(value: boolean) { open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`}
function setOpenMobile(value: boolean) { openMobile.value = value}
// Helper to toggle the sidebar.function toggleSidebar() { return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)}
useEventListener("keydown", (event: KeyboardEvent) => { if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault() 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 = computed(() => open.value ? "expanded" : "collapsed")
provideSidebarContext({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar,})</script>
<template> <TooltipProvider :delay-duration="0"> <div data-slot="sidebar-wrapper" :style="{ '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, }" :class="cn( `group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar`, props.class)" v-bind="$attrs" > <slot /> </div> </TooltipProvider></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useSidebar } from "./utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { toggleSidebar } = useSidebar()</script>
<template> <button data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" :tabindex="-1" title="Toggle Sidebar" :class="cn( `absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2`, `in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize`, `[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize`, `group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar`, `[[data-side=left][data-collapsible=offcanvas]_&]:-right-2`, `[[data-side=right][data-collapsible=offcanvas]_&]:-left-2`, props.class, )" @click="toggleSidebar" > <slot /> </button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Separator } from "@/registry/edmi/ui/separator"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <Separator data-slot="sidebar-separator" data-sidebar="separator" :class="cn( `mx-2 w-auto bg-sidebar-border`, props.class)" > <slot /> </Separator></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { PanelLeft } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { useSidebar } from "./utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { toggleSidebar } = useSidebar()</script>
<template> <Button data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" size="icon-sm" :class="cn(props.class)" @click="toggleSidebar" > <PanelLeft class="rtl:rotate-180" /> <span class="sr-only">Toggle Sidebar</span> </Button></template>import { createContext } from "reka-ui";import type { ComputedRef, Ref } from "vue";
export const SIDEBAR_COOKIE_NAME = "sidebar_state";export 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";export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
export const [useSidebar, provideSidebarContext] = createContext<{ state: ComputedRef<"expanded" | "collapsed">; open: Ref<boolean>; setOpen: (value: boolean) => void; isMobile: Ref<boolean>; openMobile: Ref<boolean>; setOpenMobile: (value: boolean) => void; toggleSidebar: () => void;}>("Sidebar");import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";import type { HTMLAttributes } from "vue";
export interface SidebarProps { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; class?: HTMLAttributes["class"];}
export { default as Sidebar } from "./Sidebar.vue";export { default as SidebarContent } from "./SidebarContent.vue";export { default as SidebarFooter } from "./SidebarFooter.vue";export { default as SidebarGroup } from "./SidebarGroup.vue";export { default as SidebarGroupAction } from "./SidebarGroupAction.vue";export { default as SidebarGroupContent } from "./SidebarGroupContent.vue";export { default as SidebarGroupLabel } from "./SidebarGroupLabel.vue";export { default as SidebarHeader } from "./SidebarHeader.vue";export { default as SidebarInput } from "./SidebarInput.vue";export { default as SidebarInset } from "./SidebarInset.vue";export { default as SidebarMenu } from "./SidebarMenu.vue";export { default as SidebarMenuAction } from "./SidebarMenuAction.vue";export { default as SidebarMenuBadge } from "./SidebarMenuBadge.vue";export { default as SidebarMenuButton } from "./SidebarMenuButton.vue";export { default as SidebarMenuItem } from "./SidebarMenuItem.vue";export { default as SidebarMenuSkeleton } from "./SidebarMenuSkeleton.vue";export { default as SidebarMenuSub } from "./SidebarMenuSub.vue";export { default as SidebarMenuSubButton } from "./SidebarMenuSubButton.vue";export { default as SidebarMenuSubItem } from "./SidebarMenuSubItem.vue";export { default as SidebarProvider } from "./SidebarProvider.vue";export { default as SidebarRail } from "./SidebarRail.vue";export { default as SidebarSeparator } from "./SidebarSeparator.vue";export { default as SidebarTrigger } from "./SidebarTrigger.vue";
export { useSidebar } from "./utils";
export const sidebarMenuButtonVariants = cva( "peer/menu-button group/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-[7px] p-2 text-left text-[13.5px] ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", { variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: "border border-sidebar-border bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", }, size: { default: "h-8 text-[13.5px]", sm: "h-7 text-xs", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", }, }, defaultVariants: { variant: "default", size: "default", }, },);
export type SidebarMenuButtonVariants = VariantProps< typeof sidebarMenuButtonVariants>;export const SIDEBAR_COOKIE_NAME = "sidebar_state";export 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";export const SIDEBAR_KEYBOARD_SHORTCUT = "b";import { getContext, setContext } from "svelte";import { IsMobile } from "$lib/registry/hooks/is-mobile.svelte.js";import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
type Getter<T> = () => T;
export type SidebarStateProps = { /** * A getter function that returns the current open state of the sidebar. * We use a getter function here to support `bind:open` on the `Sidebar.Provider` * component. */ open: Getter<boolean>;
/** * A function that sets the open state of the sidebar. To support `bind:open`, we need * a source of truth for changing the open state to ensure it will be synced throughout * the sub-components and any `bind:` references. */ setOpen: (open: boolean) => void;};
class SidebarState { readonly props: SidebarStateProps; open = $derived.by(() => this.props.open()); openMobile = $state(false); setOpen: SidebarStateProps["setOpen"]; #isMobile: IsMobile; state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
constructor(props: SidebarStateProps) { this.setOpen = props.setOpen; this.#isMobile = new IsMobile(); this.props = props; }
// Convenience getter for checking if the sidebar is mobile // without this, we would need to use `sidebar.isMobile.current` everywhere get isMobile() { return this.#isMobile.current; }
// Event handler to apply to the `<svelte:window>` handleShortcutKeydown = (e: KeyboardEvent) => { if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { e.preventDefault(); this.toggle(); } };
setOpenMobile = (value: boolean) => { this.openMobile = value; };
toggle = () => { if (this.#isMobile.current) { this.openMobile = !this.openMobile; return; } this.setOpen(!this.open); };}
const SYMBOL_KEY = "scn-sidebar";
/** * Instantiates a new `SidebarState` instance and sets it in the context. * * @param props The constructor props for the `SidebarState` class. * @returns The `SidebarState` instance. */export function setSidebar(props: SidebarStateProps): SidebarState { return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));}
/** * Retrieves the `SidebarState` instance from the context. This is a class instance, * so you cannot destructure it. * @returns The `SidebarState` instance. */export function useSidebar(): SidebarState { return getContext(Symbol.for(SYMBOL_KEY));}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-content" data-sidebar="content" class={cn( "no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-footer" data-sidebar="footer" class={cn("gap-2 p-2 flex flex-col", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLButtonAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, child, ...restProps }: WithElementRef<HTMLButtonAttributes> & { child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const mergedProps = $derived({ class: cn( "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", className ), "data-slot": "sidebar-group-action", "data-sidebar": "group-action", ...restProps, });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <button bind:this={ref} {...mergedProps}> {@render children?.()} </button>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-group-content" data-sidebar="group-content" class={cn("text-sm w-full", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), children, child, class: className, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> & { child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const mergedProps = $derived({ class: cn( "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", className ), "data-slot": "sidebar-group-label", "data-sidebar": "group-label", ...restProps, });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <div bind:this={ref} {...mergedProps}> {@render children?.()} </div>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-group" data-sidebar="group" class={cn("p-2 relative flex w-full min-w-0 flex-col", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-header" data-sidebar="header" class={cn("gap-2 p-2 flex flex-col", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Input } from "$lib/registry/ui/input/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), value = $bindable(""), class: className, ...restProps }: ComponentProps<typeof Input> = $props();</script>
<Input bind:ref bind:value data-slot="sidebar-input" data-sidebar="input" class={cn("h-8 w-full", className)} {...restProps}/><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<main bind:this={ref} data-slot="sidebar-inset" class={cn("relative flex w-full flex-1 flex-col bg-background 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]:border md:peer-data-[variant=inset]:border-border md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className)} {...restProps}> {@render children?.()}</main><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLButtonAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, showOnHover = false, children, child, ...restProps }: WithElementRef<HTMLButtonAttributes> & { child?: Snippet<[{ props: Record<string, unknown> }]>; showOnHover?: boolean; } = $props();
const mergedProps = $derived({ class: cn( "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", showOnHover && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0", className ), "data-slot": "sidebar-menu-action", "data-sidebar": "menu-action", ...restProps, });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <button bind:this={ref} {...mergedProps}> {@render children?.()} </button>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();</script>
<div bind:this={ref} data-slot="sidebar-menu-badge" data-sidebar="menu-badge" class={cn( "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--brand)_30%,var(--popover))] bg-brand-soft px-1.5 font-mono text-[11px] font-medium text-brand-text tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1", className )} {...restProps}> {@render children?.()}</div><script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const sidebarMenuButtonVariants = tv({ base: "peer/menu-button group/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-[7px] p-2 text-left text-[13.5px] ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: "border border-sidebar-border bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", }, size: { default: "h-8 text-[13.5px]", sm: "h-7 text-xs", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", }, }, defaultVariants: { variant: "default", size: "default", }, });
export type SidebarMenuButtonVariant = VariantProps<typeof sidebarMenuButtonVariants>["variant"]; export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>["size"];</script>
<script lang="ts"> import { mergeProps } from "bits-ui"; import * as Tooltip from "$lib/registry/ui/tooltip/index.js"; import { cn, type WithElementRef, type WithoutChildrenOrChild } from "$lib/utils.js"; import { useSidebar } from "./context.svelte.js"; import type { ComponentProps, Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, child, variant = "default", size = "default", isActive = false, tooltipContent, tooltipContentProps, ...restProps }: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & { isActive?: boolean; variant?: SidebarMenuButtonVariant; size?: SidebarMenuButtonSize; tooltipContent?: Snippet | string; tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>; child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const sidebar = useSidebar();
const buttonProps = $derived({ class: cn(sidebarMenuButtonVariants({ variant, size }), className), "data-slot": "sidebar-menu-button", "data-sidebar": "menu-button", "data-size": size, "data-active": isActive, ...restProps, });</script>
{#snippet Button({ props }: { props?: Record<string, unknown> })} {@const mergedProps = mergeProps(buttonProps, props)} {#if child} {@render child({ props: mergedProps })} {:else} <button bind:this={ref} {...mergedProps}> {@render children?.()} </button> {/if}{/snippet}
{#if !tooltipContent} {@render Button({})}{:else} <Tooltip.Root> <Tooltip.Trigger> {#snippet child({ props })} {@render Button({ props })} {/snippet} </Tooltip.Trigger> <Tooltip.Content side="right" align="center" hidden={sidebar.state !== "collapsed" || sidebar.isMobile} {...tooltipContentProps} > {#if typeof tooltipContent === "string"} {tooltipContent} {:else if tooltipContent} {@render tooltipContent()} {/if} </Tooltip.Content> </Tooltip.Root>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();</script>
<li bind:this={ref} data-slot="sidebar-menu-item" data-sidebar="menu-item" class={cn("group/menu-item relative", className)} {...restProps}> {@render children?.()}</li><script lang="ts"> import { Skeleton } from "$lib/registry/ui/skeleton/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, showIcon = false, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLElement>> & { showIcon?: boolean; } = $props();
// Random width between 50% and 90% const width = `${Math.floor(Math.random() * 40) + 50}%`;</script>
<div bind:this={ref} data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" class={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} {...restProps}> {#if showIcon} <Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> {/if} <Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style="--skeleton-width: {width};" /> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAnchorAttributes } from "svelte/elements";
let { ref = $bindable(null), children, child, class: className, size = "md", isActive = false, ...restProps }: WithElementRef<HTMLAnchorAttributes> & { child?: Snippet<[{ props: Record<string, unknown> }]>; size?: "sm" | "md"; isActive?: boolean; } = $props();
const mergedProps = $derived({ class: cn( "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-[7px] px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", className ), "data-slot": "sidebar-menu-sub-button", "data-sidebar": "menu-sub-button", "data-size": size, "data-active": isActive, ...restProps, });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <a bind:this={ref} {...mergedProps}> {@render children?.()} </a>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), children, class: className, ...restProps }: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();</script>
<li bind:this={ref} data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" class={cn("group/menu-sub-item relative", className)} {...restProps}> {@render children?.()}</li><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();</script>
<ul bind:this={ref} data-slot="sidebar-menu-sub" data-sidebar="menu-sub" class={cn( "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden", className )} {...restProps}> {@render children?.()}</ul><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();</script>
<ul bind:this={ref} data-slot="sidebar-menu" data-sidebar="menu" class={cn("gap-0 flex w-full min-w-0 flex-col", className)} {...restProps}> {@render children?.()}</ul><script lang="ts"> import * as Tooltip from "$lib/registry/ui/tooltip/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import { SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON, } from "./constants.js"; import { setSidebar } from "./context.svelte.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), open = $bindable(true), onOpenChange = () => {}, class: className, style, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { open?: boolean; onOpenChange?: (open: boolean) => void; } = $props();
const sidebar = setSidebar({ open: () => open, setOpen: (value: boolean) => { open = value; onOpenChange(value);
// This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, });</script>
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
<Tooltip.Provider delayDuration={0}> <div data-slot="sidebar-wrapper" style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}" class={cn( "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", className )} bind:this={ref} {...restProps} > {@render children?.()} </div></Tooltip.Provider><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { useSidebar } from "./context.svelte.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
const sidebar = useSidebar();</script>
<button bind:this={ref} data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" tabindex={-1} onclick={sidebar.toggle} title="Toggle Sidebar" class={cn( "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className )} {...restProps}> {@render children?.()}</button><script lang="ts"> import { Separator } from "$lib/registry/ui/separator/index.js"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, ...restProps }: ComponentProps<typeof Separator> = $props();</script>
<Separator bind:ref data-slot="sidebar-separator" data-sidebar="separator" class={cn("mx-2 w-auto bg-sidebar-border", className)} {...restProps}/><script lang="ts"> import SidebarIcon from "phosphor-svelte/lib/Sidebar"; import { Button } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import { useSidebar } from "./context.svelte.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, onclick, ...restProps }: ComponentProps<typeof Button> & { onclick?: (e: MouseEvent) => void; } = $props();
const sidebar = useSidebar();</script>
<Button bind:ref data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" size="icon-sm" class={cn(className)} type="button" onclick={(e) => { onclick?.(e); sidebar.toggle(); }} {...restProps}> <SidebarIcon class="rtl:rotate-180" /> <span class="sr-only">Toggle Sidebar</span></Button><script lang="ts"> import * as Sheet from "$lib/registry/ui/sheet/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import { SIDEBAR_WIDTH_MOBILE } from "./constants.js"; import { useSidebar } from "./context.svelte.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), side = "left", variant = "sidebar", collapsible = "offcanvas", class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; } = $props();
const sidebar = useSidebar();</script>
{#if collapsible === "none"} <div class={cn( "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", className )} bind:this={ref} {...restProps} > {@render children?.()} </div>{:else if sidebar.isMobile} <Sheet.Root bind:open={() => sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> <Sheet.Content bind:ref data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" class={cn( "w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden", className )} style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};" {side} > <Sheet.Header class="sr-only"> <Sheet.Title>Sidebar</Sheet.Title> <Sheet.Description>Displays the mobile sidebar.</Sheet.Description> </Sheet.Header> <div class="flex h-full w-full flex-col"> {@render children?.()} </div> </Sheet.Content> </Sheet.Root>{:else} <div bind:this={ref} class="group peer hidden text-sidebar-foreground md:block" data-state={sidebar.state} data-collapsible={sidebar.state === "collapsed" ? collapsible : ""} data-variant={variant} data-side={side} data-slot="sidebar" > <!-- This is what handles the sidebar gap on desktop --> <div data-slot="sidebar-gap" class={cn( "transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_(--spacing(4)))]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" )} ></div> <div data-slot="sidebar-container" data-side={side} class={cn( "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:start-0 data-[side=left]:group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)_*_-1)] data-[side=right]:end-0 data-[side=right]:group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)_*_-1)] md:flex", // Adjust the padding for floating and inset variants. variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_(--spacing(4))_+_2px)]" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-e group-data-[side=right]:border-s", className )} {...restProps} > <div data-sidebar="sidebar" data-slot="sidebar-inner" class="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-xl group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border" > {@render children?.()} </div> </div> </div>{/if}import { useSidebar } from "./context.svelte.js";import Root from "./sidebar.svelte";import Content from "./sidebar-content.svelte";import Footer from "./sidebar-footer.svelte";import Group from "./sidebar-group.svelte";import GroupAction from "./sidebar-group-action.svelte";import GroupContent from "./sidebar-group-content.svelte";import GroupLabel from "./sidebar-group-label.svelte";import Header from "./sidebar-header.svelte";import Input from "./sidebar-input.svelte";import Inset from "./sidebar-inset.svelte";import Menu from "./sidebar-menu.svelte";import MenuAction from "./sidebar-menu-action.svelte";import MenuBadge from "./sidebar-menu-badge.svelte";import MenuButton from "./sidebar-menu-button.svelte";import MenuItem from "./sidebar-menu-item.svelte";import MenuSkeleton from "./sidebar-menu-skeleton.svelte";import MenuSub from "./sidebar-menu-sub.svelte";import MenuSubButton from "./sidebar-menu-sub-button.svelte";import MenuSubItem from "./sidebar-menu-sub-item.svelte";import Provider from "./sidebar-provider.svelte";import Rail from "./sidebar-rail.svelte";import Separator from "./sidebar-separator.svelte";import Trigger from "./sidebar-trigger.svelte";
export { Content, Content as SidebarContent, Footer, Footer as SidebarFooter, Group, Group as SidebarGroup, GroupAction, GroupAction as SidebarGroupAction, GroupContent, GroupContent as SidebarGroupContent, GroupLabel, GroupLabel as SidebarGroupLabel, Header, Header as SidebarHeader, Input, Input as SidebarInput, Inset, Inset as SidebarInset, Menu, Menu as SidebarMenu, MenuAction, MenuAction as SidebarMenuAction, MenuBadge, MenuBadge as SidebarMenuBadge, MenuButton, MenuButton as SidebarMenuButton, MenuItem, MenuItem as SidebarMenuItem, MenuSkeleton, MenuSkeleton as SidebarMenuSkeleton, MenuSub, MenuSub as SidebarMenuSub, MenuSubButton, MenuSubButton as SidebarMenuSubButton, MenuSubItem, MenuSubItem as SidebarMenuSubItem, Provider, Provider as SidebarProvider, Rail, Rail as SidebarRail, Root, // Root as Sidebar, Separator, Separator as SidebarSeparator, Trigger as SidebarTrigger, Trigger, useSidebar,};import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider,} from "@/components/ui/sidebar";
export default function Demo() { return ( <div className="relative h-[320px] w-full overflow-hidden rounded-xl border border-border bg-muted [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <SidebarProvider className="h-full min-h-0"> <Sidebar variant="floating" collapsible="none"> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Settings</SidebarGroupLabel> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton isActive>Profile</SidebarMenuButton> </SidebarMenuItem> <SidebarMenuItem> <SidebarMenuButton>Wallets</SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </SidebarContent> </Sidebar> <SidebarInset className="p-4 text-sm text-muted-foreground"> Floating variant, not collapsible. </SidebarInset> </SidebarProvider> </div> );}<script setup lang="ts">import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider,} from "@/components/ui/sidebar";</script>
<template> <div class="relative h-[320px] w-full overflow-hidden rounded-xl border border-border bg-muted [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full" > <SidebarProvider class="h-full min-h-0"> <Sidebar variant="floating" collapsible="none"> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Settings</SidebarGroupLabel> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton is-active>Profile</SidebarMenuButton> </SidebarMenuItem> <SidebarMenuItem> <SidebarMenuButton>Wallets</SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </SidebarContent> </Sidebar> <SidebarInset class="p-4 text-sm text-muted-foreground">Floating variant, not collapsible.</SidebarInset> </SidebarProvider> </div></template><script lang="ts"> import * as Sidebar from "$lib/components/ui/sidebar";</script>
<div class="relative h-[320px] w-full overflow-hidden rounded-xl border border-border bg-muted [transform:translateZ(0)] [&_[data-slot=sidebar-container]]:absolute [&_[data-slot=sidebar-container]]:h-full"> <Sidebar.Provider class="h-full min-h-0"> <Sidebar.Root variant="floating" collapsible="none"> <Sidebar.Content> <Sidebar.Group> <Sidebar.GroupLabel>Settings</Sidebar.GroupLabel> <Sidebar.Menu> <Sidebar.MenuItem> <Sidebar.MenuButton isActive>Profile</Sidebar.MenuButton> </Sidebar.MenuItem> <Sidebar.MenuItem> <Sidebar.MenuButton>Wallets</Sidebar.MenuButton> </Sidebar.MenuItem> </Sidebar.Menu> </Sidebar.Group> </Sidebar.Content> </Sidebar.Root> <Sidebar.Inset class="p-4 text-sm text-muted-foreground"> Floating variant, not collapsible. </Sidebar.Inset> </Sidebar.Provider></div>| Prop | Type | Default |
|---|---|---|
SidebarProvider defaultOpen / open / onOpenChange |
boolean / boolean / (open) => void |
true |
Sidebar side |
left | right |
left |
Sidebar variant |
sidebar | floating | inset |
sidebar |
Sidebar collapsible |
offcanvas | icon | none |
offcanvas |
SidebarMenuButton isActive |
boolean |
false |
SidebarMenuButton size |
default | sm | lg |
default |
SidebarMenuButton tooltip |
string | TooltipContent props |
none |
SidebarMenuButton render |
ReactElement |
<button> (Base UI, replaces asChild) |
SidebarMenuAction showOnHover |
boolean |
false |
useSidebar() returns { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar }. Cmd/Ctrl+B toggles. On mobile the sidebar opens as a Sheet (use-mobile hook).
Parts: SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupAction, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarMenuBadge, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarInput, SidebarSeparator, SidebarInset, SidebarRail, SidebarTrigger.
✦ Edmi additions
Section titled “✦ Edmi additions”- Floating and inset variants use a 1px border only (flat).
- Menu badges use the soft brand fill with a tinted border, mono numerals.
- Menu buttons are 32px tall with 7px radius; the active row uses
--sidebar-accent.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue: the
use-mobilehook is not shipped (skipped).SidebarProviderusesuseMediaQueryfrom@vueuse/coreinstead.