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 used | contenteditable, dashed-underline cues, click-to-edit value cells, hover-on-cell affordances, pencil icons |
| What is used | Read-only display in the Till; one Edit text-label button per section header; one focused popup per section |
| Where it ships | Customers 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 bydata-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
- 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.
- 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.
- 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. - Audit captures the section, not the field. Each popup save is one
recordMutationcall with the section'sbefore_jsonandafter_json. The audit chain is one row per intentional edit, not one row per keystroke or blur. - 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. Acontenteditable <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-permagainst the cached actor permissions on render; the popup's Save also checks server-side viacore/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) usekbConfirmbefore 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 hasPUT /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
- In-situ editing — the broader philosophy this implements (edit where you are; the popup IS where the section is)
- Audit everything — every popup save lands in the chain
- Three strata — where
kbConfirm,kbToast, and the permission seam live - Customers module — the first module that ships the pattern end-to-end
- Header-icon doctrine — the work-area-header pattern this composes with