Skip to main content

Work-area headers

The three working surfaces — Sales Till, Inventory Bucket, Service Slot — share a unified header pattern. This page records two iterations: the icon-driven doctrine that shipped in v0.6.5/.6 and was rolled back in v0.6.11, and the explicit-control pattern that replaced it.

Icon doctrine ROLLED BACK in v0.6.11

The previous version of this page documented an "icon-driven dual-action" doctrine where every working surface's header icon followed a rule (originally bright=active/dim=inactive, then inverted in v0.6.6 to bright=create/dim=edit). After a week of use, the doctrine was rolled back — the icons-as-actions abstraction kept asking operators a question the workflow didn't need them to answer. v0.6.12 introduced the current pattern documented below: explicit + Customer pill + Clear × button + named action buttons. This page is left here as a record of the model that was tried and didn't stick.

The current pattern (v0.6.12+)

Every working surface has the same header row:

PositionElementPurpose
LeftOperator-facing surface label (Sales, Service, Purchases) + · N items countWhat this surface is, how full it is
Middle+ Customer pill (Till + Slot) or + Supplier pill (Purchases, empty-only)Attach a customer or supplier to the working thing
RightClear × — always present, disabled-styled when emptyEmpty the surface with a helmConfirm prompt; audit-logged

Below the header sits the lines area (cart lines / bucket lines / ticket lines), then the footer action row with named action buttons.

Sales Till footer specifically (v0.6.16): Layaway · Special Order · Estimate · Charge. Four explicit named buttons — the operator never has to guess what each does. Charge is back at the bottom (v0.6.4 had removed it under the icon doctrine; v0.6.16 brought it back when the doctrine was rolled back).

Operator-facing names vs. doctrine names

The work-area surfaces have two layers of naming, and the layers are deliberately different:

LayerNamesWhere it appears
Doctrine namesTill · Slot · BucketChat, commit messages, Label-mode chips (when it existed), this bible, the CSS class names (.till-*), the element IDs (till-line-count, service-line-count, bucket-line-count)
Operator-facing namesSales · N items · Service · N items · Purchases · N itemsThe actual header label the cashier reads

The doctrine names are load-bearing: three different work-area mental models that all happen to share the .till-* CSS. Calling them Till / Slot / Bucket in chat lets a contributor say "the slot grows by 12px when the customer pill widens" without ambiguity. The operator-facing names are what the cashier already clicked to get there — repeating the tab name in the header collapses two name-jumps into one.

History of this split:

  • v0.6.12 added the unified header but kept the doctrine names visible (Till · N items).
  • v0.6.30 renamed the visible labels to match the tab names: Till → Sales, Slot → Service, Bucket → Purchase Orders.
  • v0.6.30.1 shortened Purchase OrdersPurchases to match the shorter operator-facing term Tom actually uses. The Products-tab nav-item label stayed "Products" since the tab IS the product catalog, not a list of purchases.

The fourth surface (v0.6.43) is the Customers screen in its detail view — when a customer is selected, the right-zone shows {Customer name} (DM Serif) + meta line + ✎ / 🗑 action icons in the same header anatomy. See slice 2.

Clear × evolution

The Clear × got iterated repeatedly because it's the surface most likely to absorb a "this doesn't quite read right" feedback note. The current settled treatment:

VersionChangeWhy
v0.6.12× ships, hidden when empty, visible-and-active when populatedFirst cut; clean look on empty till
v0.6.27× always rendered, disabled-styled when empty, active when populatedA fresh operator looking at an empty till couldn't see the affordance was even there
v0.6.27.1× bolder + bigger (font-size 20px, weight 800, slate-600, 1.5px border)Read as a real button, not a decoration. First use of the new fix-level bump (4th version segment) for cosmetic-only tweaks
v0.6.28.1Every other × in the app (~25 modals, customer-detach pills, generic aria-label="Close") bumped to the same bold treatmentOne bold × set + thin × set looked inconsistent. CSS selector matches id-ends-with "-cancel-x" + data-modal-close + aria-label="Close" so new × controls pick up the bold styling for free
v0.6.36.1× sized at 48px (was 20px), nearly filling the header height; light slate-300 treatment when emptyRead as a primary header control rather than a tucked-away afterthought
v0.6.37× reverted to 20px (48px proved too dominant); lighter-when-empty disabled treatment staysVisual settled here

Click still routes through the same helmConfirm dialog the original behaviour used — nothing wipes without an explicit operator press. The per-surface clear handlers do the right thing for each work area: bucket clear cancels every pending order-queue row; till clear releases cart holds + detaches the customer; slot clear wipes the loaded ticket / draft back to empty without touching the underlying ticket on the server.

What the rollback fixed

The icon-driven doctrine had a clear rule (bright = X, dim = Y) but it forced the operator to translate between visual state and available action on every click. Three concrete failure modes that the rollback addressed:

  1. The bright-vs-dim question added a cognitive step. Operators wanted to do the thing, not interpret the icon's state. Naming the button (Charge, Layaway, + Customer) removed the translation step.
  2. The doctrine couldn't carry secondary actions cleanly. Adding a Customer to the Till is a secondary action (the primary is ringing the sale). Putting it on an icon made it look co-equal with Charge, which it isn't.
  3. The doctrine inverted itself once. v0.6.6 flipped the color rule (bright=activebright=create) after James asked the inverse question. The inversion landed cleanly enough but it signaled that the concept — icons-as-state-machine — was the wrong abstraction. Explicit named controls don't have a "which way around" question.

Brand-mark slot (top-left)

The top-left slot of every operator screen lives on the same chrome strip as the work-area headers — it's the surface the customer's eye lands on first, so it earns its own discipline:

  • Default state: a compass-rose .brand-mark SVG (KVICK.BIKE wordmark beside it as .brand-text). Click → opens the "Does it do this?" feature-list modal (the per-section numbered list with build version stamped at the top).

  • Custom state (v0.6.55): when the operator drops a logo into Settings → Shop, the brand-mark slot swaps the default SVG for the uploaded image. The swap is live — the brand slot listens to helm:shop-logo-changed and repaints without reload.

  • Aspect-ratio adaptive layout (v0.6.55): the brand-text always sits somewhere alongside the logo so a customer recognizes which platform they're on. The position depends on the uploaded logo's aspect ratio, measured on image load:

    AspectLayoutWhy
    ≤ 1.5 (square / tall)Logo BESIDE KVICK.BIKE, same row, where the SVG used to beCompact, mirrors the original look
    > 1.5 (wide / long).brand-stacked column — logo ABOVE KVICK.BIKELets wide horizontal logos breathe; preserves wordmark legibility
  • Click delegation: the feature-list modal click handler is delegated onto the brand container element (v0.6.55), so either surface — the default SVG or the uploaded <img> — opens the modal. Earlier wiring (v0.4-era) bound directly to .brand-mark; the container-level delegation means an uploaded logo doesn't break the discoverability of the feature list.

The customer-facing branding is therefore substrate (the slot, the modal it opens, the wordmark) with surface drift per shop (the uploaded logo image). See Substrate Line for the underlying principle.

Title icon doctrine — final form (v0.6.116)

The page-title icons (the small image next to each tab's page heading — Till.png, Multitool.png, Customers.png, Products.png) settled into a unified rule after a multi-revision arc. The final rule, applied uniformly across all four working tabs:

Dim when the surface is idle. Bright when there's active work on it.

TabIconBright trigger
SalesTill.pngtill.length > 0 OR a customer is attached
ServiceMultitool.pngcurrentTicket loaded
CustomersCustomers.pngcustomer card open
ProductsProducts.pngbucket has staged lines

The dim baseline is opacity: 0.35 + filter: grayscale(85%); bright is no opacity / no grayscale. Hover tooltip describes the rule.

The arc that got here

The chassis thrashed on this rule across the v0.6 series — worth recording because the wrong abstractions kept proposing themselves.

VersionRuleWhy it lost
v0.6.5/.6Bright = create-new, dim = edit-existing (icon as state-machine for dual actions)Operators had to translate visual state into available action; tax on every click. See "the icon doctrine — rolled back" above.
v0.6.11Rolled back the dual-action icon entirely; icons became decorative-onlyLost the operator's "is there work on this surface?" peripheral-vision signal.
v0.6.110Service-icon inverted: dim when active so the Slot stays the focusCreated inconsistency with the other three tabs (they were all dim-when-idle). Operator scanning across the topnav couldn't tell at-a-glance which tabs had work.
v0.6.116Unified: dim when idle / bright when populated, across every tabStuck. One rule, four surfaces, peripheral-vision check works.

The cumulative learning: icons signal the surface's state, not the operator's intent. State is what the operator wants to read in a glance ("is there work on Sales? on Service?"); intent is what they want to express via named buttons (Charge, Save, Park). The doctrine puts the icons on the state side of that line.

Page-title icon evolution alongside the rule

The icons themselves also got larger and more consistent over the run:

  • v0.6.102 — Multitool icon added to Service title (mirrors Till.png on Sales — first cross-tab icon mirror).
  • v0.6.105 — Sales + Service title icons 2× bigger (26 px → 52 px). The previous size read as decorative; the new size makes the bright/dim state genuinely useful at peripheral-vision distance.
  • v0.6.113 — Customers icon (Customers.png) added on the Customers title.
  • v0.6.114 — Customers icon idle rules match Sales-Till exactly (so the doctrine started unifying before v0.6.116 settled it).
  • v0.6.115 — Products icon (Products.png) on the Products page title.
  • v0.6.116 — The unifying commit that locked the rule above.
  • v0.6.136 — Restore bucket Close pill + taller Customers icon (chrome polish).
  • v0.6.166 — Picked-up cropped Customers.png + auto cache-bust icons (so dropped-in shop icons take effect without a hard refresh).
  • v0.6.190 → v0.6.191 — Title icons removed from page headers pending a uniform-crop redo, then opted back in to the v0.6.166 cache-bust sweep. (The cycle was about getting the four icons consistently sized + cropped; current state is they're in and uniform.)

Why this page stays

Three reasons to leave the supersession story in the bible instead of just deleting:

  • Failed doctrines are evidence. When a future contributor proposes "what if we made the icons drive actions?" — this page is the short answer for why we tried and didn't keep it.
  • The Substrate Line was correctly applied. The icons are surface; the rule was code-side. The rollback was one CSS swap + a few JS changes. The fact that we could roll it back cheaply is itself a Doctrine validation.
  • The unified header survived the rollback. v0.6.12 kept the "same shape on every working surface" property, just with explicit controls instead of icon-state mapping. That property is still load-bearing — it's why the operator's muscle memory carries from Till to Bucket to Slot.

What's unchanged across both iterations

These properties survive both the icon doctrine and its rollback:

  • Same shape on every working surface. Till, Bucket, Slot all carry the same header anatomy. Cross-screen muscle memory works.
  • Customer attach is a header control. Whether icon or pill, the customer-attach control lives in the header, not the lines area.
  • Clear is the right-end safety affordance. Always the ×, always confirmed, always audited.
  • Composes with the Substrate Line. The header is surface — labels, colors, button text can drift per shop. The structure (label / +Customer / Clear / lines / actions) is code-side.

What this is not

  • Not in-situ value editing. The helm-editable chassis is about editing customizable values anywhere. The work-area header is about the primary controls on a working surface. They coexist.
  • Not a global toolbar. The header lives at the top of each working surface. Different surfaces have different action sets (Till has Charge; Slot has Generate Ticket; Bucket has supplier-specific Order Cards). The unification is structure, not contents.

See also