registry
@automindzsheets 01–77shadcn 4.x
SHEET 22

Settings Layout

block / layout

Settings shell with section navigation and a save bar that owns its own dirty state, awaits the save, and puts server field errors back on the fields.

type
block
files
2
registry deps
badge, button, input, label, select, skeleton, switch, textarea, @automindz/confirm-dialog, @automindz/empty-state
npm deps
lucide-react

installs

  • lib/automindz/settings-sections.ts
  • components/automindz/settings-layout.tsx
  • Sections are data; `fields` is what the save bar diffs
  • Dirty state is computed, not passed — there is no `dirty` prop to get wrong
  • Awaited save: pending, a transient confirmation, and a failure that keeps your values
  • Server `fieldErrors` land on the fields, first one focused, unmapped ones still shown
  • Per-section save by default; `saveScope="all"` is one prop, not a fork
  • beforeunload covers tab close; router navigation is an exposed seam, not a pretence

sections is the config: id, label, description, icon, badge, and the fields the bar diffs. Pass initialValues; the layout owns the working copy, so there is no dirty prop to get wrong. onSave is awaited — return { fieldErrors } and they land on the fields, values intact. beforeunload covers tab close, but App Router nav cannot be intercepted here: hold onDirtyChange and call ref.requestLeave() in your guard. Never rename the lib — shadcn add rewrites imports by basename.

Variations

one state per section

01

Dirty state, from the values

Change the workspace name: the bar appears and counts what changed, not that something changed. Change it back and the bar goes. There is no dirty prop to pass — the layout holds the working copy and diffs it against initialValues over the declared fields, so a leftover key or a field dropped from the config cannot pin the bar open with nothing on screen to explain it. Save settles in three beats: pending, a transient Saved, then gone. The baseline moves on success, so a caller that never hands back fresh values still ends up with a clean form.

fluid

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changesin General

02

A save that fails

The same form with a handler that rejects. Everything you typed is still there — a save that clears the form on error is the failure this item exists to prevent. The reason lands on the bar, Save becomes Try again, and the fieldset unlocks so the value can be fixed and sent again. Nothing is logged to a console and nothing is delegated to a toast that outlives the form.

fluid

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changesin General

03

Server field errors

The part hand-rolled versions skip. onSave returns { fieldErrors } keyed by field id: each message lands under its own field with aria-invalid and aria-describedby, the summary above lists them in declaration order, every entry moves focus to its field, and the first one is focused for you. This variant runs saveScope="all", so change something in General and something in Billing, then save from Billing — the layout switches back to General, because an error you cannot see is an error you cannot fix. vat_number is rejected too and no field is called that: it is listed as unmapped rather than dropped.

fluid

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changes

04

Discard, and leaving a dirty section

Discard asks first, through @automindz/confirm-dialog rather than a second dialog of our own, and names the fields it is about to throw away. The section rail is the other half: on the default per-section scope, walking away from unsaved changes asks the same question — that is in-app navigation this component can actually intercept, so it does.

fluid

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changesin General

05

One bar for the whole form

saveScope="all". The bar follows you between sections and counts everything unsaved, and the rail stops asking, because nothing is being left behind. Per-section is the default: a count you cannot see on screen is a count you cannot check before pressing Save. This is the same component with one prop changed, not a fork.

fluid

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changes

06

Read-only, with the reason

A viewer without permission to edit. Every control is really disabled and the reason is stated once at the top, wired to the fieldset with aria-describedby so it is announced on entry rather than buried in a title on each field. The save bar is absent, because there is nothing it could do. Compare the two fields that stay blocked even for an editor: Data region and Purchase order number carry their own disabledReason in the other variants.

fluid

General

How this workspace appears to everyone in it.

You have view-only access to this workspace. Ask an owner for the Admin role to change these settings.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changesin General

07

Loading

Skeletons at the height of the real rows — a label line and a 32px control per field, as many rows as the section has — so the fields land without the page moving under them. The rail keeps rendering: the section list is config and is known before the values are.

fluid

General

How this workspace appears to everyone in it.

0 unsaved changesin General

08

A section with nothing in it yet

Audit log has no fields and no content. That is a state, not an oversight, so it says which plan unlocks it instead of rendering an empty column with a save bar under it. Driven here through activeSection + onActiveSectionChange, which is how a section gets deep-linked from the URL.

fluid

Audit log

Retention and export for the workspace audit trail.

Nothing to configure in Audit log yet

Audit log settings unlock on the Enterprise plan. Nothing here needs your attention yet.

0 unsaved changesin Audit log

09

The navigation guard, honestly

beforeunload covers tab close and reload by itself. In-app App Router navigation cannot be intercepted from inside a component, so the seam is exposed instead of faked: onDirtyChange hands you the flag — the strip above is holding it — and ref.requestLeave() opens the same confirmation the rail uses and resolves once the user gives the changes up. Edit a field, then press Back to overview.

fluid

Your guard sees nothing unsaved

General

How this workspace appears to everyone in it.

Shown in the sidebar and on every invoice.

app.automindz.dev/… — lowercase letters, numbers and dashes.

Optional. Members see this on the workspace card.

Data region

The region is fixed once the first project stores data. Contact support to migrate.

0 unsaved changesin General