App sidebar

Pro

The left rail: the brand, a new-thread button, the main places (threads, inbox, approvals…), the thread list, and the signed-in person at the foot.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/app-sidebar

Brings in: dropdown-menu, locale, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { BookOpen, Inbox, MessagesSquare, Play, Settings, ShieldCheck } from "lucide-react"
import { AppSidebar, type NavItem } from "@/components/app-sidebar"
import { SessionList } from "@/components/session-list"
import type { Session } from "@/lib/team"

const minutesAgo = (m: number) => new Date(Date.now() - m * 60_000).toISOString()

const nav: NavItem[] = [
  { id: "threads", label: "Threads", Icon: MessagesSquare },
  { id: "inbox", label: "Inbox", Icon: Inbox, count: 2, urgent: true },
  { id: "approvals", label: "Approvals", Icon: ShieldCheck, count: 1, urgent: true },
  { id: "runs", label: "Runs", Icon: Play, count: 4 },
  { id: "docs", label: "Docs", Icon: BookOpen },
  { id: "settings", label: "Settings", Icon: Settings },
]

const sessions: Session[] = [
  { id: "s1", title: "Add a region column to orders", updatedAt: minutesAgo(2), status: "running" },
  { id: "s2", title: "Why is checkout slow on Staging?", updatedAt: minutesAgo(40) },
  { id: "s3", title: "Clean up stale feature flags", updatedAt: minutesAgo(60 * 26) },
]

export default function Example() {
  const [place, setPlace] = React.useState("threads")
  const [thread, setThread] = React.useState("s1")
  return (
    <div className="h-[560px] w-64 overflow-hidden rounded-lg border">
      <AppSidebar
        brand={<span className="px-1 text-sm font-semibold">Keel</span>}
        nav={nav}
        activeNav={place}
        onNavigate={setPlace}
        onNewThread={() => {}}
        user={{ name: "Sam", email: "sam@acme.example", role: "Owner · Acme" }}
        usage={{ label: "Agent hours this month", used: 0.62 }}
        onProfile={() => {}}
        onShortcuts={() => {}}
        onSignOut={() => {}}
      >
        <SessionList sessions={sessions} activeId={place === "threads" ? thread : undefined} onOpen={(id) => (setPlace("threads"), setThread(id))} />
      </AppSidebar>
    </div>
  )
}

<AppSidebar>

PropTypeDefaultDescription
brandrequiredReact.ReactNode—The product's logo or name, at the top.
navrequiredNavItem[]—The main places, each with an icon and an optional count.
activeNavstring | undefined—The id of the current place, marked as the current page.
onNavigate((id: string) => void) | undefined—Called with a place's id when it's chosen. For a place with an `href`, a plain click calls this instead of following the link (for an app's own router); other clicks open the link as usual.
onNewThread(() => void) | undefined—Shows a new-thread button by the brand; called when it's pressed.
userrequired{ name: string; email: string; role?: string | undefined }—The signed-in person, at the foot; the role shows in place of the email when given.
usage{ label: string; used: number } | undefined—e.g. "62% of this month’s agent hours".
onSignOut(() => void) | undefined—Adds Sign out to the person menu; called when it's chosen.
onProfile(() => void) | undefined—Adds Profile to the person menu; called when it's chosen.
onShortcuts(() => void) | undefined—Adds Keyboard shortcuts to the person menu; called when it's chosen.
searchReact.ReactNode | undefined—Above the places; usually a CommandTrigger.
childrenReact.ReactNode | undefined—Usually a SessionList.
help{ href?: string | undefined; onOpen?: (() => void) | undefined; label?: string | undefined } | undefined—Where people get help, in the same place on every page (WCAG 3.2.6): a link (`href`) or your own handler (`onOpen`), at the foot above the person menu. `label` defaults to "Help".
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under appSidebar in <LocaleProvider strings={{ … }}>; the table is exported as appSidebarStrings.

KeyEnglish
newThreadNew thread
mainMain
waitingOnYouwaiting on you
profileProfile
keyboardShortcutsKeyboard shortcuts
signOutSign out
helpHelp

Types

NavItem
{ id: string label: string Icon: LucideIcon /** A count shown only when the service supplied it. */ count?: number | undefined /** Count means "needs you": shown in the accent, not grey. */ urgent?: boolean | undefined /** Where the place lives. Makes it a real link, so it opens in a new tab and works without JavaScript. */ href?: string | undefined }