States

Empty, loading, error — handled the same way everywhere

The map — one pattern per situation, never mixed

  • Content is loading (first paint of a card/area) → placeholder lines, demoed below.
  • An action is running (save, delete, export) → the control runs it through g.action(btn, fn): spinner INSIDE the button (it replaces the icon, the label stays) + disabled, and a repeated click starts nothing — demoed below.
  • The action finished → a toast: g.success / g.error / g.warning.
  • Content failed to load → the in-page error state with Retry, demoed below — never a toast (it disappears) and never a blank area.
  • There is nothing to show → the empty state, with the create-CTA when the user can fix the emptiness.
  • Areas owned by a lib show that lib's built-ins: lister renders skeletons/empty/error itself, former disables its own submit — do not rebuild them.

Empty — icon, one line, the fixing action

No invoices yet

Invoices you issue will show up here.

  • Shape: centered, py-5, a muted 3rem icon (ri-inbox-2-line unless a more specific one fits), a short heading, ONE explaining line.
  • The CTA appears only when the user can act on the emptiness (create the first item); filtered-to-nothing results get "Clear filters" instead.
  • The icon is muted (text-body-secondary) — an empty state is calm, not a call for attention.

Error + retry — content that failed to load

Could not load the invoices

Something went wrong on our side. Try again in a moment.

  • Same calm shape as empty (this mirrors the lister built-in: ri-error-warning-line + text + Retry as .btn-sm .btn-secondary).
  • The message never surfaces raw error internals — a human sentence; details are in the log (g.fetch reports client errors automatically).
  • Retry re-runs the SAME load; while it runs, the button takes the spinner treatment (click the demo button).

Loading content — Bootstrap placeholders, glow

  • First paint of a custom async area = .placeholder-glow with .placeholder lines roughly matching the real content's shape.
  • Replace the WHOLE placeholder block with the real markup when data lands — never morph it piece by piece.
  • Lister areas skip this — the lib renders its own skeletons.

Action in flight — spinner in the button (live demo)

  • Every control that starts async work goes through g.action(btn, fn) (genus.md "g.action"): while fn runs the button is disabled and its icon swaps for .spinner-border.spinner-border-sm — the size and the label stay; a second click while it runs is a no-op (one request, ever). A g.confirm inside fn and an awaited reload keep the button busy until they settle.
  • The release is automatic when fn settles — a failed request never leaves a dead button; a page-leaving action passes navigate: true to stay busy until the page unloads.
  • On completion the result is a toast; the button never turns green/red itself.
  • In the dev environments a click that starts a fetch or a dialog WITHOUT g.action logs a console.error (the battery and the shot tool fail on it).