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 dashed-underline inline-edit chassis carried contenteditable in-place 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 inline-edit CSS was finally swept from the codebase in v1.1.1; no element used it after the conversion.

Tom overruled the earlier no-underline-links ban on 2026-07-21. The ban had been aimed at the retired inline-edit dotted-underline chassis — hover cues on every editable span reading as visual noise. In the modal-first world nothing edits inline anymore, so the ban's target no longer exists; the ban had over-shot into forbidding a real affordance the operator needed.

.kb-link (defined in core/ui.ts) is now the affordance for text that navigates or opens something: the customer name on a historic transaction, a transaction reference number, a linked account name, an entity title that leads somewhere. Visual: subtle black dashed underline — 1px thick, 2px offset, text-decoration-color: var(--text). Distinguishable from the retired dotted-blue inline-edit cue at a glance.

Replaces the "(click to edit X)" hint text app-wide — where a page previously spelled "(click to edit customer)" beside a name, the name itself now carries .kb-link. Fewer words on the surface, discoverable by hover, applied consistently across modules.

Not for buttons, pills, or footer actions. Those already have their own affordances (fill, border, accent-colour). .kb-link is exclusively for text-in-flow that would otherwise look like plain reading text but isn't.

The other retired-affordance bans still stand — no pencil icons, no trash icons, no ×-delete on records — see the anti-patterns below.

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 truly read-only display values. If a field can't be clicked to open anything, don't cue interactivity — no cursor change, no underline, no tint. (The .kb-link above is the exception: text that IS clickable gets the cue.)
  • 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​