Focus stewardship
The doctrine
Every operator module has one steward input — the search bar the operator lives in. On boot the module registers its steward selector; from then on the Kernel takes responsibility for keeping focus there whenever a legitimate reason to move focus ends.
The steward is the safe sink for the keyboard — most importantly, for barcode scans. A misrouted scan (into a customer name, a cart quantity, a notes field) is worse than a missed scan, because the corruption is silent and shows up at reconciliation, not at the moment it happens. Focus stewardship makes that class of error impossible by construction.
| Shipped | v0.7.87 (Sales) → v0.7.91 (Inventory) — all five module tabs |
| Processes | BIKE.L2-0195 (steward focus recovery) · BIKE.L2-0196 (scanner keystroke interception) |
| Substrate | src/core/focus-steward.ts — loaded once by the shell, alongside the router |
How a module opts in
Each module calls the register API once inside its client IIFE:
window.kbRegisterSteward('sales', '#sales-search');
Stewards registered today, one per module tab:
| Module | Steward selector |
|---|---|
| Sales | #sales-search |
| Service | #sv-search |
| Customers | #customers-search |
| Purchases | #pur-search |
| Inventory | #inv-search |
| Settings | (no steward — not a search-driven surface) |
The Kernel maintains exactly one steward at a time — the steward for the currently-active module. Tab switches route through window.kbActiveModule(); modules that don't register (Settings) leave the steward unset and every recovery pathway silently no-ops.
Mechanism 1 — steward focus recovery (BIKE.L2-0195)
Four events trigger a refocus back to the active steward:
| Trigger | Detection |
|---|---|
| Modal close | MutationObserver watches every .kb-modal for its open class being removed |
| Print dismiss | window.addEventListener('afterprint', …) on the main document. Sales' popup-window print flow calls window.kbRefocusSteward() directly because afterprint fires on the popup, not the parent |
| Blur to nothing | focusout followed by focus landing on document.body (not a deliberate click into some other field) |
| Keyboard idle | 5 seconds of no keystroke, refocus back to the steward — reclaims the surface after a hands-off pause |
Every trigger runs through window.kbRefocusSteward(), which is safe to call liberally — it no-ops if:
- Any
.kb-modal.openis present (modals take precedence) - Another visible, typable element already has focus (
input/textarea/select/contenteditable,offsetParent !== null). If the operator put focus there, it stays there - The active module has no registered steward, or the steward's
offsetParent === null(its tab is hidden) - Focus is already on the steward
The guard on legitimately-focused other input is load-bearing: without it the 5-second idle timer would steal focus mid-edit from any other field on the page (the Adjust-tax reason input, an inline field in a modal, a notes textarea). That guard is why the doctrine can afford to be aggressive.
Mechanism 2 — scanner keystroke interception (BIKE.L2-0196)
A USB barcode scanner is a keyboard wedge: it types the whole barcode faster than a human can, ending in Enter. The Kernel watches for that shape at capture-phase — a burst of ≥5 characters with ≤50ms between keys, terminated by Enter — and redirects the whole burst into the active steward input regardless of where focus had drifted to.
To prevent false positives when the operator is legitimately typing fast, the interceptor consults an isUserTypingManually() heuristic: if the focused input's current value already ends with the buffered keys, the operator was typing into that field themselves, and the interceptor stays out.
What NOT to do
- Do not implement focus stewardship as per-module ad-hoc behaviour. The mechanism lives in one Kernel file; every module registers its steward and stops.
- Do not yank focus while a modal is open, or while another visible input has focus. The skip conditions exist for good reason.
- Do not add a UI toggle. It is always on. A cashier who could turn it off would forget to turn it back on, and one transaction with it off is one transaction at risk of a corrupted record.
- Do not extend it to surfaces that aren't search-driven (Settings). The register call is silent if the module doesn't opt in.
See also
- Three strata —
core/focus-steward.tssits in the Kernel - Keep-mounted SPA navigation — the router change that lets one steward per active module be the whole answer
- In-situ editing — the other UX principle that governs how the operator interacts with the till