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 sticky to 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-500 dot + "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

A record's commit bartsx
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.
  • /record — the detail view this bar commits.
  • /button — the intent buttons and their tones.
  • /modal — the ConfirmDialog a destructive intent fires.
  • /feed — where a comment is committed instead (Feed.Composer, not the bar).