"use client"
import type { ComponentProps } from "react"
import {
createContext,
use,
useCallback,
useEffect,
useId,
useLayoutEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react"
import { useRender } from "@base-ui/react/use-render"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { PanelLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import { VStack } from "@/components/ui/stack"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { breakpoints } from "@/theme/breakpoints.stylex"
import { sidebarMenuItem } from "@/theme/markers.stylex"
import { states } from "@/theme/states"
import {
color,
duration,
ease,
layer,
leading,
radius,
shadow,
space,
text,
} from "@/theme/tokens.stylex"
const sidebarForeground = "light-dark(oklch(0.145 0 0), oklch(0.985 0 0))"
const sidebarAccent = "light-dark(oklch(0.95 0 0), oklch(0.269 0 0))"
const sidebarAccentForeground = "light-dark(oklch(0.205 0 0), oklch(0.985 0 0))"
const sidebarBorder = "light-dark(oklch(0.922 0 0), oklch(1 0 0 / 10%))"
const SIDEBAR_COOKIE_NAME = "carbon-sidebar"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SIDEBAR_PEEK_CLOSE_DELAY = 250
const MOBILE_QUERY = "(width < 48rem)"
type SidebarSide = "left" | "right"
type SidebarVariant = "sidebar" | "floating" | "inset"
type SidebarCollapsible = "offcanvas" | "icon" | "none"
type SidebarState = "expanded" | "collapsed"
type SidebarContextValue = {
state: SidebarState
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
variant: SidebarVariant
peeking: boolean
setPeeking: (peeking: boolean) => void
instant: boolean
}
const SidebarContext = createContext<SidebarContextValue | null>(null)
const SidebarPanelContext = createContext<{
side: SidebarSide
collapsible: SidebarCollapsible
iconMode: boolean
}>({ side: "left", collapsible: "offcanvas", iconMode: false })
const SidebarMenuItemContext = createContext<{
size: SidebarMenuButtonSize
setSize: (size: SidebarMenuButtonSize) => void
hasAction: boolean
setHasAction: (hasAction: boolean) => void
} | null>(null)
const styles = stylex.create({
wrapper: {
display: "flex",
minHeight: "100svh",
width: "100%",
"--carbon-sidebar-width": "16rem",
"--carbon-sidebar-width-icon": "3rem",
},
wrapperInset: { backgroundColor: color.tray },
root: {
display: { default: "none", [breakpoints.md]: "block" },
color: sidebarForeground,
},
gap: {
position: "relative",
width: "var(--carbon-sidebar-width)",
backgroundColor: color.transparent,
transitionProperty: "width",
transitionDuration: {
default: duration.panel,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.spring,
},
gapOffcanvas: { width: 0 },
instant: { transitionDuration: "0s" },
gapIcon: { width: "var(--carbon-sidebar-width-icon)" },
gapIconPadded: { width: `calc(var(--carbon-sidebar-width-icon) + ${space.s4})` },
container: {
position: "fixed",
insetBlock: 0,
zIndex: layer.overlay,
display: "flex",
width: "var(--carbon-sidebar-width)",
transitionProperty: "left, right, width",
transitionDuration: {
default: duration.panel,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.spring,
},
containerLeft: { left: 0 },
containerRight: { right: 0 },
containerLeftOffcanvas: { left: "calc(-1 * var(--carbon-sidebar-width))" },
containerRightOffcanvas: { right: "calc(-1 * var(--carbon-sidebar-width))" },
containerPeekLeft: { boxShadow: shadow.peekLeft },
containerPeekRight: { boxShadow: shadow.peekRight },
containerIcon: { width: "var(--carbon-sidebar-width-icon)" },
containerIconPadded: { width: `calc(var(--carbon-sidebar-width-icon) + ${space.s4} + 2px)` },
containerPadded: { padding: space.s2 },
containerBorderRight: {
borderRightWidth: 1,
borderRightStyle: "solid",
borderRightColor: sidebarBorder,
},
containerBorderLeft: {
borderLeftWidth: 1,
borderLeftStyle: "solid",
borderLeftColor: sidebarBorder,
},
inner: {
display: "flex",
width: "100%",
height: "100%",
flexDirection: "column",
backgroundColor: color.sidebar,
},
innerFloating: {
borderRadius: radius.lg,
boxShadow: shadow.raised,
},
innerInset: { backgroundColor: color.transparent },
none: {
display: "flex",
height: "100%",
width: "var(--carbon-sidebar-width)",
flexDirection: "column",
backgroundColor: color.sidebar,
color: sidebarForeground,
},
sheet: {
width: "18rem",
padding: 0,
gap: 0,
backgroundColor: color.sidebar,
color: sidebarForeground,
"--carbon-sidebar-width": "18rem",
},
sheetBody: { display: "flex", height: "100%", width: "100%", flexDirection: "column" },
rail: {
position: "absolute",
insetBlock: 0,
zIndex: layer.sticky,
display: { default: "none", [breakpoints.sm]: "flex" },
width: space.s4,
transform: "translateX(-50%)",
transitionProperty: "background-color, left, right, transform",
transitionDuration: {
default: `${duration.color}, ${duration.panel}, ${duration.panel}, ${duration.panel}`,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: `${ease.standard}, ${ease.spring}, ${ease.spring}, ${ease.spring}`,
"::after": {
content: "''",
position: "absolute",
insetBlock: 0,
left: "50%",
width: 2,
backgroundColor: { default: null, ":hover": sidebarBorder },
},
},
railLeft: { right: `calc(-1 * ${space.s4})`, cursor: "w-resize" },
railRight: { left: 0, cursor: "e-resize" },
railLeftCollapsed: { cursor: "e-resize" },
railRightCollapsed: { cursor: "w-resize" },
railOffcanvas: {
transform: "translateX(0)",
backgroundColor: { default: null, ":hover": color.sidebar },
"::after": { left: "100%" },
},
railLeftOffcanvas: { right: `calc(-1 * ${space.s2})` },
railRightOffcanvas: { left: `calc(-1 * ${space.s2})` },
inset: {
position: "relative",
display: "flex",
width: "100%",
flexGrow: 1,
flexDirection: "column",
backgroundColor: color.background,
},
insetVariant: {
margin: { default: null, [breakpoints.md]: space.s2 },
marginInlineStart: { default: null, [breakpoints.md]: 0 },
borderRadius: { default: null, [breakpoints.md]: radius.xl },
backgroundColor: { default: color.background, [breakpoints.md]: color.card },
boxShadow: { default: null, [breakpoints.md]: shadow.raised },
},
insetVariantCollapsed: { marginInlineStart: { default: null, [breakpoints.md]: space.s2 } },
input: { backgroundColor: { default: color.background, ":disabled": color.inputDisabledBg } },
separator: { marginInline: space.s2, width: "auto", backgroundColor: sidebarBorder },
contentIcon: { overflow: "hidden" },
group: { position: "relative", width: "100%" },
groupLabel: {
display: "flex",
height: space.s8,
flexShrink: 0,
alignItems: "center",
borderRadius: radius.md,
paddingInline: space.s2,
fontSize: text.xs,
lineHeight: leading.xs,
fontWeight: 500,
color: `color-mix(in oklab, ${sidebarForeground} 70%, transparent)`,
transitionProperty: "margin, opacity",
transitionDuration: {
default: duration.panel,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.spring,
"--carbon-icon-size": space.s4,
},
groupLabelIcon: { marginBlockStart: `calc(-1 * ${space.s8})`, opacity: 0 },
groupAction: {
position: "absolute",
top: space.s3_5,
right: space.s3,
display: "flex",
aspectRatio: "1",
width: space.s5,
alignItems: "center",
justifyContent: "center",
borderRadius: radius.md,
padding: 0,
color: { default: sidebarForeground, ":hover": sidebarAccentForeground },
backgroundColor: { default: null, ":hover": color.itemHighlightBg },
transitionProperty: "color, background-color, box-shadow",
transitionDuration: duration.color,
transitionTimingFunction: ease.standard,
"--carbon-icon-size": space.s4,
"::after": {
content: "''",
position: "absolute",
inset: `calc(-1 * ${space.s2})`,
display: { default: "block", [breakpoints.md]: "none" },
},
},
groupContent: { width: "100%", fontSize: text.sm, lineHeight: leading.sm },
menu: { width: "100%" },
menuItem: { position: "relative" },
menuButton: {
display: "flex",
width: "100%",
alignItems: "center",
gap: space.s2,
overflow: "hidden",
borderRadius: radius.md,
padding: space.s2,
textAlign: "left",
fontSize: text.sm,
lineHeight: leading.sm,
backgroundColor: {
default: null,
":hover": color.itemHighlightBg,
":active": color.itemHighlightBg,
},
color: {
default: null,
":hover": sidebarAccentForeground,
":active": sidebarAccentForeground,
},
transitionProperty: "width, height, padding, color, background-color, box-shadow",
transitionDuration: {
default: duration.color,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.standard,
pointerEvents: { default: null, ":disabled": "none" },
opacity: { default: 1, ":disabled": 0.5 },
"--carbon-icon-size": space.s4,
"--carbon-menu-open-bg": color.itemHighlightBg,
"--carbon-menu-open-fg": sidebarAccentForeground,
},
rowActive: {
backgroundColor: sidebarAccent,
color: sidebarAccentForeground,
},
rowActiveLabel: {
WebkitTextStrokeWidth: "0.3px",
WebkitTextStrokeColor: "currentColor",
},
menuButtonWithAction: { paddingInlineEnd: space.s8 },
menuButtonIcon: { width: space.s8, height: space.s8, padding: space.s2 },
menuButtonIconLg: { padding: 0 },
menuAction: {
position: "absolute",
right: space.s1,
display: "flex",
aspectRatio: "1",
width: space.s5,
alignItems: "center",
justifyContent: "center",
borderRadius: radius.md,
padding: 0,
color: {
default: sidebarForeground,
":hover": sidebarAccentForeground,
[stylex.when.ancestor(":hover", sidebarMenuItem)]: sidebarAccentForeground,
},
backgroundColor: { default: null, ":hover": color.itemHighlightBg },
transitionProperty: "color, background-color, box-shadow, opacity",
transitionDuration: duration.color,
transitionTimingFunction: ease.standard,
"--carbon-icon-size": space.s4,
"::after": {
content: "''",
position: "absolute",
inset: `calc(-1 * ${space.s2})`,
display: { default: "block", [breakpoints.md]: "none" },
},
},
menuActionShowOnHover: {
opacity: {
default: 1,
[breakpoints.md]: {
default: 0,
[stylex.when.ancestor(":hover", sidebarMenuItem)]: 1,
[stylex.when.ancestor(":focus-within", sidebarMenuItem)]: 1,
},
},
},
menuBadge: {
pointerEvents: "none",
position: "absolute",
right: space.s1,
display: "flex",
height: space.s5,
minWidth: space.s5,
alignItems: "center",
justifyContent: "center",
userSelect: "none",
borderRadius: radius.md,
paddingInline: space.s1,
fontSize: text.xs,
lineHeight: leading.xs,
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
color: {
default: sidebarForeground,
[stylex.when.ancestor(":hover", sidebarMenuItem)]: sidebarAccentForeground,
},
},
menuSkeleton: {
display: "flex",
height: space.s8,
alignItems: "center",
gap: space.s2,
borderRadius: radius.md,
paddingInline: space.s2,
},
menuSkeletonIcon: { width: space.s4, height: space.s4, borderRadius: radius.md },
menuSkeletonText: { height: space.s4, flexGrow: 1 },
menuSub: {
marginInline: space.s3_5,
display: "flex",
minWidth: 0,
transform: "translateX(1px)",
flexDirection: "column",
gap: space.s1,
borderLeftWidth: 1,
borderLeftStyle: "solid",
borderLeftColor: sidebarBorder,
paddingInline: space.s2_5,
paddingBlock: space.s0_5,
},
menuSubItem: { position: "relative" },
menuSubButton: {
display: "flex",
height: space.s7,
minWidth: 0,
transform: "translateX(-1px)",
alignItems: "center",
gap: space.s2,
overflow: "hidden",
borderRadius: radius.md,
paddingInline: space.s2,
color: {
default: sidebarForeground,
":hover": sidebarAccentForeground,
":active": sidebarAccentForeground,
},
backgroundColor: {
default: null,
":hover": color.itemHighlightBg,
":active": color.itemHighlightBg,
},
transitionProperty: "color, background-color, box-shadow",
transitionDuration: duration.color,
transitionTimingFunction: ease.standard,
pointerEvents: { default: null, ":disabled": "none" },
opacity: { default: 1, ":disabled": 0.5 },
"--carbon-icon-size": space.s4,
},
truncate: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
hidden: { display: "none" },
})
const menuButtonVariants = stylex.create({
default: {},
outline: {
backgroundColor: {
default: color.background,
":hover": color.itemHighlightBg,
":active": color.itemHighlightBg,
},
boxShadow: {
default: `0 0 0 1px ${sidebarBorder}`,
":hover": `0 0 0 1px ${sidebarAccent}`,
":focus-visible": shadow.rowRing,
},
},
})
const menuButtonSizes = stylex.create({
default: { height: space.s8, fontSize: text.sm, lineHeight: leading.sm },
sm: { height: space.s7, fontSize: text.xs, lineHeight: leading.xs },
lg: { height: space.s12, fontSize: text.sm, lineHeight: leading.sm },
})
const menuActionTop = stylex.create({
default: { top: space.s1_5 },
sm: { top: space.s1 },
lg: { top: space.s2_5 },
})
const menuSubButtonSizes = stylex.create({
sm: { fontSize: text.xs, lineHeight: leading.xs },
md: { fontSize: text.sm, lineHeight: leading.sm },
})
const sidebarStyles = {
menuButton: [styles.menuButton, states.rowFocusRing],
menuButtonVariants,
menuButtonSizes,
menuSubButton: [styles.menuSubButton, states.rowFocusRing],
truncate: styles.truncate,
}
type SidebarMenuButtonVariant = keyof typeof menuButtonVariants
type SidebarMenuButtonSize = keyof typeof menuButtonSizes
type SidebarMenuSubButtonSize = keyof typeof menuSubButtonSizes
type Overrides = { xstyle?: StyleXStyles; className?: string }
function subscribeToViewport(onChange: () => void) {
const list = window.matchMedia(MOBILE_QUERY)
list.addEventListener("change", onChange)
return () => list.removeEventListener("change", onChange)
}
function useIsMobile() {
return useSyncExternalStore(
subscribeToViewport,
() => window.matchMedia(MOBILE_QUERY).matches,
() => false,
)
}
function useSidebar() {
const context = use(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
type SidebarProviderProps = useRender.ComponentProps<"div"> &
Overrides & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
variant?: SidebarVariant
}
function SidebarProvider({
render,
ref,
style,
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
variant = "sidebar",
xstyle,
className,
children,
...props
}: SidebarProviderProps) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = useState(false)
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
const open = openProp ?? uncontrolledOpen
const [instant, setInstant] = useState(false)
const restoreFrom = useRef({ controlled: openProp !== undefined, open: uncontrolledOpen })
useLayoutEffect(() => {
if (restoreFrom.current.controlled) return
const saved = new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=(true|false)`).exec(
document.cookie,
)?.[1]
if (saved === undefined || (saved === "true") === restoreFrom.current.open) return
setInstant(true)
setUncontrolledOpen(saved === "true")
let frame = requestAnimationFrame(() => {
frame = requestAnimationFrame(() => setInstant(false))
})
return () => cancelAnimationFrame(frame)
}, [])
const setOpen = useCallback(
(value: boolean | ((open: boolean) => boolean)) => {
const next = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(next)
} else {
setUncontrolledOpen(next)
}
document.cookie = `${SIDEBAR_COOKIE_NAME}=${next}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open],
)
const [peeking, setPeekingNow] = useState(false)
const peekTimer = useRef<number | undefined>(undefined)
const setPeeking = useCallback((next: boolean) => {
window.clearTimeout(peekTimer.current)
if (next) setPeekingNow(true)
else peekTimer.current = window.setTimeout(() => setPeekingNow(false), SIDEBAR_PEEK_CLOSE_DELAY)
}, [])
useEffect(() => {
if (open) {
window.clearTimeout(peekTimer.current)
setPeekingNow(false)
}
}, [open])
useEffect(() => () => window.clearTimeout(peekTimer.current), [])
const toggleSidebar = useCallback(() => {
if (isMobile) {
setOpenMobile((current) => !current)
} else {
setOpen((current) => !current)
}
}, [isMobile, setOpen])
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
} else if (event.key === "Escape") {
window.clearTimeout(peekTimer.current)
setPeekingNow(false)
}
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [toggleSidebar])
const state: SidebarState = open ? "expanded" : "collapsed"
const contextValue = useMemo<SidebarContextValue>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
variant,
peeking,
setPeeking,
instant,
}),
[
state,
open,
setOpen,
isMobile,
openMobile,
toggleSidebar,
variant,
peeking,
setPeeking,
instant,
],
)
const sx = stylex.props(styles.wrapper, variant === "inset" && styles.wrapperInset, xstyle)
return (
<SidebarContext value={contextValue}>
{useRender({
defaultTagName: "div",
render,
ref,
props: {
"data-slot": "sidebar-wrapper",
"data-variant": variant,
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
children,
},
})}
</SidebarContext>
)
}
type SidebarProps = useRender.ComponentProps<"div"> &
Overrides & {
side?: SidebarSide
collapsible?: SidebarCollapsible
}
function Sidebar({
style,
side = "left",
collapsible = "offcanvas",
xstyle,
className,
children,
onPointerEnter,
onPointerLeave,
...props
}: SidebarProps) {
const { isMobile, state, openMobile, setOpenMobile, variant, peeking, setPeeking, instant } =
useSidebar()
if (collapsible === "none") {
const sx = stylex.props(styles.none, xstyle)
return (
<SidebarPanelContext value={{ side, collapsible, iconMode: false }}>
<SidebarPanel
data-slot="sidebar"
data-side={side}
data-variant={variant}
{...sx}
{...props}
className={clsx(sx.className, className)}
style={{ ...sx.style, ...style }}
>
{children}
</SidebarPanel>
</SidebarPanelContext>
)
}
if (isMobile) {
return (
<SidebarPanelContext value={{ side, collapsible, iconMode: false }}>
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent
data-sidebar="sidebar"
data-mobile=""
data-side={side}
side={side}
showCloseButton={false}
xstyle={[styles.sheet, xstyle]}
style={style}
className={className}
{...props}
>
<SheetHeader xstyle={states.srOnly}>
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div {...stylex.props(styles.sheetBody)}>{children}</div>
</SheetContent>
</Sheet>
</SidebarPanelContext>
)
}
const collapsed = state === "collapsed"
const offcanvas = collapsed && collapsible === "offcanvas"
const peek = offcanvas && peeking
const icon = collapsed && collapsible === "icon"
const padded = variant === "floating" || variant === "inset"
const sx = stylex.props(
styles.container,
side === "left" ? styles.containerLeft : styles.containerRight,
offcanvas &&
!peek &&
(side === "left" ? styles.containerLeftOffcanvas : styles.containerRightOffcanvas),
peek && (side === "left" ? styles.containerPeekLeft : styles.containerPeekRight),
instant && styles.instant,
padded && styles.containerPadded,
icon && (padded ? styles.containerIconPadded : styles.containerIcon),
!padded && (side === "left" ? styles.containerBorderRight : styles.containerBorderLeft),
xstyle,
)
return (
<SidebarPanelContext value={{ side, collapsible, iconMode: icon }}>
<div
data-slot="sidebar"
data-state={state}
data-collapsible={collapsed ? collapsible : ""}
data-peek={peek ? "" : undefined}
data-variant={variant}
data-side={side}
{...stylex.props(styles.root)}
>
<div
data-slot="sidebar-gap"
{...stylex.props(
styles.gap,
offcanvas && styles.gapOffcanvas,
icon && (padded ? styles.gapIconPadded : styles.gapIcon),
instant && styles.instant,
)}
/>
<SidebarPanel
data-slot="sidebar-container"
data-side={side}
{...sx}
{...props}
onPointerEnter={(event) => {
onPointerEnter?.(event)
if (offcanvas) setPeeking(true)
}}
onPointerLeave={(event) => {
onPointerLeave?.(event)
if (offcanvas) setPeeking(false)
}}
className={clsx(sx.className, className)}
style={{ ...sx.style, ...style }}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
{...stylex.props(
styles.inner,
variant === "floating" && styles.innerFloating,
variant === "inset" && styles.innerInset,
)}
>
{children}
</div>
</SidebarPanel>
</div>
</SidebarPanelContext>
)
}
function SidebarPanel({ render, ref, ...props }: useRender.ComponentProps<"div">) {
return useRender({ defaultTagName: "div", render, ref, props })
}
type SidebarTriggerProps = ComponentProps<typeof Button> & {
peek?: boolean
}
function SidebarTrigger({
onClick,
onPointerEnter,
onPointerLeave,
peek = false,
...props
}: SidebarTriggerProps) {
const { toggleSidebar, open, isMobile, setPeeking } = useSidebar()
const peeks = peek && !open && !isMobile
return (
<Button
data-slot="sidebar-trigger"
variant="ghost"
size="icon-sm"
aria-label="Toggle sidebar"
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
onPointerEnter={(event) => {
onPointerEnter?.(event)
if (peeks) setPeeking(true)
}}
onPointerLeave={(event) => {
onPointerLeave?.(event)
if (peeks) setPeeking(false)
}}
{...props}
>
<PanelLeft />
</Button>
)
}
function SidebarRail({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"button"> & Overrides) {
const { toggleSidebar, state } = useSidebar()
const { side, collapsible } = use(SidebarPanelContext)
const collapsed = state === "collapsed"
const offcanvas = collapsed && collapsible === "offcanvas"
const left = side === "left"
const sx = stylex.props(
styles.rail,
left ? styles.railLeft : styles.railRight,
collapsed && (left ? styles.railLeftCollapsed : styles.railRightCollapsed),
offcanvas && styles.railOffcanvas,
offcanvas && (left ? styles.railLeftOffcanvas : styles.railRightOffcanvas),
xstyle,
)
return useRender({
defaultTagName: "button",
render,
ref,
props: {
type: "button",
"data-slot": "sidebar-rail",
"aria-label": "Toggle sidebar",
tabIndex: -1,
onClick: toggleSidebar,
title: "Toggle sidebar",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
type SidebarInsetProps = useRender.ComponentProps<"main"> & Overrides
function SidebarInset({ render, ref, style, xstyle, className, ...props }: SidebarInsetProps) {
const { variant, state } = useSidebar()
const inset = variant === "inset"
const sx = stylex.props(
styles.inset,
inset && styles.insetVariant,
inset && state === "collapsed" && styles.insetVariantCollapsed,
xstyle,
)
return useRender({
defaultTagName: "main",
render,
ref,
props: {
"data-slot": "sidebar-inset",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
function SidebarInput({ xstyle, ...props }: ComponentProps<typeof Input>) {
return <Input data-slot="sidebar-input" xstyle={[styles.input, xstyle]} {...props} />
}
function SidebarHeader({
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
return (
<VStack
data-slot="sidebar-header"
gap={2}
padding={2}
xstyle={xstyle}
className={className}
{...props}
/>
)
}
function SidebarFooter({
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
return (
<VStack
data-slot="sidebar-footer"
gap={2}
padding={2}
xstyle={xstyle}
className={className}
{...props}
/>
)
}
function SidebarSeparator({ xstyle, ...props }: ComponentProps<typeof Separator>) {
return <Separator data-slot="sidebar-separator" xstyle={[styles.separator, xstyle]} {...props} />
}
function SidebarContent({
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
const { iconMode } = use(SidebarPanelContext)
return (
<VStack
data-slot="sidebar-content"
grow
scrollable
xstyle={[iconMode && styles.contentIcon, xstyle]}
className={clsx("no-scrollbar", className)}
{...props}
/>
)
}
function SidebarGroup({
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
return (
<VStack
data-slot="sidebar-group"
padding={2}
xstyle={[styles.group, xstyle]}
className={className}
{...props}
/>
)
}
type SidebarGroupLabelProps = useRender.ComponentProps<"div"> & { xstyle?: StyleXStyles }
function SidebarGroupLabel({ xstyle, className, render, ...props }: SidebarGroupLabelProps) {
const { iconMode } = use(SidebarPanelContext)
const sx = stylex.props(
styles.groupLabel,
states.rowFocusRing,
iconMode && styles.groupLabelIcon,
xstyle,
)
return useRender({
defaultTagName: "div",
render,
props: {
"data-slot": "sidebar-group-label",
...sx,
...props,
className: clsx("carbon-icons", sx.className, className),
},
})
}
type SidebarGroupActionProps = useRender.ComponentProps<"button"> & { xstyle?: StyleXStyles }
function SidebarGroupAction({ xstyle, className, render, ...props }: SidebarGroupActionProps) {
const { iconMode } = use(SidebarPanelContext)
const sx = stylex.props(
styles.groupAction,
states.rowFocusRing,
iconMode && styles.hidden,
xstyle,
)
return useRender({
defaultTagName: "button",
render,
props: {
type: "button",
"data-slot": "sidebar-group-action",
...sx,
...props,
className: clsx("carbon-icons", sx.className, className),
},
})
}
function SidebarGroupContent({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
const sx = stylex.props(styles.groupContent, xstyle)
return useRender({
defaultTagName: "div",
render,
ref,
props: {
"data-slot": "sidebar-group-content",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
function SidebarMenu({ xstyle, className, ...props }: useRender.ComponentProps<"ul"> & Overrides) {
return (
<VStack<"ul">
render={<ul />}
data-slot="sidebar-menu"
gap={1}
xstyle={[styles.menu, xstyle]}
className={className}
{...props}
/>
)
}
function SidebarMenuItem({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"li"> & Overrides) {
const [size, setSize] = useState<SidebarMenuButtonSize>("default")
const [hasAction, setHasAction] = useState(false)
const value = useMemo(() => ({ size, setSize, hasAction, setHasAction }), [size, hasAction])
const sx = stylex.props(styles.menuItem, sidebarMenuItem, xstyle)
return (
<SidebarMenuItemContext value={value}>
{useRender({
defaultTagName: "li",
render,
ref,
props: {
"data-slot": "sidebar-menu-item",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})}
</SidebarMenuItemContext>
)
}
type SidebarMenuButtonProps = useRender.ComponentProps<"button", { active: boolean }> & {
isActive?: boolean
variant?: SidebarMenuButtonVariant
size?: SidebarMenuButtonSize
tooltip?: string | ComponentProps<typeof TooltipContent>
xstyle?: StyleXStyles
}
function SidebarMenuButton({
render,
isActive = false,
variant = "default",
size = "default",
tooltip,
xstyle,
className,
...props
}: SidebarMenuButtonProps) {
const { isMobile, state } = useSidebar()
const { iconMode } = use(SidebarPanelContext)
const item = use(SidebarMenuItemContext)
useLayoutEffect(() => {
item?.setSize(size)
}, [item, size])
const sx = stylex.props(
styles.menuButton,
states.rowFocusRing,
menuButtonVariants[variant] ?? menuButtonVariants.default,
menuButtonSizes[size] ?? menuButtonSizes.default,
isActive && styles.rowActive,
isActive && styles.rowActiveLabel,
item?.hasAction && !iconMode && styles.menuButtonWithAction,
iconMode && styles.menuButtonIcon,
iconMode && size === "lg" && styles.menuButtonIconLg,
xstyle,
)
const button = useRender({
defaultTagName: "button",
render: tooltip ? (
<TooltipTrigger
render={
typeof render === "function" ? (props) => render(props, { active: isActive }) : render
}
/>
) : (
render
),
state: { active: isActive },
stateAttributesMapping: { active: (active) => (active ? { "data-active": "true" } : null) },
props: {
type: render ? undefined : "button",
"data-slot": "sidebar-menu-button",
"data-sidebar": "menu-button",
"data-variant": variant,
"data-size": size,
...sx,
...props,
className: clsx("carbon-icons", sx.className, className),
},
})
if (!tooltip) return button
const content = typeof tooltip === "string" ? { children: tooltip } : tooltip
return (
<Tooltip>
{button}
{state === "collapsed" && !isMobile && (
<TooltipContent side="right" align="center" {...content} />
)}
</Tooltip>
)
}
type SidebarMenuActionProps = useRender.ComponentProps<"button"> & {
showOnHover?: boolean
xstyle?: StyleXStyles
}
function SidebarMenuAction({
xstyle,
className,
render,
showOnHover = false,
...props
}: SidebarMenuActionProps) {
const { iconMode } = use(SidebarPanelContext)
const item = use(SidebarMenuItemContext)
useLayoutEffect(() => {
if (!item) return
item.setHasAction(true)
return () => item.setHasAction(false)
}, [item])
const sx = stylex.props(
styles.menuAction,
states.rowFocusRing,
menuActionTop[item?.size ?? "default"],
showOnHover && styles.menuActionShowOnHover,
iconMode && styles.hidden,
xstyle,
)
return useRender({
defaultTagName: "button",
render,
props: {
type: render ? undefined : "button",
"data-slot": "sidebar-menu-action",
"data-sidebar": "menu-action",
...sx,
...props,
className: clsx("carbon-icons", sx.className, className),
},
})
}
function SidebarMenuBadge({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"div"> & Overrides) {
const { iconMode } = use(SidebarPanelContext)
const item = use(SidebarMenuItemContext)
const sx = stylex.props(
styles.menuBadge,
menuActionTop[item?.size ?? "default"],
iconMode && styles.hidden,
xstyle,
)
return useRender({
defaultTagName: "div",
render,
ref,
props: {
"data-slot": "sidebar-menu-badge",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
type SidebarMenuSkeletonProps = useRender.ComponentProps<"div"> & Overrides & { showIcon?: boolean }
function SidebarMenuSkeleton({
render,
ref,
style,
xstyle,
className,
showIcon = false,
...props
}: SidebarMenuSkeletonProps) {
const id = useId()
let hash = 0
for (const char of id) hash = (hash * 31 + char.charCodeAt(0)) % 41
const width = `${50 + hash}%`
const sx = stylex.props(styles.menuSkeleton, xstyle)
return useRender({
defaultTagName: "div",
render,
ref,
props: {
"data-slot": "sidebar-menu-skeleton",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
children: (
<>
{showIcon && (
<Skeleton data-slot="sidebar-menu-skeleton-icon" xstyle={styles.menuSkeletonIcon} />
)}
<Skeleton
data-slot="sidebar-menu-skeleton-text"
xstyle={styles.menuSkeletonText}
style={{ maxWidth: width }}
/>
</>
),
},
})
}
function SidebarMenuSub({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"ul"> & Overrides) {
const { iconMode } = use(SidebarPanelContext)
const sx = stylex.props(styles.menuSub, iconMode && styles.hidden, xstyle)
return useRender({
defaultTagName: "ul",
render,
ref,
props: {
"data-slot": "sidebar-menu-sub",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
function SidebarMenuSubItem({
render,
ref,
style,
xstyle,
className,
...props
}: useRender.ComponentProps<"li"> & Overrides) {
const sx = stylex.props(styles.menuSubItem, xstyle)
return useRender({
defaultTagName: "li",
render,
ref,
props: {
"data-slot": "sidebar-menu-sub-item",
...sx,
...props,
className: clsx(sx.className, className),
style: { ...sx.style, ...style },
},
})
}
type SidebarMenuSubButtonProps = useRender.ComponentProps<"a", { active: boolean }> & {
size?: SidebarMenuSubButtonSize
isActive?: boolean
xstyle?: StyleXStyles
}
function SidebarMenuSubButton({
render,
size = "md",
isActive = false,
xstyle,
className,
...props
}: SidebarMenuSubButtonProps) {
const { iconMode } = use(SidebarPanelContext)
const sx = stylex.props(
styles.menuSubButton,
states.rowFocusRing,
menuSubButtonSizes[size] ?? menuSubButtonSizes.md,
isActive && styles.rowActive,
iconMode && styles.hidden,
xstyle,
)
return useRender({
defaultTagName: "a",
render,
state: { active: isActive },
stateAttributesMapping: { active: (active) => (active ? { "data-active": "true" } : null) },
props: {
"data-slot": "sidebar-menu-sub-button",
"data-size": size,
...sx,
...props,
className: clsx("carbon-icons", sx.className, className),
},
})
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
sidebarStyles,
useSidebar,
type SidebarCollapsible,
type SidebarMenuButtonSize,
type SidebarMenuButtonVariant,
type SidebarMenuSubButtonSize,
type SidebarSide,
type SidebarVariant,
}