Settings layout

Pro

Settings: grouped sections down the side and the open one beside them. Narrow (it measures its own width, so it works inside a panel too), the list becomes a select above the section. Sections the viewer can't open stay in the list with a lock and the reason, rather than disappearing.

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/settings-layout

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

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Bell, CreditCard, KeyRound, Plug, UserRound, Users } from "lucide-react"
import { SettingsLayout, SettingsSection, type SettingsGroup } from "@/components/settings-layout"

const groups: SettingsGroup[] = [
  {
    label: "You",
    items: [
      { id: "profile", label: "Profile", Icon: UserRound },
      { id: "notifications", label: "Notifications", Icon: Bell },
    ],
  },
  {
    label: "Workspace",
    items: [
      { id: "members", label: "Members", Icon: Users },
      { id: "connections", label: "Connections", Icon: Plug },
      { id: "keys", label: "API keys", Icon: KeyRound },
      { id: "billing", label: "Billing", Icon: CreditCard, locked: "Only owners can see billing" },
    ],
  },
]

const sections: Record<string, { title: string; description: string; body: string }> = {
  profile: { title: "Profile", description: "How you appear to your team.", body: "Name, avatar and time zone." },
  notifications: { title: "Notifications", description: "What Keel tells you about, and where.", body: "Approvals and failed runs always notify you." },
  members: { title: "Members", description: "Who can use Keel in Acme.", body: "8 members, 2 pending invites." },
  connections: { title: "Connections", description: "Repositories and databases the agents can reach.", body: "GitHub, Production and Staging databases." },
  keys: { title: "API keys", description: "Keys for calling Keel from CI.", body: "No keys yet." },
}

export default function Example() {
  const [active, setActive] = React.useState("connections")
  const s = sections[active]!
  return (
    <div className="flex h-[420px] overflow-hidden rounded-lg border">
      <SettingsLayout groups={groups} active={active} onSelect={setActive}>
        <SettingsSection title={s.title} description={s.description}>
          <p className="text-sm">{s.body}</p>
        </SettingsSection>
      </SettingsLayout>
    </div>
  )
}

<SettingsLayout>

PropTypeDefaultDescription
groupsrequiredSettingsGroup[]—The sections, under their group labels; locked ones stay listed with the reason.
activerequiredstring—The id of the open section.
onSelectrequired(id: string) => void—Called with a section's id when it's chosen; never for a locked one.
titlestring | undefined—The accessible name of the section list. Defaults to "Settings".
childrenrequiredReact.ReactNode—What goes inside.
classNamestring | undefined—Extra classes for the outer element.

<SettingsSection>

The heading and body of one settings section.

PropTypeDefaultDescription
titlerequiredstring—The section's heading.
descriptionReact.ReactNode | undefined—A line under the heading saying what the section controls.
childrenrequiredReact.ReactNode—What goes inside.
classNamestring | undefined—Extra classes for the outer element.

Strings

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

KeyEnglish
settingsSettings
section{title} section
locked{label} (locked: {reason})

Types

SettingsItem
{ id: string label: string Icon?: LucideIcon | undefined /** Why this section can't be opened by the viewer; it stays listed. */ locked?: string | undefined }
SettingsGroup
{ label: string; items: SettingsItem[] }