{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sidebar", "type": "registry:ui", "title": "Sidebar", "files": [ { "path": "ui/sidebar.tsrx", "type": "registry:ui", "target": "components/ui/sidebar.tsrx", "content": "// Base UI base sidebar — class strings from the maintainer-supplied radix source, verbatim. Composes\n// this base's own button, input, separator, sheet, skeleton and tooltip, so it inherits their\n// dialects (the sheet's transition motion, the separator's aria-orientation) for free.\n//\n// THE TOOLTIP TRIGGER USES `render`, WHICH IS THE WHOLE REASON THIS FAMILY IS PORTABLE. Radix wraps\n// the menu button with `{button}`; Base UI has no Slot, but\n// `Tooltip.Trigger` accepts a `render` prop taking an element, which is the same composition —\n// `render={button}`. That is a real primitive with a documented prop, not a guess.\n//\n// `asChild` IS ABSENT FROM THIS BASE'S OWN PARTS, matching badge, breadcrumb and item. Five of them\n// take it in the radix source — GroupLabel, GroupAction, MenuButton, MenuAction, MenuSubButton — and\n// each renders a plain host element, so there is no primitive here whose `render` prop to borrow and\n// nothing settles which spelling upstream's Base UI sidebar uses. It is typed `never` so markup\n// carried over from the radix base fails to compile rather than silently rendering the wrapper AND\n// its child.\n//\n// THAT GAP IS LOAD-BEARING HERE, more than it was for badge: `` wrapping\n// a router link is the ordinary way to build a nav. Until the upstream source settles the spelling,\n// build the link inside the button, or apply the exported `sidebarMenuButtonVariants({ variant, size })`\n// to your own element.\n//\n// Octane adaptations: \"use client\" dropped, lucide-react -> @octanejs/lucide; the provider/Sheet\n// trees compose with createElement where a child must be a real element descriptor, and CSS custom\n// properties in `style` are cast (they sit outside CSSProperties).\nimport {\n\tcreateContext,\n\tcreateElement,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseMemo,\n\tuseState,\n\ttype OctaneNode,\n} from 'octane';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { PanelLeftIcon } from '@octanejs/lucide';\n\nimport { cn } from '@/lib/utils';\nimport { useIsMobile } from '@/hooks/use-mobile';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Separator } from '@/components/ui/separator';\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\n\nconst SIDEBAR_COOKIE_NAME = 'sidebar_state';\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_WIDTH = '16rem';\nconst SIDEBAR_WIDTH_MOBILE = '18rem';\nconst SIDEBAR_WIDTH_ICON = '3rem';\nconst SIDEBAR_KEYBOARD_SHORTCUT = 'b';\n\ntype Props = { className?: string; children?: OctaneNode } & Record;\n\nexport interface SidebarContextProps {\n\tstate: 'expanded' | 'collapsed';\n\topen: boolean;\n\tsetOpen: (open: boolean) => void;\n\topenMobile: boolean;\n\tsetOpenMobile: (open: boolean) => void;\n\tisMobile: boolean;\n\ttoggleSidebar: () => void;\n}\n\nconst SidebarContext = createContext(null);\n\nexport function useSidebar(): SidebarContextProps {\n\tconst context = useContext(SidebarContext);\n\tif (!context) {\n\t\tthrow new Error('useSidebar must be used within a SidebarProvider.');\n\t}\n\n\treturn context;\n}\n\nexport function SidebarProvider(props: Props & {\n\tdefaultOpen?: boolean;\n\topen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tstyle?: Record;\n}) {\n\tconst {\n\t\tdefaultOpen = true,\n\t\topen: openProp,\n\t\tonOpenChange: setOpenProp,\n\t\tclassName,\n\t\tstyle,\n\t\tchildren: _children,\n\t\t...rest\n\t} = props;\n\tconst isMobile = useIsMobile();\n\tconst [openMobile, setOpenMobile] = useState(false);\n\n\t// This is the internal state of the sidebar.\n\t// We use openProp and setOpenProp for control from outside the component.\n\tconst [_open, _setOpen] = useState(defaultOpen);\n\tconst open = openProp ?? _open;\n\tconst setOpen = useCallback((value: boolean | ((value: boolean) => boolean)) => {\n\t\tconst openState =\n\t\t\ttypeof value === 'function' ? value(open) : value;\n\t\tif (setOpenProp) {\n\t\t\tsetOpenProp(openState);\n\t\t} else {\n\t\t\t_setOpen(openState);\n\t\t}\n\n\t\t// This sets the cookie to keep the sidebar state.\n\t\tdocument.cookie =\n\t\t\t`${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n\t});\n\n\t// Helper to toggle the sidebar.\n\tconst toggleSidebar = useCallback(() => {\n\t\treturn isMobile ? setOpenMobile((value) => !value) : setOpen((value) => !value);\n\t});\n\n\t// Adds a keyboard shortcut to toggle the sidebar.\n\tuseEffect(() => {\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\ttoggleSidebar();\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener('keydown', handleKeyDown);\n\t\treturn () => window.removeEventListener('keydown', handleKeyDown);\n\t});\n\n\t// We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n\t// This makes it easier to style the sidebar with Tailwind classes.\n\tconst state = open ? 'expanded' : 'collapsed';\n\n\tconst contextValue = useMemo(\n\t\t() => ({\n\t\t\tstate,\n\t\t\topen,\n\t\t\tsetOpen,\n\t\t\tisMobile,\n\t\t\topenMobile,\n\t\t\tsetOpenMobile,\n\t\t\ttoggleSidebar,\n\t\t}),\n\t);\n\n\treturn createElement(\n\t\tSidebarContext.Provider,\n\t\t{ value: contextValue },\n\t\tcreateElement(\n\t\t\t'div',\n\t\t\t{\n\t\t\t\t'data-slot': 'sidebar-wrapper',\n\t\t\t\tstyle: {\n\t\t\t\t\t'--sidebar-width': SIDEBAR_WIDTH,\n\t\t\t\t\t'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,\n\t\t\t\t\t...style,\n\t\t\t\t} as Record,\n\t\t\t\tclassName: cn(\n\t\t\t\t\t'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar',\n\t\t\t\t\tclassName,\n\t\t\t\t),\n\t\t\t\t...rest,\n\t\t\t},\n\t\t\tprops.children,\n\t\t),\n\t);\n}\n\nexport function Sidebar(props: Props & {\n\tside?: 'left' | 'right';\n\tvariant?: 'sidebar' | 'floating' | 'inset';\n\tcollapsible?: 'offcanvas' | 'icon' | 'none';\n\tdir?: string;\n}) {\n\tconst {\n\t\tside = 'left',\n\t\tvariant = 'sidebar',\n\t\tcollapsible = 'offcanvas',\n\t\tclassName,\n\t\tchildren: _children,\n\t\tdir,\n\t\t...rest\n\t} = props;\n\tconst { isMobile, state, openMobile, setOpenMobile } = useSidebar();\n\n\tif (collapsible === 'none') {\n\t\treturn createElement(\n\t\t\t'div',\n\t\t\t{\n\t\t\t\t'data-slot': 'sidebar',\n\t\t\t\tclassName: cn(\n\t\t\t\t\t'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground',\n\t\t\t\t\tclassName,\n\t\t\t\t),\n\t\t\t\t...rest,\n\t\t\t},\n\t\t\tprops.children,\n\t\t);\n\t}\n\n\tif (isMobile) {\n\t\treturn createElement(\n\t\t\tSheet,\n\t\t\t{ open: openMobile, onOpenChange: setOpenMobile, ...rest },\n\t\t\tcreateElement(\n\t\t\t\tSheetContent,\n\t\t\t\t{\n\t\t\t\t\tdir,\n\t\t\t\t\t'data-sidebar': 'sidebar',\n\t\t\t\t\t'data-slot': 'sidebar',\n\t\t\t\t\t'data-mobile': 'true',\n\t\t\t\t\tclassName: 'w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden',\n\t\t\t\t\tstyle: { '--sidebar-width': SIDEBAR_WIDTH_MOBILE } as Record,\n\t\t\t\t\tside,\n\t\t\t\t},\n\t\t\t\tcreateElement(\n\t\t\t\t\tSheetHeader,\n\t\t\t\t\t{ key: 'header', className: 'sr-only' },\n\t\t\t\t\tcreateElement(SheetTitle, { key: 'title' }, 'Sidebar'),\n\t\t\t\t\tcreateElement(SheetDescription, { key: 'desc' }, 'Displays the mobile sidebar.'),\n\t\t\t\t),\n\t\t\t\tcreateElement(\n\t\t\t\t\t'div',\n\t\t\t\t\t{ key: 'body', className: 'flex h-full w-full flex-col' },\n\t\t\t\t\tprops.children,\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t}\n\n\treturn createElement(\n\t\t'div',\n\t\t{\n\t\t\tclassName: 'group peer hidden text-sidebar-foreground md:block',\n\t\t\t'data-state': state,\n\t\t\t'data-collapsible': state === 'collapsed' ? collapsible : '',\n\t\t\t'data-variant': variant,\n\t\t\t'data-side': side,\n\t\t\t'data-slot': 'sidebar',\n\t\t},\n\t\t// This is what handles the sidebar gap on desktop\n\t\tcreateElement('div', {\n\t\t\tkey: 'gap',\n\t\t\t'data-slot': 'sidebar-gap',\n\t\t\tclassName: cn(\n\t\t\t\t'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',\n\t\t\t\t'group-data-[collapsible=offcanvas]:w-0',\n\t\t\t\t'group-data-[side=right]:rotate-180',\n\t\t\t\tvariant === 'floating' || variant === 'inset'\n\t\t\t\t\t? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'\n\t\t\t\t\t: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',\n\t\t\t),\n\t\t}),\n\t\tcreateElement(\n\t\t\t'div',\n\t\t\t{\n\t\t\t\tkey: 'container',\n\t\t\t\t'data-slot': 'sidebar-container',\n\t\t\t\t'data-side': side,\n\t\t\t\tclassName: cn(\n\t\t\t\t\t'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',\n\t\t\t\t\t// Adjust the padding for floating and inset variants.\n\t\t\t\t\tvariant === 'floating' || variant === 'inset'\n\t\t\t\t\t\t? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'\n\t\t\t\t\t\t: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',\n\t\t\t\t\tclassName,\n\t\t\t\t),\n\t\t\t\t...rest,\n\t\t\t},\n\t\t\tcreateElement(\n\t\t\t\t'div',\n\t\t\t\t{\n\t\t\t\t\t'data-sidebar': 'sidebar',\n\t\t\t\t\t'data-slot': 'sidebar-inner',\n\t\t\t\t\tclassName: 'flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border',\n\t\t\t\t},\n\t\t\t\tprops.children,\n\t\t\t),\n\t\t),\n\t);\n}\n\nexport function SidebarTrigger({ className, onClick, ...props }: Props & {\n\tonClick?: (event: MouseEvent) => void;\n}) @{\n\tconst { toggleSidebar } = useSidebar();\n\n\t {\n\t\t\tonClick?.(event);\n\t\t\ttoggleSidebar();\n\t\t}}\n\t\t{...props}\n\t>\n\t\t\n\t\tToggle Sidebar\n\t\n}\n\nexport function SidebarRail({ className, ...props }: Props) @{\n\tconst { toggleSidebar } = useSidebar();\n\n\t