A control panel of small independent decisions, and the ruling on which of them apply immediately versus wait behind a Save.
Page-class laws·docs/settings.md·111 lines
Kit src/components/settings-page/Living example · Settings
How every settings page — a control panel of small, independent decisions — is built. The kit is src/components/settings-page/ (+ the form kit's FormFooter for save-gated cards); the living example is /labs/layout/settings. Same contract as data-view.md: the kit enforces what it can with defaults, this file records the rest.
Sibling boundary with form.md: a form is an interview — one record, one submit, dirty covers the whole page. A settings page is a control panel — many independent decisions, visited repeatedly, changed one at a time. Misclassifying costs real UX: an interview chopped into per-card saves nags; a control panel behind one giant Save loses edits from three sections up.
01
Every section is its own feature, so every section is its own Card — the inverse of the form's one-Card-of-sections, and the cross-class law's "a page with several features shows several cards" in its purest form. The stack sits on the same max-w-2xl measure as forms so the two classes read as kin.
02
<SettingsStack>
<SettingsCard title="General" description="…"
footer={<FormFooter dirty={isDirty}>…Reset · Save…</FormFooter>}>
…identity fields (a miniature form per docs/form.md)…
</SettingsCard>
<SettingsCard title="Preferences" description="…">
<SettingRow label="Language" description="…" htmlFor="lang">
<Select … onValueChange={applyAndToast} />
</SettingRow>
<SettingRow label="Compact mode" description="…" htmlFor="compact">
<Switch … onCheckedChange={applyAndToast} />
</SettingRow>
</SettingsCard>
<SettingsCard danger title="Danger zone" description="…">
<SettingRow label="Delete workspace" description="…">
<AlertDialog>…destructive Button → typed confirm…</AlertDialog>
</SettingRow>
</SettingsCard>
</SettingsStack>| Piece | Component | Rule |
|---|---|---|
| Stack | SettingsStack | Cards top to bottom at max-w-2xl. More than ~5 cards → group into categories via PageHeader's tabs slot, one route per category. |
| Section | SettingsCard | One decision area, one Card. A dashed-seam title band, then SettingRows divided by dashed hairlines. |
| Row | SettingRow | Name + consequence left, control right. The description says what the switch actually does — a toggle whose label needs guessing is a support ticket. |
| Save footer | form kit's FormFooter | Only on save-gated cards; the card is then a miniature form and everything in docs/form.md applies inside it. |
| Danger zone | SettingsCard danger | Destructive ring, destructive title. One per page, ALWAYS last. |
03
Each card commits to exactly one semantic; mixing them in one card is the class's central anti-pattern.
FormFooter; dirty scope is THAT CARD only.AlertDialog is the gate, and truly irreversible actions (delete) require typing the resource name to arm the button.Which semantic a control gets follows from the control type, not taste — that's what makes ten settings pages feel like one product.
04
AlertDialog; delete-grade actions arm only when the user types the resource name. The zone never uses dirty-gating — confirmation IS the gate.05
SettingsCard.FormFooter gated on that card's isDirty.danger card, AlertDialog per action, typed confirm for irreversibles.06