Duck Settings Panel
Sections of label-beside-control rows, labelled without ever minting an id for a control it does not own, plus a save bar that appears only when there is something to save.
BlockClient component
Preview
Installation
One command writes the section and every component it renders.
$
pnpm dlx shadcn@latest add @duck/duck-settings-panelThe block lands in components/blocks/duck-settings-panel.tsx. It builds on sticker-card, holo-separator and quack-button, which the CLI installs alongside it. Already installed items are skipped.
Usage
import { DuckSettingsPanel } from "@/components/blocks/duck-settings-panel"A block is a starting point, not a widget. The props exist so the example renders with real content — once the file is in your project, hard-code what never changes and delete the rest.
Props
Everything not listed here is forwarded to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | React.ReactNode | - | Page heading above the sections. |
| description | React.ReactNode | - | One paragraph under it. |
| sections | DuckSettingsSection[] | - | title, description, actions, rows. Sections are separated by a rule, or carded with variant. |
| dirty | boolean | false | Reveals the save bar. The panel holds no values, so this is yours — compare against what you loaded. |
| saving | boolean | false | Disables both buttons and puts save into its loading state. |
| onReset | () => void | - | Renders the reset button when passed. |
| footer | React.ReactNode | - | Left of the buttons: an error, a "saved 2 minutes ago". |
| variant | "plain" | "cards" | "plain" | Sections divided by a hairline, or each one in its own sticker card. |
| saveLabel | string | "Save changes" | Submit button label. |
| resetLabel | string | "Reset" | Reset button label. |
Rules
What keeps this section from turning into noise.
- A row is a label, so the control is associated implicitly. The block never mints an id for a control it did not create — cloning props into an unknown element breaks the day someone passes a composed one.
- A plural control takes labelling="group": a radio group, an OTP strip or a slider pair cannot share one label, since a label may only point at one field.
- The control column is a fixed width. Twenty rows that each size themselves against their own label do not line up.
- The save bar appears only when dirty, inside a polite live region — its arrival is the feedback that an edit registered.
- It sticks to the panel, not the viewport. A settings panel inside a dialog must not pin a bar over the page behind it.
