Settings layout
ProSettings: 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.
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
"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>
| Prop | Type | Default | Description |
|---|---|---|---|
| groupsrequired | SettingsGroup[] | — | The sections, under their group labels; locked ones stay listed with the reason. |
| activerequired | string | — | 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. |
| title | string | undefined | — | The accessible name of the section list. Defaults to "Settings". |
| childrenrequired | React.ReactNode | — | What goes inside. |
| className | string | undefined | — | Extra classes for the outer element. |
<SettingsSection>
The heading and body of one settings section.
| Prop | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | — | The section's heading. |
| description | React.ReactNode | undefined | — | A line under the heading saying what the section controls. |
| childrenrequired | React.ReactNode | — | What goes inside. |
| className | string | 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.
| Key | English |
|---|---|
| settings | Settings |
| 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[] }