Skip to main content

No inline editing — Till is read-only, sections earn pills

The canonical edit pattern. The Till is a read-only aggregator; each section's header carries one Edit pill that opens a single-purpose popup for that section's fields. Save once, popup closes, the section repaints from the canonical store.

What's not usedcontenteditable, dashed-underline cues, click-to-edit value cells, hover-on-cell affordances, pencil icons
What is usedRead-only display in the Till; one Edit text-label button per section header; one focused popup per section
Where it shipsCustomers module v1.0.0; every future module inherits the pattern

What it looks like

Each section in the Till has a header row:

<div class="cd-sec">
<span>Contact</span>
<button type="button"
id="cd-contact-edit-btn"
class="btn btn-sm"
data-perm="customers.edit">Edit</button>
</div>
<div class="cd-row"><div class="k">Name</div><div class="v"><span></span></div></div>
<div class="cd-row"><div class="k">Phone</div><div class="v"><span></span></div></div>

  • The section label says what this section is (Contact, Family, Marketing, Notes, Equipment, Loyalty, Identity, …).
  • The Edit button is small (btn btn-sm), text-labelled "Edit", gated by data-perm (permissions).
  • The rows below display the section's values read-only — there's no hover affordance, no cursor change, no click handler on the values themselves.

Clicking the Edit pill opens a single-purpose popup: just this section's fields, a Save button, a Cancel button. Save writes to the canonical store via the module's API, closes the popup, and the section repaints. One save per edit interaction.

Why

  1. The Till stays scannable. An aggregator with twelve sections and no edit chrome reads as a clean record. Hover affordances, inline borders, and pencil glyphs accumulated visual debt across surfaces; the cleanup removed all of it.
  2. Save discipline is built in. Each popup is one section's worth of fields with one Save button. The operator can't accidentally leave a half-edited field hanging — they either save the section or cancel it.
  3. Permission gating is per-pill. data-perm="customers.edit" on the Edit button hides it (or disables it) for staff without the right. The read-only display below renders regardless — junior staff see the data; only people with edit rights see the pill.
  4. Audit captures the section, not the field. Each popup save is one recordMutation call with the section's before_json and after_json. The audit chain is one row per intentional edit, not one row per keystroke or blur.
  5. Modals carry validation context. Money fields can render with currency adornment; dates render with kbDatePicker; phone fields validate format; the bike-year field knows it's a year. A contenteditable <span> can't do any of that without growing JS to fight HTML.

How it composes

  • Permissions (core/perms-client.ts) — the Edit button checks data-perm against the cached actor permissions on render; the popup's Save also checks server-side via core/perms.ts.
  • Audit (core/audit.ts) — popup saves go through recordMutation(), lands in the hash-chained audit log.
  • Confirm dialogs (kbConfirm) — destructive actions inside a popup (Delete this bike, Remove this family member) use kbConfirm before the actual call.
  • Toast feedback (kbToast) — success / error pop bottom-right after Save.
  • Module API — each module's /api/{module}/... routes handle the section saves. Customers has PUT /api/customers/:id/contact, PUT /api/customers/:id/family-members/:m, etc.

What this replaced

A legacy .helm-editable chassis carried dashed-underline-then-contenteditable inline editing across the operator surface for several months. It was retired pre-v0.6.385 because:

  • Save semantics were per-field-on-blur — partial saves arrived in the chain in any order, and the audit chain filled with one-field rows that needed to be re-aggregated to reconstruct intent.
  • Hover affordances on every editable span read as visual noise. A customer card with a dozen editable fields looked like a craft store.
  • Validation couldn't live in HTML — money, dates, year-ranges, phone formats each grew their own per-cell JS to fight the contenteditable surface.

The dead .helm-editable CSS was finally swept from the codebase in v1.1.1; no element used it after the conversion.

Anti-patterns to avoid

  • A pencil icon ✎ next to a field, even as a hint. The Edit pill in the section header is the affordance; per-field icons re-introduce the noise the cleanup removed.
  • Hover affordances on display values. Read-only means read-only — no cursor change on hover, no underline, no tint.
  • Multiple Edit pills on one section (one per row). One pill per section, opening one popup with all the section's fields.
  • A contenteditable <span> anywhere on a Till surface. Editing always routes to a popup.
  • A modal-within-the-Till that edits one value. Use a popup; it's a focused single-purpose surface. The modal infrastructure (kb-modal*) is the popup's chassis.

See also