SettingsDialog
A settings window: sections on the left, one pane on the right, and an optional search that jumps to the row it found. It lives in its own entry point with the parts a pane is built from.
import { SettingsDialog } from '@adecore/ui/settings';import { useState } from 'react';
import { Bell, Info, Keyboard, Palette, Settings, Sparkles } from 'lucide-react';
import { Button, Icon, Select, Switch } from '@adecore/ui';
import { MasterDetail, MasterItem, SettingsDialog, SettingsRow, SettingsSection, type SettingsSearchResult } from '@adecore/ui/settings';
function AppearancePane() {
const [theme, setTheme] = useState('system');
const [compact, setCompact] = useState(false);
return (
<SettingsSection title="Window" description="How the app looks on this computer.">
<SettingsRow
label="Theme"
searchId="theme"
control={
<Select
label="Theme"
value={theme}
onValueChange={setTheme}
items={[
{ value: 'system', label: 'Match the system' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]}
/>
}
/>
<SettingsRow
label="Compact rows"
description="Fits more on a small screen."
searchId="compact"
control={<Switch label="Compact rows" checked={compact} onCheckedChange={setCompact} />}
/>
</SettingsSection>
);
}
function NotificationsPane() {
const [sounds, setSounds] = useState(true);
return (
<SettingsSection title="Alerts">
<SettingsRow label="Play a sound" searchId="sound" control={<Switch label="Play a sound" checked={sounds} onCheckedChange={setSounds} />} />
</SettingsSection>
);
}
const SHORTCUT_GROUPS = ['General', 'Editing', 'Navigation'];
function ShortcutsPane() {
const [picked, setPicked] = useState('General');
return (
<MasterDetail
listWidth={280}
listLabel="Shortcut groups"
list={SHORTCUT_GROUPS.map((group) => (
<MasterItem key={group} selected={picked === group} onSelect={() => setPicked(group)}>
{group}
</MasterItem>
))}
detail={<p className="text-sm text-text-muted">The shortcuts of {picked.toLowerCase()} go here.</p>}
/>
);
}
function AboutHero() {
return (
<div className="flex flex-col items-center gap-1.5 bg-surface-sunken px-8 pt-28 pb-12 text-center">
<Icon icon={Sparkles} size={20} className="mb-2 text-accent" />
<h3 className="text-lg font-semibold text-text">Example</h3>
<p className="text-xs text-text-muted">Version 1.0.0</p>
</div>
);
}
const DETAILS = [
{ label: 'Electron', value: '38.2.0' },
{ label: 'Chromium', value: '140.0.7339' },
{ label: 'Node', value: '22.19.0' },
{ label: 'Platform', value: 'macOS' }
];
const LINKS = ['Website', 'Source code', 'Report a problem'];
function AboutPane() {
const [automatic, setAutomatic] = useState(true);
return (
<>
<SettingsSection title="Updates">
<SettingsRow
label="Download updates by themselves"
control={<Switch label="Download updates by themselves" checked={automatic} onCheckedChange={setAutomatic} />}
/>
</SettingsSection>
<SettingsSection title="Details">
{DETAILS.map((row) => (
<SettingsRow key={row.label} label={row.label} control={<span className="text-xs text-text-muted">{row.value}</span>} />
))}
</SettingsSection>
<SettingsSection title="Links">
{LINKS.map((link) => (
<SettingsRow
key={link}
label={link}
control={
<Button variant="secondary" size="sm">
Open
</Button>
}
/>
))}
</SettingsSection>
</>
);
}
const GROUPS = [
{
label: null,
sections: [
{ id: 'appearance', icon: Palette, label: 'Appearance', description: 'Theme and density.', pane: AppearancePane },
{ id: 'notifications', icon: Bell, label: 'Notifications', description: 'What asks for your attention.', pane: NotificationsPane }
]
},
{
label: 'Advanced',
sections: [{ id: 'shortcuts', icon: Keyboard, label: 'Shortcuts', description: 'Every key the app knows.', pane: ShortcutsPane, split: true }]
}
];
const FOOTER = [{ id: 'about', icon: Info, label: 'About', description: 'The version you run.', pane: AboutPane, hero: AboutHero }];
const SEARCHABLE: SettingsSearchResult[] = [
{ section: 'appearance', id: null, label: 'Appearance' },
{ section: 'appearance', id: 'theme', label: 'Theme' },
{ section: 'appearance', id: 'compact', label: 'Compact rows' },
{ section: 'notifications', id: 'sound', label: 'Play a sound' },
{ section: 'shortcuts', id: null, label: 'Shortcuts' }
];
const search = {
find: (query: string) => SEARCHABLE.filter((result) => result.label.toLowerCase().includes(query.trim().toLowerCase())),
hint: '⌘F'
};
export default function SettingsDialogDemo() {
const [open, setOpen] = useState(false);
const [section, setSection] = useState('appearance');
const [target, setTarget] = useState<string | null>(null);
return (
<>
<Button variant="secondary" onClick={() => setOpen(true)}>
<Icon icon={Settings} size={14} />
Open settings
</Button>
<SettingsDialog
open={open}
onOpenChange={setOpen}
section={section}
onNavigate={(next) => {
setSection(next.section);
setTarget(next.target ?? null);
}}
groups={GROUPS}
footer={FOOTER}
search={search}
target={target}
onTargetShown={() => setTarget(null)}
/>
</>
);
}Sections and panes
You describe the sections, the dialog draws them. groups is the navigation, in order, each with an optional label (the first group usually goes without one). footer sections stand at the foot of the navigation. A section names a pane, a component the dialog renders only while that section is open, inside a Suspense and an ErrorBoundary. A lazy pane loads when its section is first opened.
A pane is a padded column that scrolls, with a fade at its top once something scrolled under the header. A split section gets the whole height instead, for a MasterDetail whose two sides scroll on their own.
A section with a hero opens on that block instead, run to the edges of the pane, with the title bar floating over its top. The bar is clear at first and fills in with the dialog's surface and a hairline over the first 70 pixels of scroll. The bar covers the top 86 pixels of the hero, more when its description wraps. The pane follows under the hero, one gap below it, in the same padded column as any other pane.
You keep which section is open. onNavigate tells you when a person picks another, with the arrow keys in the navigation or a click.
Search
search.find(query) answers the results for what a person typed. It is yours, so it can search your own words in any language. A result names a section and, optionally, the searchId of a SettingsRow in it. Picking one calls onNavigate with the section and the row as target. Pass that back in as target, and the row scrolls into view and lights up for a moment, then calls onTargetShown, where you clear it.
Escape in the search field clears the query first; only an empty field lets Escape close the dialog. Enter jumps to the first result. search.hint prints the shortcut that focuses the field, and search.focusAt focuses and selects it each time the number grows, so your shortcut handler can bump it.
An account at the foot
account puts one more section under the footer, past a hairline, whose tab you draw yourself, such as an avatar and a name. The tab is a Base UI Tabs.Tab with the section's id as its value.
Narrow windows
The dialog is 1200 by 760 pixels and steps down with the viewport. Under 960 pixels the navigation narrows, and under 640 it turns into a select of sections above the pane.
Props
| Prop | Type | |
|---|---|---|
open | boolean | Required. |
onOpenChange | (open: boolean) => void | Required. |
section | string | Required. The id of the open section. |
onNavigate | (next: { section: string; target?: string | null }) => void | Required. |
groups | readonly SettingsGroupEntry[] | Required. { label: string | null; sections } |
footer | readonly SettingsSectionEntry[] | |
account | { section: SettingsSectionEntry; tab: ReactNode } | |
search | SettingsSearch | { find(query), hint?, focusAt? } |
target | string | null | The row a search result jumped to. |
onTargetShown | () => void | |
className | string | On the popup. |
ref | Ref<HTMLDivElement> |
A SettingsSectionEntry is { id, icon, label, description, pane, split?, hero? }, where the description is the line under the pane's title. A SettingsSearchResult is { section, id, label }, with id: null for a result that is the pane itself.
SettingsDialogProps, SettingsGroupEntry, SettingsSectionEntry, SettingsSearch and SettingsSearchResult are exported types.