Patterns
Action bar
A work surface's commit bar — where a record's intents live, and the only place a change becomes real. The commit doctrine is simple: nothing autosaves, read mode is write mode (no Edit button, no little pencils — you just type), and your staged edits only land when you press an intent. One bar covers everything from a quick rename to a full workflow.
Staged commit
Try editing the name — there's no Edit button to find. The bar lights up with "staged changes"; Save commits it (watch the title update), Discard puts it back, and Delete asks first. Nothing sticks until you press an intent.
The doctrine
- No live commits. Every change stages locally; nothing persists until an intent button is pressed.
- Read mode is write mode. Fields are always editable; the commit action gates the intent, so there is no mode toggle or per-field pencil.
- Intents scale. The same bar carries Save / Discard / Delete now and a workflow's Stage / Submit / Approve later — a different intents array, not a new pattern.
- No danger zone. A destructive action is just a critical-toned intent that fires a ConfirmDialog, not a separate region.
Materiality
A flat, solid-color bar pinned to the bottom; status left, intents right. All tokens. No glass here — the frosted strip is the proposed system's treatment; official surfaces are opaque.
- sticky
- By default
stickyto the bottom of the surface — a solid, opaque bar with a top hairline so content scrolls beneath it (the frosted-glass treatment belongs to the proposed system). Set sticky=false to render inline (as in this demo). - staged dot
- A
--gold-aa-500dot + "staged changes" appears when dirty — the live/uncommitted signal. - intents
- Each intent maps to a Button register via tone (primary / secondary / critical / ghost); commit intents disable until there are staged changes.
Accessibility
- A labelled region. The bar is role=region with an aria-label, so it is a navigable landmark.
- Staged state is announced. The status is an aria-live region, so "staged changes" is read when edits begin.
- Real buttons, real disabled. Intents are Buttons; commit intents are disabled (not hidden) until dirty, and locked while pending.
API
- intents: ActionBarIntent[]
- Ordered { id, label, tone?, icon?, onPress, disabled? }. Primary commit last by convention; solid-fill tones (primary, critical) take an icon.
- dirty?: boolean
- Shows the staged-changes status. Gate commit intents yourself via each intent's disabled (e.g. !dirty).
- pending?: boolean
- A commit is in flight — locks every intent.
- sticky? / stagedLabel? / aria-label?
- sticky (default true) pins to the bottom; stagedLabel + aria-label are the translated strings.
Usage
import { ActionBar } from '@halo-compliance/ui'
import { Check, Trash2 } from 'lucide-react'
const dirty = form.state.isDirty
<ActionBar
dirty={dirty}
intents={[
{ id: 'discard', label: 'Discard', tone: 'ghost',
disabled: !dirty, onPress: form.reset },
{ id: 'delete', label: 'Delete', tone: 'critical',
icon: <Trash2 />, onPress: () => setConfirmOpen(true) },
{ id: 'save', label: 'Save changes', tone: 'primary',
icon: <Check />, disabled: !dirty, onPress: form.handleSubmit },
]}
/> // a workflow swaps in [Return, Stage, Submit, Approve]When to use
- Any committable surface. A record you edit, or a workflow step you advance — anywhere intent must be explicit.
- One bar per surface. It is the single commit point; don't scatter Save buttons through the form.
- Not for posting to a Feed. A comment is the Feed's own concern (Feed.Composer), not a record intent.
Anti-patterns
- Autosave. Persisting on each keystroke breaks the staged-until-intent contract — and surprises a compliance user.
- An Edit toggle. Read mode is write mode; a view→edit switch is the friction this doctrine removes.
- A bare destructive button. Delete is a critical intent + a ConfirmDialog; the staged model is the safeguard, but irreversible still confirms.