helm-editable doctrine
Every inline-edit surface in the Hub uses the same three-state visual vocabulary, sharing one canonical CSS chassis. The dashed underline is the universal cue — the operator never has to learn a tab's local edit convention; the same affordance on Sales means the same thing on Service, on Customers, on Trade-in, and on the Pending PO bucket.
Pencil-icon purge. Three previously divergent chasses unified. One vocabulary, every till.
The three states
| State | Visual | Cursor |
|---|---|---|
| Rest | Plain text, no glyph, no underline | Default |
| Hover | Dashed blue underline + faint blue tint | Text-edit (text) |
| Editing | Solid blue border + 3px blue focus ring | Text caret |
That's the whole language. No pencil icon at any state. The underline carries the signal; the cursor confirms it on hover; the focus ring confirms commit-mode while typing.
The three chasses, unified
Before v0.6.332, three CSS classes carried three slightly different inline-edit looks:
| Class | Used on | Pre-v0.6.332 look |
|---|---|---|
.helm-editable | Customer card, Service slot, Trade-in form | Pencil glyph on hover (::after { content:" ✎" }), thin underline |
.helm-price-edit | Sales till line price cell | Excel-style cell border on hover, no glyph |
input.bucket-line-cost | Pending PO unit-cost field | Native <input> border, no glyph |
All three now render identically. The class names survive because the JS that wires the editor differs per surface (<span contenteditable> vs <input>), but the operator sees one thing.
Why no pencil
The pencil glyph was an accumulation of failure modes:
- It read as "is this clickable?" rather than "this is editable on click." Operators paused over the icon trying to figure out what it would do.
- Pencils on every editable span was visually noisy. A customer card with 12 editable fields looked like a craft store.
- It was redundant. Hover already revealed the dashed underline + text cursor — every input device the operator has (mouse, touch, stylus) communicates "I can edit here" without the glyph.
- It bred sibling icons.
✎,✏,✏️,📝— four different pencils accumulated across the codebase. Each one reinforced "this is editable" in a slightly different style. The cleanup removed all four.
The replacement rule:
- Editable cells → dashed underline (the doctrine)
- Edit actions that need a button (rename a synonym group, edit a ticket-details block) →
btn-smchassis with the text label "Edit"
What got purged
| Removed | Lived on | Replaced with |
|---|---|---|
.helm-editable:hover::after { content:" ✎" } (global CSS) | Every editable span | Nothing — the dashed underline is the cue |
td-edit-btn (Service slot ticket-details block) | Slot row buttons | btn-sm btn-edit with text "Edit" |
syn-edit-btn (synonym groups in Settings) | Synonym group cards | btn-sm with text "Edit" |
| ✏️ Cheque (Tender modal) | Tender method picker | 📄 (page) |
| 📝 Card-manual | Tender method picker | ⌨ (keyboard) |
| 📝 Check | Tender method picker | 📄 (page) |
When to use each surface
- Inline-editable value cell — a field whose primary affordance is editing. Customer Name, line price, PO unit cost, line qty. Apply the
.helm-editableclass (or one of its siblings); the dashed-underline cue is automatic; the click handler edits in place viacontenteditableor a native<input>. - Inline-affordance + modal save (composite tills, v0.6.383+) — see the next section.
- Action button — an action that's adjacent to a value or section. Rename a synonym group, customize a tile. Use the
btn-sm btn-edittext-label button — never an icon-only pencil.
The rule of thumb: if the operator's intent is "I want to change this number / word in place," use inline edit. If it's "I want to open the editor for this section," use a labelled button. Pencils blur that distinction; the v0.6.332 cleanup re-draws it.
The composite-till mode — dashed-underline cue, modal save (v0.6.383 → v0.6.386)
Some tills carry composite forms: Service Slot ticket details (11 fields), Trade-in intake (11 fields), Customer card (a dozen-plus fields). Inline-editing each field independently fragments the operator's mental model — they save one, get prompted on the next, lose track of what they've changed across the form.
For those surfaces, the doctrine evolved into a two-part pattern:
- The visual affordance stays. Every editable field on the till still carries
.helm-editablewith its dashed-underline + cursor:pointer styling. The operator reads the field as clickable, same as before. - The click handler routes to a per-till edit modal. Instead of
contenteditablechained per field, the click opens a dedicated modal (#slot-edit-modal,#customer-edit-modal,#tradein-edit-modal) focused on the clicked field viaopenXxxEditModal({focusField}). The operator sees all fields, types, tabs through them, and clicks Save once.
This compresses 60-line contenteditable chains down to ~10-line click-routers. Save reads each input, parses (regex-clean money → cents, year range-checks 1900–2100, etc.), writes back to the till state, repaints the inline spans via the existing setField + fmtMoney helpers, and fires refreshSummary + updateActionState — same downstream as the inline chain used to.
The visual signal to the operator is unchanged. The implementation underneath compresses a lot of duplicated contenteditable plumbing into one modal per till.
When to choose modal-save vs inline-save
| Choose inline-save | Choose modal-save |
|---|---|
| One value, one save | Composite form with cross-field validation |
| The operator's intent is "fix this number" | The operator's intent is "edit this record" |
| Save can complete without leaving the surface (e.g., line price recompute) | Save needs typed parsing (money / dates / year ranges) before commit |
| Examples: Sales till line price, PO unit cost, customer name | Examples: Service Slot ticket details, Trade-in intake, Customer card composite edit |
The dashed-underline is the same cue in both modes. The operator doesn't have to learn which surfaces are inline vs modal — the click takes them where they need to go.
What doesn't change
The class names (helm-editable, helm-price-edit, bucket-line-cost) survive because:
- The DOM wiring per surface differs —
contenteditablefor spans,<input>for fields with native validation, etc. - The save handlers per surface differ — Customer Name PUTs to
/api/customers/:id; ticket findings PUT to/api/service-tickets/:id; PO unit cost POSTs to the bucket endpoint. - Selector-level CSS overrides (e.g. price cells right-align) need a hook.
What the operator sees is unified. What the code does to make that happen is per-surface. That's a clean substrate boundary.
See also
- Header-icon doctrine — the other cross-tab visual discipline (icon-as-state on title bars + Charge-button full-colour discipline)
- No Walk-In on Service — companion enforcement doctrine; layered defenses for an invariant
- In-situ editing — the philosophy this chassis implements
- Tooltip protocol — the same kind of operator-language-first discipline, applied to scaffolding tooltips