UI kit
Every component in every variant and state, with its accessibility contract. Switch Night shift in the account menu to see the night register.
On this page
Foundation
Colour
Paper for work, navy for presence, night for the control room. One action colour (accent). LEDs are device truth: never re-tinted, never decoration.
- Call
var(--sheet) · var(--text-2) · var(--accent) · var(--led-amber)- Keyboard
- Not focusable.
- Name
- Text meets 4.5:1 on its surface; lines that carry information (cells, bars) meet 3:1.
- Live region
- None.
- Target
- Not a target.
Surfaces
Text and lines
Action and focus
Device truth (LEDs)
Chart fills (3:1 on paper)
Tone
Meaning is an attribute: data-tone="ok|warn|bad|info|idle" on any tone-aware component. The tones layer maps it to --tone-fg/bg/line/surface/dot/fill; each component sets the idle quintet on itself, so a child never inherits its parent's tone.
- Call
st('warn', 'Needs review') · badge('Late', { tone: 'bad' })- Keyboard
- Not focusable.
- Name
- Tone is never colour alone: a word, a square or a shape always carries it.
- Live region
- None.
- Target
- Not a target.
data-tone="ok"
data-tone="warn"
data-tone="bad"
data-tone="info"
data-tone="idle"
Type
Jost for identity and big numbers, Inter for everything read, mono for identifiers. Display 44 (clamped), page 34, title 24, section 19, lead 17, body 15, data 14, small 13, caption 12.
- Call
.display · .title · .lead · .small · .cap · .mono · num(v, { size })- Keyboard
- Not focusable.
- Name
- Headings are real headings; one h1 per page. Sizes scale with the station scale.
- Live region
- None.
- Target
- Not a target.
Ramp
Put in 14 reels
Choose working rack
Needs review · 2
Checks each bay: reel or no reel, against the record.
Body 15 · The reel goes back to SR-01 and the bay light turns off.
Small 13 · Recorded with your name.
Caption 12 · as of 14:32
R-2026-0042 · SR-01-T3-L14
Numbers (Jost, tabular)
Space and radius
A 4 px step. Radius by role: control r2 (6), card and inset r3 (8), sheet r4 (12), hero and station surface r5 (16).
- Call
var(--s1…s16) · var(--r-control|card|sheet|hero)- Keyboard
- Not focusable.
- Name
- Its text.
- Live region
- None.
- Target
- Not a target.
Scale
- Space
s1 4 · s2 8 · s3 12 · s4 16 · s5 20 · s6 24 · s7 28 · s8 32 · s10 40 · s12 48 · s16 64- Radius
r1 4 · r2 6 · r3 8 · r4 12 · r5 16 · pill- Row density
compact s2 s4 · default s3 s5 · roomy s4 s5- Shadow
shadow-1 resting · shadow-2 hover · shadow-3 floating layers
Size scales
One name set everywhere: xs sm md lg xl 2xl 3xl. Each scale is a token (--avatar-md, --icon-lg, --led-sm …), so [data-scale="station"] on a station frame steps type, controls, icons, avatars and LEDs up at once.
- Call
--h-sm 36 · --h-md 44 · --h-lg 56 · --h-xl 64 · station: 56 · 56 · 64 · 72 · --target-min · --target-primary- Keyboard
- Not focusable.
- Name
- Its text.
- Live region
- None.
- Target
- Controls ≥ 24 px everywhere; ≥ 44 px under pointer: coarse; on the station scale ≥ 56 px, primaries ≥ 64 px (Neo NFR-E2).
Control height
Icon
Avatar
Icon tile
LED
Station ladder (Neo NFR-E2, glove use): nothing pressed under 56 px, every primary 64, the thumb bar's primary 72
Station must-read type (Neo NFR-E3, 2 m): the one instruction ≥ 64 px on the 1280 × 800 kiosk; a handheld stays proportionate
--t-display: clamp(32px, 5.2cqi, 72px) and --t-step-lg follow the station frame's width: 64.9 px in a 1248 px kiosk pane, 72 on a wider wall screen, 32 on a 480 px handheld. The bay address a task leads with is larger still (bayAddress(bay, { size: 'hero' }), 88–168 px).
Motion
Durations and curves are named by purpose. Leaving is always faster than arriving. One authored moment per surface.
- Call
var(--m-enter) var(--e-enter)- Keyboard
- Not focusable.
- Name
- Reduced motion drops movement to 0 ms; Pause blinking (html[data-motion=paused]) stops every loop and draws a blink still, split corner to corner (lit / off). Colour and information stay.
- Live region
- None.
- Target
- Not a target.
Tokens
- Press · state
--m-press 90 · --m-state 160- Enter · exit · travel
--m-enter 240 · --m-exit 160 · --m-travel 320- Settle · seam step · shake
--m-settle 480 · --m-seam-step 280 · --m-shake 320- LED (hardware, never tuned)
--m-led 500 · slow = 2 × LED- Curves
--e-standard · --e-enter · --e-exit · --e-travel · --e-settle · --e-progress
Blink, slow blink, paused
Layout
Pages compose with page, stack, row, grid, split and section. split children never overflow (min-width 0); the side column goes under the main one below 1180 px.
- Call
.stack--0…8 · .row--wrap|between|end|center|top|gap-N · .grid--2|3|4|auto · .split--left|side-sm|md|lg- Keyboard
- Not focusable.
- Name
- Reflows to 320 px and 200 % zoom without loss.
- Live region
- None.
- Target
- Not a target.
stack--3 · row--between · grid--3
Utilities
A small closed set allowed in page markup. No literal inline styles: style="" only carries data-driven custom properties set by a kit component.
- Keyboard
- Not focusable.
- Name
- .sr is the one visually-hidden utility.
- Live region
- None.
- Target
- Not a target.
Text
Spacing and measure
mt-0…8 · mb-0…8 · self-start · self-end · measure (66ch) · measure-sm (44ch) · h3 · lead · prose · title · displayShared states
State comes from ARIA and native state, defined once in the states layer: the sheet chip on a track (aria-current, aria-pressed, :checked), the selected ring on a chosen card, the pressed pill, disabled, busy and invalid.
- Keyboard
- Each control keeps its native keyboard model.
- Name
- Forced colours: selected chips use Highlight, selected cards get a Highlight outline.
- Live region
- None.
- Target
- Not a target.
Selected chip on a track
Selected ring
Pressed pill · disabled · invalid · busy
Primitives
Icon
Lucide outlines at 1.75 stroke from one sprite. Decorative next to words; named when it stands alone.
- Call
icon('scan-line', { size: 'sm' | 'lg' | 'xl' | 'hero', label? })- Keyboard
- Not focusable.
- Name
- aria-hidden by default; with label it is role=img with that name.
- Live region
- None.
- Target
- Not a target.
Sizes · decorative and named
Button
One primary per surface; footers put the secondary first, then exactly one primary or one danger. A link that looks like a button is still a link (href).
- Call
btn(label, { kind, size, icon, iconEnd, block, align, href, type, pressed, disabled, busyLabel, meta })- Keyboard
- Tab; Enter (links and buttons) or Space (buttons).
- Name
- Its words; an icon is decorative.
- Live region
- While a request runs the label changes (data-busy-label).
- Target
- 44 px (36 px sm; 44 px sm under coarse pointers).
Kinds
Sizes
States
Block, aligned, with meta
Icon button
A control shown as an icon only. It always has a name; a badge is decorative and the name carries the count.
- Call
iconBtn('bell', 'Alerts, 5 need attention', { size, badge, pressed, href, line })- Keyboard
- Tab; Enter or Space.
- Name
- aria-label, required.
- Live region
- None.
- Target
- 40 px (36 sm, 44 lg); 44 px under coarse pointers.
Status word
Square + word: the default way to state a state inline. Hollow means “not yet”; live breathes while a value updates.
- Call
st(tone, text, { size, hollow, live, wrap })- Keyboard
- Not focusable.
- Name
- The word is the meaning; the square is decoration.
- Live region
- None.
- Target
- Not a target.
Tones
Sizes and variants
Badge, chip, tag
Badge: a state on a ground (r2, 24). Chip: a count or filter value (pill). Tag: a code or language key (mono, r1).
- Call
badge(text, { tone, outline, plain, solid }) · chip(text, { tone, icon, href }) · tag(text, { size })- Keyboard
- Not focusable.
- Name
- Their words.
- Live region
- None.
- Target
- A chip with href is ≥ 24 px tall.
Badge
Chip
Tag
Identifier, number, key
Identifiers never break inside. Numbers are Jost with tabular figures; the unit is smaller and quieter.
- Call
ident(id, { size: 'sm' | 'md' | 'lg' | 'hero' }) · num(v, { unit, size }) · kbd(k) · mono(text) · fmtInt(n) → '48 380' (server twin of kit.js fmtInt)- Keyboard
- Not focusable.
- Name
- Its text.
- Live region
- None.
- Target
- Not a target.
Identifiers
Numbers and keys
Stat
The big-number pattern: label, value with unit, one line of provenance. Meters are rows of stats.
- Call
stat({ value, unit, label, foot, size, tone, kind: 'number' | 'text' })- Keyboard
- Not focusable.
- Name
- Read in order: label, value, foot.
- Live region
- None.
- Target
- Not a target.
A row of numbers with word values (kind: text)
Sizes and tone
Avatar and person
Initials in a circle; system actors are navy with an icon. A person is avatar + name (+ role), the one recipe wherever a person appears in a row.
- Call
avatar({ ini | icon }, { size, sys, ghost, idle, label }) · person(p, { idle, wrap, role, size, as, href }) · avatarStack(people, { max, label })- Keyboard
- as: 'button' or 'link' makes it a control.
- Name
- The avatar is decorative next to the name; a stack is one image named by its label.
- Live region
- None.
- Target
- A person button is ≥ 24 px.
Avatars
Person
Stack
LED
Device truth: the colour a bay or tower lamp shows. Blink is 500 ms, slow is 1 s, both 1:1. Paused or reduced motion turns a blink into a dashed ring.
- Call
led(color, { mode: 'steady' | 'blink' | 'slow' | 'off' | 'unknown', size, label, explain })- Keyboard
- With explain: Tab to it; Enter or Space opens the hint; Esc closes it.
- Name
- Decorative beside words; label names it; explain makes it a named button.
- Live region
- None.
- Target
- The explain button is ≥ 24 px (44 on station scale).
Colours
Modes and sizes
Explains itself
Icon tile
An icon on its tone ground: dialog icons, route lists, empty states. Square (r3) or round.
- Call
iconTile(icon, { size: 'sm' | 'md' | 'lg' | 'xl', tone, round, label })- Keyboard
- Not focusable.
- Name
- Decorative unless label is given.
- Live region
- None.
- Target
- Not a target.
Sizes and tones
Rank, source, freshness, as-of
Rank is an ordinal, never a severity. Source says where a value came from. Freshness and as-of say how current it is.
- Call
rank(n) · src(kind, text) · fresh('ok' | 'stale' | 'failed', text) · asof(text) · sr(text)- Keyboard
- Not focusable.
- Name
- Words carry each mark; sr() adds words only screen readers hear.
- Live region
- None.
- Target
- Not a target.
Marks
Permit
Custody under a permit: who may work on what, until when. An accent ring, never a status tone (a permit is not good or bad).
- Call
permit(words, { icon?: 'key-round' | false })- Keyboard
- Not focusable.
- Name
- Its words; the key icon is decorative.
- Live region
- None.
- Target
- Not a target.
Permit
Release mark
What ships in Neo R1 and what is parked (the owner's decision, 2026-09-28). later marks a parked surface: a solid ink pill with its clock, "Later · not in R1", in the rail's Later group, the Preview control's Later list, a Later note and the atlas. r1 is the quiet outline a list uses when it marks both. Metadata, never a tone: nothing about it is good or bad.
- Call
release('later' | 'r1', { size: 'sm' | 'md' })- Keyboard
- Not focusable.
- Name
- Its words ("Later · not in R1", "R1"); the clock is decorative.
- Live region
- None.
- Target
- Not a target: it labels one.
Marks
Meta, cite, toned icon
meta: a small icon + words in a row's meta line. cite: an Activity entry id with a copy-link button (shown when script runs). A toned icon takes the tone's foreground; the words still say it.
- Call
meta(icon, text) · cite(ev, { href }) · icon(name, { tone })- Keyboard
- cite: the copy button is a 36 px icon button (44 on touch).
- Name
- meta: its words; cite: "Copy link to EV-…"; toned icons are decorative.
- Live region
- Copy says "Link to EV-… copied." as a toast.
- Target
- Not a target.
Marks
Battery, problem link, phone primary
battery: a glyph filled to the level with the number (under 20 % bad, under 40 % warn). problemLink: status · id · why as one wrapping link. btn({ primaryOn: "phone" }): the primary look only under 560 px. chip({ wrap }): a chip whose words wrap. iconTile({ ping }): one radar pulse (stops with motion).
- Call
battery(pct, { label, hideNumber }) · problemLink({ tone, status, id, text, href }) · btn(label, { primaryOn: 'phone' }) · chip(text, { wrap }) · iconTile(icon, { ping })- Keyboard
- Not focusable.
- Name
- battery: role=img "Battery 45 %"; the link reads status, id and why in order.
- Live region
- None.
- Target
- Links in text; buttons 44 px.
Station variants
btn({ wrap }): a long label wraps and the button grows (a station bar at 320 px). iconBtn({ variant: "overlay", round }): a control over a picture (the camera). person(p, { size: "tile", as }): a big avatar over the name (the sign-in people grid; aria-current marks the chosen one). person(p, { as: "button", chip: true }): the person's menu button in a station head, a ringed pill with a chevron that folds to its avatar on a phone.
- Call
btn(label, { wrap }) · iconBtn(icon, label, { variant: 'overlay', round, size }) · person(p, { size: 'tile', as: 'link' | 'button', href, role }) · person(p, { as: 'button', size: 'md', chip: true })- Keyboard
- Not focusable.
- Name
- Their words; icon buttons are named by label.
- Live region
- None.
- Target
- 44 px and up.
Variants
Person chip (station head)
Person tiles
Forms
Field
Label, control, then the error (server) and the hint. A client error shows only after the person has interacted (:user-invalid).
- Call
field({ label, id, control, hint, error, clientError, optional, size })- Keyboard
- The control’s own.
- Name
- The label (for); hint and error are its description (aria-describedby).
- Live region
- None: errors arrive with the page or the swapped form.
- Target
- Controls are 44 px.
Resting, optional, server error, client check
Shown on the rack label and in every list.
More than the reel holds (5 000 pcs).
Pieces on the reel now.
Like SR-01-T3-L14.
That is not a bay address.
Input, select, textarea
Sizes sm 36 · md 44 · lg 56 · xl 64 · code (one-time codes). Widths xs · sm · md · lg · full.
- Call
input({ name, type, size, mono, width, … }) · select({ name, options: [value, label, { disabled, lang }], value, size, width }) · textarea({ name, rows, code })- Keyboard
- Native.
- Name
- Always inside field() or given aria-label.
- Live region
- None.
- Target
- 36–64 px tall.
Sizes
Select, textarea, states
Input group
The search and scan field family: icon, input, unit, action. xl is the scan field on task pages.
- Call
inputGroup({ icon, input, unit, action, size, mono })- Keyboard
- Native; the action is its own tab stop.
- Name
- The input carries aria-label (or a field label).
- Live region
- None.
- Target
- 36–64 px.
Sizes and parts
Check, switch, fieldset
Checkbox and radio rows with their words; the one switch (a checkbox with role=switch) for settings that apply at once; fieldsets group them, and can require at least one box.
- Call
check({ label, name, type, hint }) · switchRow({ label, name, checked, hint }) · fieldset({ legend, hint, gate: { minChecked } }, body)- Keyboard
- Space toggles; arrows move between radios.
- Name
- The row’s words; the fieldset legend names the group.
- Live region
- None.
- Target
- ≥ 24 px rows (22 px boxes under coarse pointers).
Fieldset with a gate
Radios and switches
Option cards
One choice group of cards: radio or checkbox, with or without a drawn indicator, a tone (danger), meta, a why note, media, and a Then: line that shows only while chosen.
- Call
options({ name, legend, mediaAt: 'end' | 'top' | 'start', items: [{ value, title, desc, then, tone, meta, why, media, disabled, checked }], type, indicator, cols, size })- Keyboard
- Arrows move between radios; Space toggles checkboxes. The focus ring sits on the card.
- Name
- The legend names the group; each card is its input’s label.
- Live region
- None.
- Target
- The whole card.
Content that follows a choice
Words or a block that follow which radio is checked: a submit that says what the chosen answer does, a hint, or fields only one answer needs. Every case is rendered; the kit emits the :has() rules for this one choice set, so no script is involved and page CSS never lists values. A case key may name several values ('list|none').
- Call
byChoice({ scope: formOrDialogId, name, cases: { value: Part, 'v1|v2': Part }, fallback, as: 'span'|'div'|'p', cls })- Keyboard
- The radios' own; hidden cases are display:none, so their controls leave the tab order.
- Name
- A submit is named by the one visible case.
- Live region
- None: the change follows the person's own choice.
- Target
- The content's own.
A submit label, and fields for one answer (CSS only)
Segmented control
One track, one current chip. nav: links (aria-current) · radio: native radios · submit: name/value buttons (aria-pressed) · toggle: buttons that act at once.
- Call
seg({ label, items, mode: 'nav' | 'radio' | 'submit' | 'toggle', name, value, size, block })- Keyboard
- nav and submit: Tab between items. radio: one tab stop, arrows move.
- Name
- aria-label names the group; items by their words.
- Live region
- None.
- Target
- 36 px items (40 under coarse pointers; 28 sm; 44 lg).
Modes
Toggle group
Short square toggles on a track: the days a rule runs on. Each is a real checkbox (a radio without multi), so the form posts its values as they are; checked wears the one selected chip. A gate asks for at least one.
- Call
toggleGroup({ name, legend, legendHidden, hint, items: [{ value, label, checked, disabled, title }], multi = true, gate: { minChecked } })- Keyboard
- Tab to each toggle, Space toggles (native checkboxes).
- Name
- The legend names the group; each toggle its label (give a title with the full day name).
- Live region
- None.
- Target
- 44 × 40 px (44 tall on touch).
Runs on
Setting rows
Site and personal settings: the label in a 200 px column, the control (≤ 320 px, or the rest with wide) and its hint under it; ruled rows; one column under 700 px. Edited is a visible word, not a colour. The working rack as a form control is rackPick({ size: "field" }). Inputs and selects never grow past their column (max-width 100 %), so a long option cannot push a 320 px station sideways; the rack picker and the Preview button reach 44 px under a coarse pointer and on the station scale.
- Call
settingRows(rows, { label }) · settingRow({ label, labelFor, id, control, hint, wide, edited }) · rackPick({ rack, tone, size: 'bar' | 'field' })- Keyboard
- The control's own.
- Name
- labelFor makes the label a <label>; the hint is wired with aria-describedby.
- Live region
- None.
- Target
- The control's own (44 px).
Rows
The rack beeps when a reel is out longer than this.
Scans and bay lights on this station use it.
Filter chips and toolbar
A GET form of pills: the URL holds the state (a link to it lands on the same results) and htmx swaps the results on change. The toolbar lines up search, chips, selects and what the list is.
- Call
urlWith(base, { f: ['low', 'esd'] }) (arrays repeat a param) · chips({ name, label, bare, attrs, items: [{ value, name, checked, label, count }], value, multi, hx, keep, action, trailing }) · toolbar({ search, filters, selects, trailing })- Keyboard
- Arrows between single-choice chips; Space toggles multi-choice chips.
- Name
- The form is named by label; chips by their words and counts.
- Live region
- The count line in trailing is a role=status sibling, never the swapped region.
- Target
- 36 px (44 under coarse pointers).
Single and multi choice
Bare chips inside one toolbar form (independent on/off chips)
Toolbar
Reason, acknowledgement, typed phrase
The standard gates in a confirm: a reason (4–500, recorded with your name), a required acknowledgement, and a phrase typed by hand (paste blocked). The submit waits (data-gated) until they are met.
- Call
reasonField({ id, min, max }) · ackCheck({ label }) · phraseGate({ phrase, id })- Keyboard
- Native.
- Name
- Each has a label; the unmet reason is the gated button’s description (app.js).
- Live region
- None.
- Target
- 44 px.
One-button post
A single action that writes: a form that adds no box, posted by htmx. Without hx it swaps nothing (the answer is 204 and a toast); hx.target names the region the answer replaces.
- Call
postButton({ action, hx, fields, close }, button) · hiddenFields(record)- Keyboard
- The button’s.
- Name
- The button’s words.
- Live region
- The answer is a toast (one live region).
- Target
- The button’s.
Inline in a row
Suggest, toggle, file drop, counter
suggest: a quick-insert word that submits ("Add to reason"). toggle: a phone Details / Less pill; its :checked reveals row parts by CSS. fileDrop: a labelled drop zone over a real file input. textarea({ count }): a stable n / max line. reasonField({ placeholder }), ackCheck({ clientError }), inputGroup({ width }), options({ layout: "line" }) with bayStrip, OptionItem.pending.
- Call
suggest(label, { name, value }) · toggle({ label, labelOn, name, checked }) · fileDrop({ id, accept, title, hint, icon }) · textarea({ count, maxlength }) · ackCheck({ label, clientError, id }) · inputGroup({ width: 'xs'|'sm'|'md'|'lg' })- Keyboard
- Native: buttons, checkbox (Space), file input (Enter opens the picker).
- Name
- The toggle is a checkbox named by its visible words; the drop zone is the file input's label; its hint describes it.
- Live region
- The counter is not live (it would speak every key).
- Target
- 44 px on touch.
Suggest and toggle
File drop · counter · ack with client error
0 / 200
Tick this to continue.
Line options with a bay strip · a pending (drafted) choice
Overlays
Dialog
One dialog: a head (optional tone icon, h2 title, sub), a body that scrolls, a foot with the secondary first and one primary. A form dialog wraps head, body and foot in a display: contents form. open renders data-modal="open" (never the open attribute): app.js shows it modally on load and after a swap.
- Call
dialog({ id, title, sub, icon, tone, body, foot, kind, size: 'md' | 'wide' | 'station', close, form, open, clears })- Keyboard
- Opens from its invoker; focus moves in (autofocus or the first control), is trapped, and returns to the invoker. Esc closes; clicking the backdrop closes (closedby=any).
- Name
- aria-labelledby its h2 title.
- Live region
- None; a result shows as a toast or in the swapped region.
- Target
- The × is 40 px; foot buttons 44 px.
Open one
Drawer
The same dialog from the right edge (560 or 760 px): alerts, a record beside the list, a long editor. It travels in and leaves faster.
- Call
dialog({ kind: 'drawer', size: 'wide', media?, … }) — media: an entity lead (a person's avatar at lg) in place of the icon tile- Keyboard
- As the dialog. Always has a × button.
- Name
- aria-labelledby its title.
- Live region
- None.
- Target
- As the dialog.
Open one
Confirm
A decision with a consequence: tone icon, what it does (consequences), then the gates it needs (reason, typed phrase, acknowledgement). No × button; the safe choice has focus; the submit waits until the gates are met.
- Call
confirm({ id, tone, icon, title, sub, consequences, reason, ack, phrase, submit, cancel, form, clears, attrs, status, order: 'body-first' | 'consequences-first', returnFocus }) · dialog({ …, returnFocus: '#row-x .rowlink__a' }) for a server-opened dialog with no invoker- Keyboard
- Focus starts on Cancel. Esc cancels; the backdrop does not (closedby=closerequest).
- Name
- aria-labelledby its title.
- Live region
- The unmet gate is the submit’s description.
- Target
- 44 px.
Open one
Tip
A short hint (popover=hint) for a control that already has a name: icon-rail labels, what an LED means. Never the only place a meaning lives.
- Call
tip(id, text, { side, note }) + interestfor / popovertarget on the invoker- Keyboard
- Shows on hover or focus (interestfor), or opens from a button; Esc closes.
- Name
- role=tooltip; the invoker keeps its own name.
- Live region
- None.
- Target
- Not a target.
Hover or focus
Toast
Confirms what happened, on navy, bottom right, at most 3. ok and info leave after 4 s, warn after 7 s, bad stays until dismissed. A toast never replaces a receipt or a Then: line.
- Call
respond(ctx, { url, toast: { text, tone } }) · HX-Trigger {"toast": …} · the shell's one-time flash after a write that navigates (respond({ navigate }))- Keyboard
- The × dismisses; the toast region is a top-layer popover above dialogs.
- Name
- Its words.
- Live region
- One region: role=status, polite; bad uses role=alert.
- Target
- The × is 28 px.
Send one
Palette and scan sheet
The shell’s own overlays: the palette (⌘K, Ctrl K or “/”) finds records, actions and pages; a scanner burst anywhere opens the scan sheet with what the code is and the one next step.
- Call
Rendered by app/layout/shell.ts; opened by the omni field or app.js- Keyboard
- Palette: type, arrows move, Enter opens, Esc closes. Scan sheet: the primary has focus.
- Name
- The palette input is a combobox named “Scan or search”; the list is a listbox.
- Live region
- Results replace the listbox; the count of matches is its size.
- Target
- 46 px palette rows.
Open it
Developer overlay
The prototype's own tools, out of every page's layout: one floating button bottom-right opens one panel with what the page registers. Its Preview states (R1, then Later with each Neo source), Simulate (what the page registered with demoBar() or a station's plays, moved here by the script), View as (the people it is designed for, ?as=) and This page (purpose, release, first-viewport budget). A section with nothing in it is not drawn. The button stays fully visible above a bar the page docks at the bottom; inside a page's own dialog with a registered simulation it rides in that dialog. Never ported.
- Call
devOverlay({ page: { title, purpose?, release?, budget? }, preview?, people?, sims?, links?, station? }) — the shell renders it on every page; devSlot(body) registers a simulation; GET /__preview.json lists what every page registers- Keyboard
- AltShiftD opens and closes it anywhere, also over a page's dialog; Esc closes it and focus returns to the button; the current state's link has focus when it opens.
- Name
- The button: “Prototype tools”; the dialog: its title.
- Live region
- None.
- Target
- 44 px; 56 px on a station (NFR-E2), and every control in the panel follows the station scale there.
The page's own
The panel, as it opens on Put in
Preview stateReady
Later · not in R1
- Default quantity · "4 000 is right"Neo 0026 §14–16
Simulate
View as
This page
- Purpose
- Put reels in by evidence: scan, the bay lights, the sensor confirms.
- Release
- R1
- First viewport
- 1 primary · 1 notice · 3 blocks
Blocks
The structures pages are made of. Each is one function with semantic props; tone is data-tone, state is ARIA or native, and a variant is a prop. Pages never write these class names.
Sheet
The white surface: a hairline, 12 px radius and a resting shadow. A head, body and foot when it holds a titled group; a tone tints surface and border.
- API
sheet(body, { pad, tone, surface: 'raised'|'flat'|'sunk'|'hero', as, label }) · sheetHead(title, { meta, actions, level }) · sheetBody(body) · sheetFoot(actions)- Keyboard
- None of its own.
- Name
- A labelled div becomes role=region; a head names the group by its title.
- Live region
- None.
- Target
- Controls inside keep their own sizes.
pad sm · md · lg
surface raised · flat · sunk · hero
tone
head · body · foot
Controllers
CTRL-S01 to CTRL-S04 answer on Line 2. The next health read is at 14:36.
Inset
The 8 px sunk panel inside a sheet or dialog: before/after boxes, a step-up, a range, a code well.
- API
inset(body, { tone, pad: 'sm'|'md'|'lg' })- Keyboard
- None.
- Name
- Not a landmark; its heading or first line says what it holds.
- Live region
- None.
- Target
- —
Before
SR-01 blinks on any fault.
pad sm
pad lg
Section
A titled band of a page. The head carries a title, a meta line and actions; bands are separated by a hairline. Level 3 for sub-bands.
- API
section({ title, id, level: 2|3, meta, actions, flush, done }, body) — done: a finished section rests as its title and one receipt line (What shows when)- Keyboard
- None.
- Name
- aria-labelledby its title when it has an id (a named region).
- Live region
- None.
- Target
- Head actions are 36–44 px buttons.
Racks on Line 2
The section body.
Flush band (no block padding)
Used at the top of a sheet or drawer.
List, row and group
Every entity row family: fleet, candidates, inbox, returns, queue, results, settings rows. One row recipe with lead, title + sub, meta, aside and actions; the grid template (cols) is the only per-page variation. Under 640 px of its own width the meta wraps under the title and actions go to the end.
- API
list(rows, { label, as, cols, density, divided, surface: 'sheet'|'none', foot, busy, leadStack, align: 'center'|'start', stackBelow: 'sm'|'md' }) — under 640 px of its own width (480 with stackBelow 'sm') a cols list becomes text + actions, actions at the end · listRow({ lead, title, sub, meta, aside, actions, detail, open, more, form, tone, state, current, href, changed }) — at rest the sub clamps at 2 lines and the detail folds behind Details (see What shows when) · listRow.skeleton(n) · listGroup(label, { count, meta, tone, level })- Keyboard
- Tab reaches the row link, then the row's own controls.
- Name
- The list is named (aria-label); a whole-row link is named by the title; group heads are headings.
- Live region
- None; rows that change flash is-changed, and the page announces what changed.
- Target
- The whole row is the link target; row buttons are 36 px.
default rows (flex): lead icon tile · title + sub · meta · actions
Needs attention 2
- R-2026-0042 · PN-0603-10KPut in at SR-01-T3-L14, not seen by the bay sensor
- R-2026-0317 · PN-0402-1UReported missing at SR-02-T1-L03
Working normally 2
- R-2026-0419 · PN-0805-4K7Stored at SR-01-T2-L07
- R-2026-0420 · PN-0603-10KTaken out for WO-260714-07
cols: a content grid (the fleet row) · whole-row link · current · changed
- 1Paste line · position 1Online
- 2Line 2 · position 3Limited
- 3Being movedOffline
density compact · no surface · done state
- Scan the reel label
- Put it in SR-01-T3-L14
- Wait for the bay sensor
density roomy · a detail under the row: folded at rest (What shows when, rule 2)
- PN-0603-10K10 kΩ 0603 · 3 reels short for WO-260714-07
Details
leadStack · align start: a wide rack glyph lead goes above the text under 640 px; rows with tall facts stay top-aligned
- SR-02 · High-capacity rack BSMT Line 2 · position 2Limited
- SR-01 · Reel rack ASMT Line 2 · position 1Online
loading: listRow.skeleton(3) in a busy list
tones on rows (the words carry the state)
- A info row
- A ok row
- A warn row
- A bad row
- A idle row
Table
Tabular data in a named, focusable scroll region. Numbers align right in head and cells; sortable heads carry aria-sort and a link. Group rows are one th across the columns. stack turns rows into cards under 640 px, using each cell's data-label.
- API
table({ label, head: [{ label, align, sort, href, width, hideLabel, current }], body, foot, density: 'compact'|'dense', stack, stickyHead: true|'page', stickyFirst, stickyFoot, rowLinks, flat, caption, busy }) · tableGroup(label, colspan, { tone, meta, cells }) · sortHeader(col) · tableSkeleton(cols, rows)- Keyboard
- Tab stops on the scroll region (arrows scroll it), then on sort links and row controls.
- Name
- The region and the table carry the label (a hidden caption unless one is shown).
- Live region
- None; aria-busy while skeleton rows show.
- Target
- Sort links 24 px tall; row buttons 36 px.
sortable head · group rows · tone rows · foot · first column sticks
current column (aria-current on its head) · summary group rows (a cell per column) · sticky foot
| Right | Shift A | Shift B | Shift C |
|---|---|---|---|
| Floor | 3 of 3 | 3 of 3 | 2 of 3 |
| Put in | Yes | Yes | Yes |
| Decide a count | Yes | Yes | No |
| Setup | 0 of 2 | 1 of 2 | 0 of 2 |
| Change a rack | No | Yes | No |
| Rights per shift | 3 | 4 | 2 |
rowLinks on a stickyFirst table · a row picked by its checkbox · a done row (data-state)
| Pick | Order | Line | State |
|---|---|---|---|
| WO-260714-07 | SMT Line 2 | In progress | |
| WO-260714-05 | SMT Line 1 | Completed |
compact · stack (cards on a phone) · visible caption
| Reel | Order | Due |
|---|---|---|
| R-2026-0311 | WO-260714-07 | 15:10 |
| R-2026-0312 | WO-260714-09 | 16:40 |
dense · busy (skeleton rows)
| Reel | Place | Pcs |
|---|
stickyHead: the head sticks inside a height-limited region
| Time | What | Who |
|---|---|---|
| 14:31 | Reel R-2026-0420 stored at SR-01-T3-L14 | Márta Kovács |
| 14:30 | Reel R-2026-0421 stored at SR-01-T3-L15 | Márta Kovács |
| 14:29 | Reel R-2026-0422 stored at SR-01-T3-L16 | Márta Kovács |
| 14:28 | Reel R-2026-0423 stored at SR-01-T3-L17 | Márta Kovács |
| 14:27 | Reel R-2026-0424 stored at SR-01-T3-L18 | Márta Kovács |
| 14:26 | Reel R-2026-0425 stored at SR-01-T3-L19 | Márta Kovács |
Spec and facts
Key/value: spec is the dt/dd list (cols: key column + value; rows: ruled rows with values at the end). facts is label-over-value in a grid, with an optional second line and provenance.
- API
spec(rows: [k, v, { tone, mono, stale }?][], { layout: 'cols'|'rows', size, align, keyWidth, label }) · facts([{ k, v, sub, src, tone }], { cols, size: 'sm'|'md'|'lg' })- Keyboard
- None.
- Name
- dt names each dd; a labelled spec is a named group.
- Live region
- None; stale values dim and keep their "as of".
- Target
- —
cols (default)
- Reference
- EV-26091-0412
- By
- Márta Kovács
- Confirmed by
- Bay sensor T3-L14
- At
- 14:31:07
rows (ruled, values at the end) · tone
- Bay light
- Green, steady
- Sensor
- Not seen
- Fits
- Up to 15″ reels
- Tower
- Yellow
rows · sm · start · fixed key width · stale value
- Requires
- stock.adjust
- Checked
- 14:36 by the server
- Last read
- 14:02
facts md (3 columns) · sub · provenance
- Part Label
- PN-0603-10K10 kΩ 0603 resistor
- Quantity Counted
- 4 200 pcsof 5 000 on the label
- Where
- SR-01-T3-L14Reel rack A · tier 3
facts sm (4 columns) · lg (2 columns) · tone
- Order
- WO-260714-07
- Line
- SMT-2
- Due
- 15:10
- Short
- 3 reels
- Put in
- 42 reels
- Taken out
- 37 reels
Meters
The hairline-ruled strip of readings, each a stat with its key and provenance. The only metric pattern: never a KPI card mosaic.
- API
meters([{ label, value, unit, foot, tone, extra }], { size: 'sm'|'md'|'lg', cols, label })- Keyboard
- None.
- Name
- Each reading's key names it; a labelled strip is a group.
- Live region
- None; stale readings dim ([data-stale]).
- Target
- —
size sm (tallies)
Bar
One bar/track recipe: a fill; stacked segments (with the pattern language: unknown = grey hatch, diff = amber hatch, draft = not final yet, outline = waiting, short = red outline); placed ranges on a scale; ticks for need, minimum, now or today.
- API
bar({ value, max, tone, label, size: 'xs'|'sm'|'md'|'lg'|'xl', segments: [{ value, tone, kind, at }], marks: [{ at, label, tone }], width, role: 'img'|'progressbar'|'none' })- Keyboard
- None.
- Name
- role=img with the full sentence (label); role=progressbar for a running task; none when the text beside it says it all.
- Live region
- None.
- Target
- —
sizes xs · sm · md · lg · xl
tones (fills are 3:1 against paper)
segments: done · part · unknown · diff · draft · outline · short
placed ranges · marks with labels · progressbar · fixed width
Timeline
One timeline for the audit trail, the ledger and the shift lane: mono time, a pip on a line, then the body. A square pip is a step Janus decided, a round pip one a sensor confirmed, a ring one still open.
- API
timeline([{ t, datetime, title, sub, tone, pip: 'square'|'round'|'ring', who, action, current, state }, { group, meta }], { density: 'compact'|'default'|'ledger', label, stickyGroups, groupLevel })- Keyboard
- Tab reaches actions in the rows.
- Name
- An ordered list, named by label; group heads are headings.
- Live region
- None; the page announces new entries.
- Target
- Row actions 36 px.
default · tones · pips · group · current · archived
Today
- 14:31Reel stored at SR-01-T3-L14Bay sensor confirmedMárta Kovács
- 14:29Put-in startedR-2026-0042 · PN-0603-10K
- 14:12Sensor did not confirmSent to Review
Yesterday
- 16:02Tower rule changedReplaced by v14
- 15:40Controller CTRL-S02 offline
compact · ledger
- 14:31Stored
- 14:30Scanned
- 14:29Assigned bay
- 14:31:07Reel R-2026-0042 storedSR-01-T3-L14 · EV-26091-0412
- 14:30:52Reel label scannedSMT-2 station
- 14:12:40Count difference 1SR-02-T1-L03
Steps
One stepper: an ordered list with the current step aria-current="step" and a screen-reader word for done, needs attention and skipped. Horizontal bars, numbered, or a vertical path; horizontal steps keep only the current step's words when narrow.
- API
steps([{ label, sub, state: 'done'|'now'|'warn'|'todo'|'skip', href }], { label, orientation: 'h'|'v', numbered, compact, words, done })- Keyboard
- Tab reaches step links (a wizard path).
- Name
- The ol is named by label; state words are in the text.
- Live region
- None; the page moves focus to the new step title.
- Target
- Step links are the label text (≥ 24 px row).
horizontal · every state
- Scanthe reel label (done)
- Put inSR-01-T3-L14 (done)
- Confirmthe bay sensor
- Labelprint if asked (skipped)
- Close
numbered · warn
- Upload (done)
- Map columns2 columns unmatched (needs attention)
- Review
- Commit
compact · narrow (only the current step keeps its words)
- Read (done)
- Compare (done)
- Decide
- Close
vertical · numbered · links (a wizard path)
- Welcome (done)
- Name this siteLine 2 · Budapest (done)
- RacksConnect the first rack
- People
- PrintersLater (skipped)
vertical · pips
- Controller found (done)
- Address set (done)
- Bus checkBoard 3 did not answer (needs attention)
- Self-test
Disclosure, technical details, versions
Native details with one chevron. Plain rows, a compact line (technical details), a sheet card with an aside in the summary, or a ghost-button trigger. Versions: who changed a setting, and a way back that makes a new version.
- API
disclosure(summary, body, { open, variant: 'plain'|'sheet'|'button'|'compact', flush, aside, level, name }) · techDetails(rows, { summary, open }) · versions(rows: [{ version, when, by, note, current, changed, changes }], { summary, meta, goBack, seeChanges, inUse, open })- Keyboard
- Enter or Space on the summary toggles; the summary is one tab stop.
- Name
- The summary text names it; no heading or control inside a summary.
- Live region
- None (expanded state is native).
- Target
- Summaries are 36–52 px tall.
plain · open
What happens to the reel
It stays in SR-01 until someone takes it out for an order.
level 2 · flush
Advanced
Section-sized summary.
button (a ghost trigger)
techDetails
Technical details
- Reason
- Right missing
- Requires
- planning.material.view
- Checked
- 14:36 by the server
sheet · aside
Others on it2 people
Bence and Anna are in SR-02 now.
versions · go back · see changes
Versions · 3
- v14Blink only on blocking faultsEszter Szabó · today 09:12In useSee changes in v14
- v13Tower yellow for low stockBence Tóth · 22 SepSee changes in v13
- v12First rulesEszter Szabó · 2 SepSee changes in v12
Facets, then, status line
Facets: one physical fact and the places it shows, dot-separated. thenLine: the consequence before commit. statusLine: the "safe line", a square, a bold lead and a sentence; a live line is in the page (empty) before it speaks.
- API
facets(parts) · thenLine(text, { lead }) · leadLine(lead, text) · statusLine(tone, lead, text, { live })- Keyboard
- None.
- Name
- Plain text.
- Live region
- statusLine with live is role=status (polite), rendered even when empty.
- Target
- —
facets
Count difference 1Reported missingBay light red
thenLine · leadLine
Then: The reel goes back to SR-01 and the order is short 1 reel.
1 decision covers it: release 350 pcs of R-2026-0066 from quarantine.
statusLine · every tone
Safe to take. The bay sensor sees the reel.
Next: Scan the reel label.
Not yet. The bay sensor has not seen it.
Stop. This reel is quarantined.
Waiting for the scanner.
Callout and notice
Callout: an inline question or note with its actions (rack mismatch, ask for a right, locked until known, dispatch, supervisor deciding, draft note). Notice: a tinted surface with an icon, a 650 title and a body; spacing comes from the parent stack. Under 560 px its trailing actions drop under the words, aligned with them.
- API
callout({ tone, body, title, icon, actions, layout: 'row'|'stack', live, unknown }) · notice({ tone, icon | false, title, body, actions, size: 'sm'|'md'|'lg', role: 'status'|'alert', trailing, titleLevel })- Keyboard
- Tab reaches the actions.
- Name
- The title or first sentence says what it is; icons are decorative.
- Live region
- role=status or role=alert only when the notice arrives after load (never on a swapped container).
- Target
- Actions 36 px.
callout · unknown (dashed frame: no report yet)
Usage rate Unknown
No readings from SMT Line 1 counters since 13:48.
callout row (info) · stack (idle, the sunk ask-for-a-right) · warn with icon
This bay is on SR-02. Your working rack is SR-01.
You need the right to change bay lights
Eszter Szabó can give it to you.
Locked until known. The last write to CTRL-S02 has no answer yet.
notice · every tone · title + body + actions
Scanner: last read 14:31
Import finished
Out of date
Could not load reels
View only
size sm · no icon · size lg (a verdict)
Test passed
Context bar
The compact context line in a page head's actions: what you are looking at, what stays as it is, and at most one action ("Viewing SR-02 · working rack stays SR-01 · Make working rack"). Quieter than a notice: no tone, one line, 44 px.
- API
contextBar({ icon, text, action, live, id })- Keyboard
- Its one action is a normal button or link.
- Name
- Its words; the icon is decorative.
- Live region
- live: true when the line changes after a swap (the working rack switched).
- Target
- The action is a 36 px button inside the 44 px bar.
Block options
sideNav items can open a dialog (opens) or be listed but not pickable (disabled). actionbar({ attached }) sits flat on its sheet. codeBlock(text | Markup, { copied }) marks parts and says its own copy words. secret({ countdownLabel }) names its timer ("Hides in 28 s"). steps({ orientation: "v", collapse: "h" }) becomes a scrolling row on phones; steps({ keepLabels }) keeps every label in narrow containers. table({ stack }) cells take data-col="status|meta" roles on phones; tr[aria-current] wears the selected look.
- API
sideNav(label, [{ href | opens, disabled, … }], current) · actionbar({ attached }) · codeBlock(text | Markup, { copy, copied }) · secret({ countdown, countdownLabel }) · steps(items, { orientation: 'v', collapse: 'h', keepLabels }) · td[data-col]- Keyboard
- An opens item is a native invoker button; a disabled item is not focusable.
- Name
- Their words; the secret timer is named "Hides in".
- Live region
- None (the timer is not live).
- Target
- Nav rows 40 px (44 on touch).
sideNav · opens a dialog · disabled
steps · vertical, a row on phones · keepLabels
- Rack (done)
- Shape (done)
- Controller
- Wiring
- Check
- Draft (done)
- Approved (done)
- In use
- Retired
codeBlock with marked parts and its copy words
POST /sap/orders 422 E_MATNR unknown: PN-0603-10K
secret with its timer words · actionbar attached
Editor body
Action row
A 72 px row that is one link or button: an icon tile, a title, a sub line that wraps, a chevron (station "More options", the handheld lists). Stack them.
- API
actionRow({ icon, title, sub, href | type, name, value, trailing })- Keyboard
- One Tab stop per row (a link or a button).
- Name
- Title then sub.
- Live region
- None.
- Target
- 72 px rows.
Action tile
A glove-scale start tile that is one link or button: an icon, a big title, a sub line and an aside chip (the LED colour the flow lights). The station home's "Put in" and "Take out". 80 px tall, 88 px on the station scale; the title follows the step-title token.
- API
actionTile({ icon, title, sub, aside, href | type, name, value, id, attrs })- Keyboard
- One Tab stop (a link or a button).
- Name
- The title (aria-labelledby); the sub and the aside describe it (aria-describedby).
- Live region
- None.
- Target
- ≥ 80 px (88 on the station scale), full width.
Default scale
Station scale (data-scale="station")
Focus card
The one thing to do now, set above or beside the rest of a list: an accent ring, an optional eyebrow (status … act by), the title, a lead, a big figure (a number with its unit and label, or a figure of your own such as a bay address) with meta beside or under it, and one action on the right. Under 560 px of card the action drops under the words and fills the width. A tone rings it only when the card itself is the warning; the status word carries lateness.
- API
focusCard({ title, eyebrow, lead, figure: { value, unit, label } | Markup, meta, action, tone, level, id, attrs })- Keyboard
- Tab reaches the action (and any links in the lead).
- Name
- The title, a heading when level is given; the figure is text in reading order.
- Live region
- None; a page that repaints it announces the change itself.
- Target
- The action is the caller's button (xl on stations).
Number figure with an act-by bar · wide
Continue WO-260714-07
T2-L28 is lit blue at SR-01 · then Line 2 · feeder 14
18min
until feeder 14 runs out
Eyebrow and a bay figure · narrow, station scale
Continue WO-260714-07 · item 1
Take R-2026-0012 from
T2-L28
Tier 2 of 5 · right half · SR-01No figure · tone warn
All 3 reels at SR-01 are on your cart.
Next stop SR-03 · T2-L09 · item 7
Bullet and icon lists
bulletList: sentences, each led by an 8 px tone square (what needs attention, coverage gaps, what a decision closed). iconList: rules and guards, each led by an icon. One or two columns (one under 700 px); ruled adds hairlines above and below.
- API
bulletList(items, { tone, cols: 1|2, label, ruled }) · iconList([{ icon, text }], { cols: 1|2, label, muted })- Keyboard
- Not focusable (links inside keep their own).
- Name
- A real list; label names it when the heading above does not.
- Live region
- None.
- Target
- Not a target.
bulletList · warn · two columns · ruled
- Márta Kovács has no badge yet.
- Two people sign in with a passcode only.
- Shift C has no supervisor on Sunday.
- László Varga screens in Hungarian; his team uses English.
iconList · muted
- Nobody can remove the last Administrator.
- A person who leaves keeps their records; only sign-in stops.
- Every change is kept with who made it.
iconList · md · tones
- Always kept: records, audit trail, users.
- Can be cleared: stock and work data.
Empty and skeleton
Empty: an honest empty state with one way forward (inline for "none in this view"). Skeleton: a shimmer that stops when motion is reduced or paused; the label is plain text and announce() says when loading ends.
- API
empty({ icon, title, body, actions, size: 'inline'|'sm'|'md'|'lg', level, role }) · skeleton({ lines } | { w, h }, { label })- Keyboard
- Tab reaches the action.
- Name
- The title (a heading when level is given).
- Live region
- role=alert for "could not be loaded" only; skeletons are aria-busy.
- Target
- Actions 44 px.
md in a sheet
Nothing waiting
Reels that need a decision show here, oldest first.
sm · lg
No reel with that code
Check the label, or search by part.
No racks yet
inline · skeleton lines · block
None in this view.
Loading reels…
Later · not in R1
A parked surface says so first: what it is, why Neo R1 leaves it out (the Neo source, cited) and what R1 does instead, with the way there. Parked means kept, not deleted (the owner's decision, 2026-09-28). The shell renders it under the head of every parked page and parked Preview state (park(id, later) in the page's routes, later on a Preview state); a page renders it compact where an R1 page keeps a parked part. A note on the quiet surface with the ink release rule down its edge: out of scope, not wrong, so never a warn tone, a hatch or a dash. Links inside it are the one sanctioned way from an R1 page into a parked one (tests/parking.spec.ts).
- API
later({ what, why, source, instead, insteadHref, insteadLabel, size: 'page' | 'compact' })- Keyboard
- Its one link, if any.
- Name
- role=note, named "Later · not in R1"; the facts are a description list.
- Live region
- None: it is there on arrival.
- Target
- The link is inline in text.
page (the shell, under a parked page's head)
The station capture journal: scans, count marks and reports kept on the device while it is offline, sent later in order
- Not in R1
- R1 keeps no client-side queue or capture journal: a station without its connection stops and says so
- R1 instead
- The stop signal: “Not connected — nothing is being booked”, and every unanswered action reconciled by its intent key See the stop signal
compact (a parked part inside an R1 page)
While I’m away: hand your supervisor duties to someone until a time
- Not in R1
- Delegation is Later in Neo
- R1 instead
- Access roles with scoped grants, changed on Access control
Receipt
After a write: a stamp (square or icon + outcome word), an optional lead sentence, By / Confirmed by / Reference, everything it closed, actions and technical details. Nested in a sheet, inset or dialog it drops its shadow. With focus it takes focus after the write.
- API
receipt({ stamp: { tone, text, icon }, lead, rows, closed: [{ verb, text }], actions, details, focus, flat, changed, label })- Keyboard
- With focus: tabindex=-1, focused after the write (data-focus); Tab continues into its actions.
- Name
- The stamp word states the outcome; a labelled receipt is a named group.
- Live region
- None; focus moves to it (the stamp is read).
- Target
- Actions 36 px.
ok · icon · lead · closed · actions · details
Recorded
R-2026-0042 is in SR-01-T3-L14.
- By
- Márta Kovács
- Confirmed by
- Bay sensor T3-L14
- Reference
- EV-26091-0412
- At
- 14:31:07
- Closed count difference 1
- Off bay light T3-L14
- Stock +4 200 PN-0603-10K
Technical details
- Event
- reel.stored
- Controller
- CTRL-S01 · board 3
square stamps: warn · bad · info
Needs review
- By
- Bence Tóth
- Reason
- Sensor did not confirm in 8 s
Rejected
- By
- Eszter Szabó
- Reference
- EV-26091-0398
Outcome unknown
- Sent
- 14:30
- Last answer
- none yet
nested in a sheet: flat automatically
Request sent
Sent
- To
- Eszter Szabó
- At
- 14:36
Legend
One key for racks, bars and diagrams. Swatch specs follow the pattern language: dashed = Unknown, amber hatch = a Difference; a lit bay is its LED colour with a ring.
- API
legend([{ swatch: 'cell:occ'|'cell:target:green'|'seg:short'|'seg:bad-hatch'|'cover:<kind>'|'led:amber[:blink]'|'dash'|'hatch'|'tone:warn'|'pip:round'|'mark'|'due'|'band', label }], { label })- Keyboard
- None.
- Name
- A named list; swatches are decorative, the words carry the meaning.
- Live region
- None.
- Target
- —
cover keys · due · band · bad hatch
- Available
- Taken out
- Short
- Unknown
- Due
- Planned window
- Late
rack cells
- Empty
- Occupied
- Reserved
- Difference
- Fault
- Unknown
- Off
- Decided
- Lit (green)
- Found
bar segments
- Available
- Reserved
- Waiting for review
- Unknown
- Difference
- Projected
- Short
- Taken out
- Being put in
- On the way
- Quarantine
LEDs · tones · pips · marks
- Green
- Amber
- Red, blinking
- Recorded
- Needs review
- Decided
- Sensor-confirmed
- Need
- Unknown
- Difference
Action bar and changes
The action bar is the one recipe for drafts, bulk actions and stop bars: pinned to the bottom (it floats, so it carries the float shadow) or in the flow. changeList is the one review-of-changes presentation: ruled rows or a table, grouped by area; change() is one inline was → now for a table cell or a sentence.
- API
actionbar({ text, actions, tone: 'draft'|'danger'|'neutral', sticky, live, label, icon }) · changeList([{ label, from, to, group }], { layout: 'rows'|'table', label, head, words }) · change(from, to, { was, now })- Keyboard
- Tab reaches its buttons in order (secondary first, then the primary).
- Name
- A labelled bar is a named region; del/ins carry "was"/"now" for screen readers.
- Live region
- live: the text is role=status (a bulk count); keep the bar itself out of outerHTML swaps.
- Target
- Buttons 36–44 px.
draft (navy) · inline
neutral · live bulk count
danger · pinned inside its box (sticky)
The bar sticks to the bottom of the viewport while this box is on screen.
changeList rows · groups
Tower lights
- Blink when
was Any faultnow Blocking faults- Yellow means
was Low stocknow Low stock or a difference
Sound
- Quiet hours
was Nonenow 22:00–06:00
changeList table
| Setting | Now | After |
|---|---|---|
| Tower lights | ||
| Blink when | now Blocking faults | |
| Sound | ||
| Quiet hours | now 22:00–06:00 | |
change · inline, in a sentence and a cell
Tier 3 boards was 5, 6 now 10, 6 in the draft.
| Tier | Side A |
|---|---|
| T3 |
Code, files, secret, QR, check list
Data blocks: a mono well that scrolls (with Copy), an export file list with sizes and checksums, a value shown once with a visible time limit, a pairing code, and may/never lists.
- API
codeBlock(text, { max, copy, label, wrap, id }) · fileList([{ name, href, meta, sum, icon }], { label }) · secret({ value, state: 'masked'|'revealed', countdown, label, actions }) · qr(seed, { size, masked, label }) · checkList([{ ok, text }], { label, words })- Keyboard
- The code well is a tab stop (arrows scroll it); Copy is a button; file names are links.
- Name
- The well is a named region; the QR is role=img with its purpose; check marks read Yes/No.
- Live region
- Copy confirms with a toast; the secret's countdown ticks visibly (no live region).
- Target
- Copy and secret actions 36 px.
codeBlock · copy · max 6 lines
{
"event": "reel.stored",
"reel": "R-2026-0042",
"bay": "SR-01-T3-L14",
"by": "marta.kovacs",
"confirmedBy": "sensor:T3-L14",
"at": "2026-09-24T14:31:07+02:00"
}fileList
- reels-2026-09-24.csv
sha256 9f2c…41ab - manifest.json
secret masked · revealed with countdown
qr sm · md · masked · checkList
- Yes: Reads bay sensors
- Yes: Lights bays to guide
- No: Never changes stock by itself
Compositions
Page-level arrangements built from blocks. Their ratios and breakpoints are fixed here, so pages only choose a width.
Page head
The one page head: crumbs above the h1 on a page about one thing, the page's explanation beside the title (K6: an ⓘ that opens a popover, or with panel the page help panel, a drawer), a badge (a status, never a session fact: View only and Support session are the top bar's), one sub-line that states something true now (a count, a state, a scope), a meta row, and the page actions on the right. The h1 names the section or the thing and stays the same on every tab; page tabs follow it.
- API
PageHeader(title).explain(summary, body, { panel }).sub(text, { id }).crumbs(trail).meta([state, place] | part).tech(rows).badge(part).actions(part, attrs).tight().attrs(map).id(id).titleId(id).focus().level(n).render()- Keyboard
- Tab reaches crumbs, then the actions.
- Name
- The heading is the page name; the badge sits beside it, outside the heading.
- Live region
- None; after a write that replaces the page, focus() lands on the heading.
- Target
- Actions 44 px; crumbs 24 px.
full: crumbs · badge · sub · meta · actions (shown here at h2)
WO-260714-07 · Line 2 paste
About this work order
A work order only creates demand. Each line takes the stock there is now and closes the rest short.
3 lines short for the 15:10 start
tight · title and actions only
Inventory
What is where, and how sure.
K0 · Page frame
Every desk page: .page → the head → the body region. The head is always the frame's first child (the shell puts the section tabs and a Later note right after it), .page takes no stack classes, sections space themselves, and a write or a live update swaps the region below the head, never the frame.
- API
pageFrame({ head, width: 'default'|'narrow'|'md'|'wide', region, regionAttrs, body, id })- Keyboard
- Source order: head, tabs, body.
- Name
- The h1 in the head names the page.
- Live region
- The region carries data-live; a swap keeps the head (and focus) still.
- Target
- —
a frame with one section (h2 here)
Counts
2 open · 1 with a difference to post
Open
The region below the head.
K1 · Index page
A list of one kind of record: the head (a count or scope, and the one create or import primary, which opens a drawer), level-2 views, one row of filters (filterBar(): search, one row of chips, the rest behind More filters, the count), then the results, open rows before final ones. The filter bar is one GET form: a chip or a field swaps the results and keeps the URL. On a phone the chips and More filters fold behind one Filters button, so the bar stays one row.
- API
indexPage({ head, views, filters: filterBar({ id, label, action, hx, keep, search, chips, more, count, trailing, on }), body, region })- Keyboard
- Search, then the chips (arrow keys within the radio group), More filters (a popover: Escape returns), then the results.
- Name
- The bar is a search landmark named by its label; the chips are a named group.
- Live region
- The results region swaps (hx-push-url); the count is an output.
- Target
- Chips 36 px (44 under a coarse pointer).
Demand, the work-order lens (h2 here)
Demand
4 open · 1 late
| Demand | To | Due | State |
|---|---|---|---|
| Open · 2 | |||
| WO-260714-07 | SMT Line 2 | 11:30 | In progress |
| WO-260714-09 | SMT Line 1 | 16:00 | In progress |
K2 · Queue page
Work that needs a person, ranked by what they act on next, then severity, then scope: the head (the queue in one line), the lenses, ranked groups of two-line rows (a row opens its drawer; its action is secondary unless the row is the focus), then the done list folded. A healthy group compresses to one counted line.
- API
queuePage({ head, lenses, groups, done: { summary, body }, drawers, region })- Keyboard
- Rows are links to their drawer; the done list is a summary: Enter or Space.
- Name
- Each group is a heading; the done list is named by its summary.
- Live region
- The groups region re-renders as the record changes.
- Target
- Row actions 36 px.
a queue with one group and its done list (h2 here)
Review queue
3 open · 1 yours
Yours · 1
- 1WO-260714-09 is 120 pcs short of PN-SMAJ24ALine 3 · closed short at allocationHigh
Resolved today · 4
The day's decisions, newest first.
K3 · Record page
One thing: crumbs back to its index, an h1 of its id and name, .meta([state, place]) with ids in Technical details, one primary among the actions → one summary strip → its sections in working order → related (a pager, not a second queue) → "What happened" last, folded.
- API
recordPage({ head, summary, sections, related, history: { summary, body }, region })- Keyboard
- Crumbs, actions, then the sections in order; the history is a summary.
- Name
- The h1 names the thing.
- Live region
- The record region re-renders as it changes.
- Target
- —
a demand record (h2 here)
RO-0120 · Line 2 buffer
Technical details
- Created by
- Bence Tóth · 14:20
Replenishment order · SR-03SMT Line 2 · bufferAllocated
- Requested
- 1 reels
- Allocated
- 1 reels
- Confirmed
- 0 reels
- Short
- 0 reels
Where its reel waits
R-2026-0034 in SR-03-T1-L08 · ready at SR-03
What happened
Show the history
- 14:20Created by Bence Tóth
K4 · Settings form
Titled groups of setting rows: a label, its control, one hint (an explanation goes behind the group's ⓘ, never a row's prose); edited rows are marked; one save for the whole draft (the draft bar). A person who may not change it sees the values as spec rows, not disabled controls. Policies is the reference.
- API
settingsPage({ head, groups: [{ id, title, explain: { summary, body }, rows }], draft, after })- Keyboard
- Rows in order; the draft bar last.
- Name
- Each group is a section heading; each control is labelled by its row.
- Live region
- The draft bar says the count of changes.
- Target
- Controls 44 px.
one group (h2 here)
Settings
Yours follow you to every device
Your preferences
Where your settings live
Yours follow you to every device. This station’s apply to everyone at it.
K5 · Stepped flow
Commissioning, recovery, the site transition, first start and imports: only the current step is expanded, later steps are one status line each in the stepper, done steps rest as receipts, and the step title takes focus when the step changes. At desk the stepper is a rail beside the step; on a phone a strip above it.
- API
steppedFlow({ label, steps: StepItem[], done, current: wizardStep(…), aside, orientation: 'v'|'h' })- Keyboard
- The stepper links the reachable steps; focus lands on the step title.
- Name
- The stepper is a nav named by its label; the step is a section named by its title.
- Live region
- None; focus moves.
- Target
- Step actions 44 px.
a vertical flow (h2 here)
Step 4 of 6
Move it to 192.168.0.25
Split
Main plus a side column (layout classes). The side is 300, 380 or 460 px; left puts it first. It stacks at 1180 px (right) or 900 px (left).
- API
split(main, side, { side: 'right'|'left', width: 'sm'|'md'|'lg', sideLabel })- Keyboard
- Reading order is the source order: main, then side (left: side first).
- Name
- sideLabel makes the side a named aside (only when it is complementary at page level).
- Live region
- None.
- Target
- —
right · md (380 px)
Main
The work: the list, the form, the drawing.
Side
Context and help.
left · sm (300 px)
Side first
Main
Workbench
The station two-pane task surface (bench, stage, dock): a large stage and a sunk side pane in one r5 frame. 7fr/5fr, a 340 px side, or half and half; the side can come first or hide while choosing. It stacks at 1100 px.
- API
workbench({ main, side, sideWidth: 'sm'|'md'|'lg', sidePosition: 'end'|'start', state, solo, label, sideLabel })- Keyboard
- Source order: stage, then side (start: side first). The stage title takes focus on each step.
- Name
- label names the bench; sideLabel makes the side a named region.
- Live region
- The stage keeps one live status line (statusLine live) outside the swapped pane.
- Target
- Station controls 56 px (station scale).
md (7fr/5fr) · stage + lens
Put it in T3-L14
Next: Wait for the bay sensor.
- Scan (done)
- Put in (done)
- Confirm
SR-01 · Reel rack A
- Bay light
- Green, steady
- Sensor
- Waiting
- Fits
- Up to 15″
sm side, first (a picks list beside the stage)
Stage
Picks
4 of 11 taken
solo (the side hides while choosing)
Choose an order
Hidden
Master–detail
A sticky master list beside the selected item (rule list, parts, access levels, stored data). 300 px list by default (240 or 340); it stacks at 900 px.
- API
masterDetail({ list, detail, label, listWidth: 'sm'|'md'|'lg' })- Keyboard
- Tab through the list (links), then into the detail.
- Name
- label names the pair; the list is usually a named sideNav with aria-current.
- Live region
- None; selecting loads the detail (a URL).
- Target
- List items 40 px.
Supplier A reels
v14 in useMatched 412 labels this week0 conflicts
- Part
- P/N: (\S+)
- Quantity
- QTY (\d+)
- Lot
- LOT (\w+)
Outcome
After a write: the sentence and the next steps beside the receipt, with one receipt width (360 px). It stacks at 900 px.
- API
outcome({ lead, actions, receipt })- Keyboard
- Focus lands on the receipt (focus) or the lead heading; then the next steps.
- Name
- The lead heading or sentence; the receipt stamp states the outcome.
- Live region
- None; focus moves.
- Target
- Next-step buttons 44 px.
R-2026-0042 is stored.
The bay light went off and the count difference is closed.
Then: The next reel for WO-260714-07 is R-2026-0043 at SR-02.
Recorded
- By
- Márta Kovács
- Confirmed by
- Bay sensor T3-L14
- Reference
- EV-26091-0412
- Closed count difference 1
Denied and lost
Denied: a missing right, who can give it, one request, technical details and places you can go (replaces the copied .denied/.dn-go). Lost: no page at this address, the address asked for, a search, the closest match and where you usually go.
- API
deniedPanel({ what, lead, who: { name, ini, role }, whoLabel, need, ask, places, placesLabel, details, level, status }) · lostPanel({ title, lead, address, search, closest, places, placesLabel, level })- Keyboard
- Tab reaches the request, the details summary, then the places.
- Name
- The title is a heading (h1 when the panel is the page); the places list is a named nav.
- Live region
- The request answers by swapping its button to "Request sent · 14:36" (focus stays on it).
- Target
- Request 44 px; places 56 px rows.
denied (under a page head: h2)
Material planning isn’t part of your access.
Signed in as Márta Kovács, Operator. Planners and supervisors have this page.
Technical details
- Reason
- Right missing
- Requires
- planning.material.view
- Address
- /planning/material
lost (h2 here; h1 when it is the page)
There’s no page at this address.
The link may be old or mistyped. Search for the reel, part or bay instead.
/inventory/reels/R-2026-00420Closest match: R-2026-0042 · PN-0603-10K at SR-01-T3-L14
Wizard step
One step of a guided flow (setup, provisioning, import): an eyebrow ("Step 2 of 6"), the title, a lead, the body and the step actions (primary next, ghost back). The title takes focus when the step changes.
- API
wizardStep({ eyebrow, title, lead, body, actions, level, wide, id })- Keyboard
- Focus moves to the step title on each step; Tab continues into the body and the actions.
- Name
- The section is named by its title (aria-labelledby).
- Live region
- None; focus moves (the title is read).
- Target
- Step actions 44 px (56 on station scale).
with a body and actions
Step 2 of 6
Name this site
People see this name on every screen and label.
You can change it later in Settings.
wide · a list body
Step 4 of 6
Connect the first rack
- CTRL-S01Found on 10.2.0.21Answering
- CTRL-S02Found on 10.2.0.22No shape yet
Step title
The station step heading of a bench pane (brand type, fluid size). It takes focus after a swap (tabindex=-1; give data-focus). PageHeader: .sub(text, { phone: 'hide' }) drops the sub under 560 px; .focus('load') focuses the h1 on a page a write navigated to. workbench({ minHeight: false }) lets the main pane take its content's height; outcome() splits by its own width (container query).
Scan the reel label
Put it in SR-01-T3-L14
Domain blocks
The concept's signature blocks. Meaning is an attribute: bays carry data-kind, lit things data-mode and an LED colour, steps data-state. Dashed means Unknown only; the amber hatch means a difference to decide only; a light hatch in the series' own colour means not final yet.
Rack glyph
A rack drawn as tiers × bays wherever a location is named. Every former per-page --cell override, regex rewrite and JS fit is a prop: size presets, one container-query fit formula, elevation, ruler, labels, per-bay attributes, marked vs lit, caution and settle. Towers lie in a strip along the top edge, at their side (a lamp capsule, never a column above the rack), and scale with the cells; at thumb a tower folds to one dot, the lit lamp.
- Call
rackGlyph(spec, { size, fit: { min, max, slots }, dim, variant, elevation, ruler, labels, cellAttrs, mark, caution, settle, pick, multi, label, decorative, quietTowers, blinkStyle, rack, id })- Keyboard
- A drawing is one image. With pick it is an ARIA grid: one tab stop, arrows move, Home/End go to the row ends, Enter or Space picks (rack:pick).
- Name
- role=img with the page's words; tower states, lit bays and the marked area are added unless the label already says them. Pick cells are named "SR-01-T3-L14, occupied"; tier labels are row headers. decorative hides it when the row text says it.
- Live region
- None. What changes is said by the page (sensor line, toast).
- Target
- Pick grids are for pointer precision at lg/elevation; every bay stays reachable by keyboard and by the "Go to bay" field.
- Green · blinking
- Red · slow blink
- Amber · steady
- Tower yellow · blinking
Rack legend
The key under a rack: the same cells, lamps and marks at 12 px, each with its word. Pass a key, or [key, words] to say it in the page's own words.
- Call
rackLegend(['occ', 'res', ['lit:green:blink', 'Put the reel in here'], 'stale', 'tower:ryg:green'])- Keyboard
- None (not interactive).
- Name
- A list named "Key"; swatches are hidden, the words carry the meaning.
- Live region
- None.
- Target
- n/a
- Empty
- Occupied
- Reserved
- Needs review
- Reel, no record
- Fault
- Unknown
- Not in service
- Decided
- Marked
- This bay
- Careful: neighbour bay
- Green · blinking — put the reel in here
- Blue · blinking — take this reel out
- Red · blinking
- Amber
- Marked area
- Locator
- Double-height tier
- Stencil slot
- Tower light
Rack notes
The rack glyph as the status picture of a station home. Each bay or tower lamp that means something carries a pin: the initials of the person it belongs to (the action ring when it is yours to act on), or an icon when nobody. The words (who is on it, since when, what to do) live behind the pin in a hint, and the light language behind a "?" under the drawing. Pins sit on their bay by CSS anchor positioning, so the drawing keeps one geometry; a band above the top tier holds the pins there and the towers' pins. Up to six bay pins and one per tower.
- Call
rackNotes(spec, { id, notes: [{ at: 'T2-L28' | 'tower:l' | 'tower:r', ini | icon, mine, label, words }], glyph: RackGlyphOpts, caption, legend: { label, body } })- Keyboard
- Each pin is a button in reading order (Tab); Enter or Space shows its words, Esc hides them. The "?" opens the legend the same way.
- Name
- A pin is named by its short label ("T2-L28, lit blue for you") and described by its words; the glyph keeps its own label naming every lit bay and tower.
- Live region
- None. The words are there when asked.
- Target
- 44 px, 56 px at the station scale (the pin is drawn at 26–36 px inside its target).
Take out R-2026-0012 · then SMT Line 2 · feeder 14.T5-L22 · reel missing since 12:52
Bence Tóth is on it.Left tower yellow · attention: T5-L22 reel missing.Right tower blinking blue · a take-out waits at this end.
- Blue blinking · take this reel out.
- Red blinking · a reel left without a take-out, or a fault.
- Yellow tower · attention on this rack.
Rack figure, card, strip and location
Compositions of the glyph. The figure puts a rack title and its place above it; the card is a to-scale thumbnail that links to the rack; the strip is one tier around the bay that matters, big enough to count; loc is the address lockup in a row.
- Call
rackFigure({ title, place, spec, label }) · rackCard({ title, place, tag, status, spec, href, nums, current, tone }) · rackStrip({ rack, bay, width, lit, occ, caution, title, meta, label }) · loc(address, glyph?)- Keyboard
- Card: one link (Tab, Enter). Figure, strip and lockup: not interactive.
- Name
- Card: its words (the glyph inside is decorative). Figure and strip: the glyph's label.
- Live region
- None.
- Target
- The whole card is the link target (≥ 44 px).
SR-01-T3-L14 · stored 14:20
Stack light
A tower drawn lying down: one capsule, the lamps it has left to right in their top-to-bottom order on the device (red, yellow, green, blue, white). The same glyph sits in a rack glyph's top strip. Blink is 500 ms, slow blink 1 s (device truth); with motion paused or reduced (or blinkStyle: 'split') a blinking lamp is drawn split corner to corner, lit / off. Unknown is the dashed grey capsule, never "off".
- Call
stackLight(lamps, on, { mode, size: 'xs'|'sm'|'md', label })- Keyboard
- None.
- Name
- role=img "Tower light: yellow, blinking" (or the page's words); label: false when the row text says it.
- Live region
- None.
- Target
- n/a
Covered-by bar
A quantity decomposed into its sources, in the order they can be used; the need is the tick. One colour encoding on every page: available ok solid · reserved accent · taken out grey · waiting for review accent outline · being put in a light ok hatch (not final yet) · on the way accent stripes · quarantine amber stripes · Unknown grey hatch · short red outline. The first source that closes the gap is outlined and its legend reads "covered" or "covers it".
- Call
coverBar('reserved:480:WO-07|unknown:310:SR-02:Troubleshoot>/rack.html', 600, 'PN-QFN32-MCU7 coverage', { dense }) · coverKey(kinds) · coverCell(kind | 'part' | null, { as: 'span' | 'button', size, label, expanded })- Keyboard
- The legend levers are links (Tab).
- Name
- The bar is one role=img with a full sentence ("480 Reserved, 310 Unknown; 600 needed").
- Live region
- None.
- Target
- Lever links sit in text.
- 1 800 Available · SR-01, 3 reels
- 480 Reserved · WO-260714-07
- 480 Reserved · WO-260714-07
- 310 Unknown · SR-02 tier 3 · Troubleshoot
- 600 Being put in · R-2026-0121 · Open in Review
- 350 On the way · from Store B
- 200 Available
- 350 Quarantine · bake needed
- 120 Waiting for review
- 230 Short
- 18 400 Available
- 4 000 Reserved
- 2 310 Taken out
- Available
- Reserved
- Taken out
- Waiting for review
- Being put in
- On the way
- Quarantine
- Unknown
- Short
- 480 Taken out · R-2026-0164
- 120 Closed short · at allocation
+ 1 reel · quantity unknown · piece fulfilment unproved
- 200 Available
- 350 Quarantine · bake needed
- 350 Short
Pick batch, demand summary
pickBatch(): the bays a station's pull lit together (Neo 0046 §4), one chip per task in walking order, the numbers the rack drawing's pins carry. Each chip: its lamp (device truth: blue blink), the reel and its line, the task's state as square + word, its own pick window as a drain, and while taken its Undo take-out with the undo window's countdown. On first appearance (ignite) the chips light in walking order, 40 ms apart; the lamps keep the hardware's one blink phase; a taken chip settles; an undone take-out relights one chip. Reduced motion: none of it, a cut. demandSummary(): any demand, from its source to its destination, requested / allocated / confirmed / short.
- Call
pickBatch([{ bay, reel, line, takes, state: 'lit' | 'taken' | 'verify' | 'lapsed' | 'dark' | 'cancelled' | 'attention', light, window: { ms, elapsed }, undo: { seconds, total }, note, relit }], { dest, rack, ignite, id, label }) · demandSummary({ source, from, dest, unit, requested, allocated, confirmed, short, unproved, state, id, label })- Keyboard
- Undo take-out is a button (Tab); nothing else in a chip is interactive.
- Name
- The batch is an ordered list named with its destination and how many are taken; each chip reads its number, bay, reel, line and state in order.
- Live region
- None: the station says each take-out once through its own live region.
- Target
- Undo take-out is a standard button (56 px on the station scale).
-
T4-L12
R-2026-0164 · line 4 · 480 pcs
Lit
Pick window 60 s -
T1-L25
R-2026-0145 · line 6 · 900 of 3 000 pcs
Lit
Pick window 60 s -
T2-L28
R-2026-0012 · line 1 · 4 000 of 4 820 pcs
Lit
Pick window 60 s
-
T4-L12
R-2026-0164 · line 4 · 480 pcs
Taken
21 s
-
T1-L25
R-2026-0145 · line 6 · 900 of 3 000 pcs
Window passed · still lit
Its pick window passed: it stays lit and keeps watching.
-
T2-L03
R-2026-0170 · line 1 · 1 reel
Taken · scan it
Scan it to verify.
-
T1-L20
R-2026-0103 · line 3 · 360 pcs
Waits · dark
-
T4-L12
R-2026-0164 · line 4 · 480 pcs
Needs attention · not lit
SR-01 is not reporting. If you already took the reel, keep it.
-
T1-L25
R-2026-0145 · line 6 · 900 of 3 000 pcs
Taken
Work order · MES · work-order importSMT Line 2
- Requested
- 13 900 pcs
- Allocated
- 7 780 pcs
- Confirmed
- 6 000 pcs
- Short
- 120 pcs
Work orderSMT Line 1
- Requested
- 4 000 pcs
- Allocated
- 360 pcs
- Confirmed
- 3 120 pcs
- Short
- 120 pcs
1 reel issued with an unknown quantity: they prove no pieces, and no deficit is invented for them.
Seam, seam row, twin
A movement is real only when the record and the floor meet. The seam shows its steps: square = Janus decides, round = a sensor (or scan) confirms; the last step closes once. The seam row says the same in one row of a list; the twin puts Record and Sensor side by side. Divergence never auto-resolves: it bends amber.
- Call
seam({ label, steps: [{ name, who, t, lane, state, led }], compact, caption, deadline, lanes }) · seamRow({ record, sensor, tone, keys, sensorId }) · twin(left, right, { open, framed, level })- Keyboard
- None (not interactive).
- Name
- The seam is an ordered list named by label; each step says its state ("Sensor, now, sensor-confirmed") and the current step has aria-current="step". The twin stamp names agree/disagree.
- Live region
- None in the component: the page announces the step that just closed (announce()).
- Target
- n/a
- Scanned, doneMárta Kovács14:20:03
- Recognised, doneR-2026-004214:20:03
- Bay lit, doneSR-01-T3-L14 green14:20:04
- Sensor, now, sensor-confirmedreel seen in the bay
- Stored, not yet
- System step
- Sensor-confirmed step
- Scanned, doneMárta Kovács14:20:03
- Recognised, doneR-2026-004214:20:03
- Bay lit, doneSR-01-T3-L14 green14:20:04
- Sensor, done, sensor-confirmedreel seen in the bay14:20:09
- Stored, doneRecorded14:20:09
- Scanned, doneMárta Kovács14:20:03
- Recognised, doneR-2026-004214:20:03
- Bay lit, doneSR-01-T3-L14 green14:20:04
- Sensor, needs review, sensor-confirmedreel seen in the bay
- Stored, not yet
- System step
- Sensor- or scan-confirmed step
- Scanned, doneMárta Kovács14:20:03
- Recognised, doneR-2026-004214:20:03
- Bay lit, nowSR-01-T3-L14 green
- Sensor, not yet, sensor-confirmed
- Stored, not yet
Step 3 of 5 · Bay lit · SR-01-T3-L14 green
- System step
- Sensor-confirmed step
- Scanned, doneMárta Kovács14:20:03
- Recognised, doneR-2026-004214:20:03
- Bay lit, doneSR-01-T3-L14 green14:20:04
- Sensor, needs review, sensor-confirmedreel seen in the bay
- Stored, not yet
Step 4 of 5 · Sensor · reel seen in the bay · needs review
- Scanned, done
- Recognised, done
- Bay lit, done
- Sensor, now, sensor-confirmed
- Stored, not yet
Waiting for the sensor · push the reel fully in
- Scanned, done
- Recognised, done
- Bay lit, failed
- Sensor, not yet, sensor-confirmed
- Stored, not yet
The bay did not light. Try again or report it.
- Scanned, doneR-2026-018714:31:02
- Put in by hand, done, sensor-confirmedMárta Kovács · I put it in T4-L0714:31:20
- Sensor, done, sensor-confirmedreel seen in T4-L0714:31:21
- Stored, doneRecorded · manual14:31:21
Record
R-2026-0042 · 10 kΩ 0603 · 5 000 pcs
Sensor
A reel in T3-L14 since 14:20
Record
R-2026-0088 · 2 150 pcs
Sensor
Empty since 12:52
Removal watch
An unexpected removal (B6; Neo 0026 §6–10, J05 §3.3): a bay went Clear with no task holding its reel. Open, it is red with the Missing delay draining to the minute Janus books it Missing; the reel pushed back into the same bay closes it with no movement. Booked, it rests as one line in the system's own voice (the square pip: Janus decides), Missing · R-… · By: Janus · 13:22, which opens its last bay, the delay and its entry. Under 560 px of card the open watch stacks, bay over words. Motion: the drain runs on the floor's clock; reduced motion keeps the bar, without the sweep. Station scale: the bay address takes the step size.
- Call
removalWatch({ reel, bay, since, at, delay, drain: { ms, elapsed } | null, booked: { at, ev }, id })- Keyboard
- None (not interactive): the page offers Found it or opens the review.
- Name
- Its words: the status word, the reel, the bay and the time; the drain is role=img, named in full (“Missing delay: 11 min left, until 14:51”): it counts down to a deadline, it is not a task’s progress.
- Live region
- None: the page announces a booking once (announce()).
- Target
- n/a
Missing R-2026-0088By: Janus · 13:22
- Last bay
- T5-L22 · dark
- Booked at
- the Missing delay, 30 min
- Reference
- EV-2461
Act-by, countdown, drain, clock
Time as the floor needs it. Act-by: when to act, with the lead-time zone before run-out hatched (not final yet) and a now marker; late turns the time red. The countdown ring drains over the real window and turns amber at a quarter left; the text form sits in a sentence. The drain is the linear bar. The clock is the site clock; app.js keeps every one ticking from one time authority.
- Call
actBy({ at, now, act, runsOut, late, label }) · countdown({ seconds, total, size, label, onZero }) · drain({ ms, elapsed, tone }) · clock({ size, time, date })- Keyboard
- None.
- Name
- Act-by bar: role=img with a sentence. Countdown: role=timer named by its purpose ("Sensor window"). Clock: role=timer "Current time".
- Live region
- Timers are not live (they would speak every second); the page announces the outcome at zero.
- Target
- n/a
Lit amber · off by itself in 30 s.
Scan parts
The presentational pieces every scan surface shares: the desk scan sheet, the kiosk, the handheld, the phone camera, the inbound and outbound benches. What a code is (resolveScan, canon) is not kit; these only draw the answer.
- Call
scanPrompt({ title, sub, size, layout, as, level: 1 | 2 | 3 }) · resolveCode(raw, canon) → { kind, code, rack, cell, r, p } (pure; app/scan.ts passes the site canon) · codeEntry({ id, action, error, folded, size, focus }) · bayAddress(bay, { size, led, as, where }) · sensorLine({ state, text, sub }) · entityHead({ id, sub, qty, status }) · reelSummary(reel) · scanSheet({ code, status, facts, says, rack, lit }) · scanLit({ text, seconds, off: { action, hx, fields }, live }) — at zero the countdown submits the Switch off form (#id-off)- Keyboard
- Code entry: Tab to the field, Enter submits; the fold is a native disclosure (Enter/Space). A button scan prompt is one button.
- Name
- The code field has a label ("Code"); its error is wired by aria-describedby and aria-invalid. The bay address is a heading when it is the screen's title.
- Live region
- sensorLine is one stable role=status; a code error is role=alert; the lit line is role=status.
- Target
- Fields and buttons 44 px (56, primaries 64–72, at station scale).
Scan a reel
or the supplier label on the bagScan the reel to put it in
Type or paste a code
T3-L14
T2-L28
T3-L14
SR-01 · tier 3, bay 14 from the leftT3-L14
- Where
- SR-01-T3-L14
- Part
- PN-0603-10K · 10 kΩ 0603
- Quantity
- 5 000 pcs
- Where
- SR-01-T3-L14
- Reel ID
- R-2026-0042 Label
- Part number
- PN-0603-10K Label
- Lot
- LOT-2614-042 Label
- Quantity
- 5000 Label
Station parts
The kiosk, handheld, phone camera, sign-in and setup wizard share one frame, keypad, set of PIN dots, result mark and bottom nav. The frame wears data-scale="station", so type, controls, icons, avatars and LEDs step up by token; no page restates a size.
- Call
faces([{ id, body, hidden }]) · kit.js crossfade(from, to) · [data-xfade-show="#id"] · kit.js ack(frame, 'ok' | 'warn') (the frame's edge flashes for a read) · kit.js seedKeypad(target, value, fresh) · kit.js identify(rackEl) (tiers light in turn, then two flashes; still under paused motion) · twin(l, r, { open, seamLabel: [agree, disagree] }) · keypad({ target, size, value, unit, steps, fn }) · pinDots(n, { filled, tone, shake }) · resultMark(tone, { size }) · bottomNav({ label, items }) · stationFrame({ head, body, bar, nav, presence })- Keyboard
- Keypad keys are buttons (Tab, Enter/Space) that type into their target (aria-controls): app.js wires every [data-keypad] (kit.js keypads()). Digits append up to maxlength, Clear empties, Delete removes the last digit, a step adds its number; the field then gets an input event (gates sync) and the value box updates. A page that owns a key cancels the opus:keypad event ({ key, step, target }). A hardware keyboard types into the field directly. Bottom nav: Tab between places.
- Name
- Delete is named "Delete last digit"; the value box is the target's label. PIN dots are hidden: a polite "3 of 6 digits" beside them says the count. The result mark is decorative beside its heading.
- Live region
- The frame carries one role=status for the station's announcements.
- Target
- Keys 64/72/80 px; nav items 64 px (72 at station scale).
SR-01-T3-L14
SMT Line 2 · Feeder 14
Put the reel in
T3-L14
SR-01 · tier 3, bay 14Thumb bar
The bar in a station's thumb zone, one order on every task screen: Back first, then what the floor is doing now (the sensor line with its live LED, the scanner's state, a retry line), then a More menu button and the one primary last. Any part may be empty. Under 720 px the status takes its own row above the buttons.
- Call
thumbBar({ back, status, end, attrs }) // in stationFrame({ bar })- Keyboard
- Its controls in reading order: Back, then the end buttons.
- Name
- The frame names the bar region ("Actions"); each control by its words.
- Live region
- The sensor line inside is not live (the frame's one status region speaks).
- Target
- Station controls: 64–72 px.
T3-L14
Stored.
Stop signal on a station, last actions
A station keeps no queue: while it is not connected nothing is booked. stationFrame({ stop }) puts the stop signal where the person acts: the kiosk's veil over its task pane (must-read size), the handheld's 56 px strip over its thumb bar. The thumb bar (and, under the veil, the pane) is [data-needs-live]: inert while not connected. lastActions() lists what this session sent and what became of it: Sending…, Recorded, Not recorded, Not booked (never sent), Outcome unknown (sent, no answer: asked about, never resent; it settles once, data-settled). Kit.js record() keeps each write's intent in the session before it is sent.
- Call
stationFrame({ stop: { form: 'veil' | 'strip', body, state, detail } }) · lastActions([{ label, at, state, reason, settled }], { id, label, empty }) · kit.js record(), intents(), settleIntents() · intent:change · intent:settled- Keyboard
- Nothing to press on the signal; the inert thumb bar takes no focus while not connected.
- Name
- The signal says "Not connected — nothing is being booked", then what the device does and "as of"; each last action its words, stamp and time.
- Live region
- None on the list: app.js says stopping and coming back once; a settled Outcome unknown is said once (station-kit.js).
- Target
- The strip is 56 px on the station scale; the veil words are must-read (64 px on the kiosk).
Scan the reel.
Put in · SR-01
Take out T2-L28
Last actions
- Take-out of R-2026-0012Outcome unknown14:31 · sent, no answer yet · asked again when Janus is back
- Scan · R-2026-0036Not booked14:32 · never sent
- Put-in of R-2026-0109 at T4-L31Recorded14:28 → 14:33
- Report · label X7Q-2231-BNot recorded14:27 · Janus never got it
Later · not in R1The station capture journal (Neo 0033 §7, 0058 §1): kiosk.html?state=journal and handheld.html?state=journal draw it. Nothing paints or sends from these.
Follow the light, manual bays
Commissioning qualifies a rack as a whole by follow-the-light insertion (Neo 0032 §14, J06 steps 10–11): Janus lights one sampled bay at a time, a named person puts a reel there, and the sensor must change at the lit bay and only there. followLight() draws each sampled bay as a two-pip seam, square (Janus lit it, blinking the bay's LED colour) then round (the sensor saw it): when the sensor answers the green line draws and the round pip settles closed, once, as the live row changes. A bay that did not follow leaves the seam broken amber. A board in manual operation (0033 §6, 0035 §4) has no sensor check: its round pip becomes the manual mark, the same solid inner border the rack glyph's spec key manual= puts on its bays (rackLegend key 'manual' says it: Manual · scan-confirmed). Reduced motion: the seam closes at once; a lit pip is drawn split, lit / off.
- Call
followLight({ label, boards: [{ title, samples: [{ bay, why, state: 'todo' | 'lit' | 'seen' | 'miss' | 'manual', by, at, occupied, seenAt, id }], note, id }], color, collapse: 'done', id }) · rackGlyph('…;manual=B-T3-L01..B-T3-L16') · rackLegend(['manual'])- Keyboard
- Nothing to press: a record of the rack answering.
- Name
- An ordered list per board, "Bays lit on Board 5 · side B, tier 1"; each row says its state first (Lit, Followed, Did not follow, Manual operation), then the bay and what the sensor said; who put the reel in is the avatar's name.
- Live region
- None inside: the page re-renders it from the live topic (commission:<controller>) without announcing; the progress figure above it carries the count.
- Target
- Rows are 36 px; nothing in them is pressed.
-
Board 5 · side B, tier 11 of 3 followed
- B-T1-L01first bay of the board Followed: Seen in B-T1-L01 · 14:31 MK
- B-T1-L08holds a reel Lit: Lit green · pull the reel and push it back in
- B-T1-L16last bay of the board Not lit yet: Not lit yet
-
Board 8 · side B, tier 4Did not follow
- B-T4-L01first bay of the board Did not follow: No change at B-T4-L01
- B-T4-L16last bay of the board Did not follow: No change at B-T4-L16 · seen in B-T4-L15 instead
Board 8 reads nothing on any channel. Its bays stay Unknown.
-
Board 7 · side B, tier 3Manual operation
- B-T3-L01 Manual operation: No sensor check · screen-guided, scan-confirmed
- B-T3-L16 Manual operation: No sensor check · screen-guided, scan-confirmed
- Manual · scan-confirmed
- Unknown
- Occupied
What shows when
The rules for what a screen shows first, and the components that make the resting rung the default.
Primary at rung 0secondary at 1–3tertiary at 4–5
Tiers and the disclosure ladder
Every fact is primary (what this person decides or does now), secondary (context that changes how they act) or tertiary (evidence and diagnostics). Primary is at rung 0; secondary at rungs 1–3; tertiary at rungs 4–5. Each rung has its kit component and the one trigger that brings it up; the components below make the resting rung the default, so a page has to ask to show more.
- Call
DESIGN.md › What shows when- Keyboard
- Per component.
- Name
- Per component.
- Live region
- None.
- Target
- Every fold is a 36 px summary (56 on the station scale).
| Rung | Where | Kit | Surfaces when |
|---|---|---|---|
| 0 | The first viewport, at rest | PageHeader (one sub-line), focusCard, bayAddress, st(), one primary btn | Always |
| 1 | Inline, in place | listRow({ current | open, detail }), seamRow, thenLine, callout | The row is selected, the item diverges, or it is at the step that needs it |
| 2 | In place, folded | disclosure (compact or sheet), listRow's Details, hint(), the done line of section · steps · followLight · removalWatch | A tap; a hint also by Settings › Hints |
| 3 | Beside the list | dialog({ kind: 'drawer' }) | The row is opened; the list stays |
| 4 | The record’s own page | review-unit, work-order, rack.html#troubleshoot | The person needs the whole case or its history |
| 5 | Technical details | techDetails, receipt({ details }), PageHeader.tech() | Asked for; never above rung 3 on an operator surface |
Rows at rest
A row rests at two lines: its title and one line of words, a state and one action. The sub is clamped at 2 lines until the row is current or holds focus. Its case (detail: a seam row, a cover bar, provenance) folds behind a Details line and shows in place only when the row is current (selected) or open (it diverges, or it is at the step that needs it). A row's own controls are its form, always shown; a detail that holds a control is treated as the form, and a detail that is already a fold is shown as it is.
- Call
listRow({ title, sub, meta, actions, detail, more, open, current, form, … })- Keyboard
- Tab reaches the row link, the row's controls, then its Details summary; Enter or Space opens it.
- Name
- Details is named by its words ("Details", or the row's `more`: "2 differences").
- Live region
- None.
- Target
- The Details summary is 36 px (44 under a coarse pointer, 56 on the station scale).
- SR-01-T5-L22Count difference 1 · open since 12:52. The count found the bay Clear where the record has a reel: decide with the reel in hand, or send someone to look at the bay before the Missing delay runs out.
Record and sensor
RecordR-2026-0088 · 2 150 pcsSensorClear since 12:52 · no task - SR-01-T5-L30Count difference 2 · reel, no record
Details
RecordEmptySensorReel since Fri 19 Sep 16:40
- SR-01-T5-L22Count difference 1 · open since 12:52RecordR-2026-0088 · 2 150 pcsSensorClear since 12:52 · no task
- PN-0603-10K3 reels short for WO-260714-07
- AppearanceBay light colours look the same in both.
Done collapses
Finished work rests as one receipt line that opens it; open work comes first. section({ done }): the title and the line. steps({ done }): two or more done steps become one done step. followLight({ collapse: 'done' }): the boards that followed become one line, first, so the board being lit is the first open one. A booked removalWatch is one line in Janus's own voice.
- Call
section({ title, done }, body) · steps(items, { label, done }) · followLight({ …, collapse: 'done' }) · removalWatch({ …, booked })- Keyboard
- Each line is a summary: Tab, then Enter or Space opens it.
- Name
- The line is its own name (the receipt words); the section keeps its heading.
- Live region
- None.
- Target
- 36 px; 56 on the station scale.
Site transition
Finished Mon 1 Sep · go/no-go 2 recorded by Bence Tóth
All seven phases, their checklists and decisions.
2 steps done · last 14:10 (done)
Restore the snapshot14:02 · Eszter Szabó (done)Finish the access review14:10 (done)- Reconcile integrations1 of 3 resumed
- Close the outage sheet
4 boards followed8 bays seen
-
Board 1 · side A, tier 12 of 2 followed
- A-T1-L01 Followed: Seen in A-T1-L01 · 14:02 DH
- A-T1-L16 Followed: Seen in A-T1-L16 · 14:04 DH
-
Board 2 · side A, tier 22 of 2 followed
- A-T2-L01 Followed: Seen in A-T2-L01 · 14:02 DH
- A-T2-L16 Followed: Seen in A-T2-L16 · 14:04 DH
-
Board 3 · side A, tier 32 of 2 followed
- A-T3-L01 Followed: Seen in A-T3-L01 · 14:02 DH
- A-T3-L16 Followed: Seen in A-T3-L16 · 14:04 DH
-
Board 4 · side A, tier 42 of 2 followed
- A-T4-L01 Followed: Seen in A-T4-L01 · 14:02 DH
- A-T4-L16 Followed: Seen in A-T4-L16 · 14:04 DH
-
-
Board 5 · side B, tier 10 of 2 followed
- B-T1-L01first bay of the board Lit: Lit green · put a reel in here
- B-T1-L16last bay of the board Not lit yet: Not lit yet
Missing R-2026-0088By: Janus · 13:22
- Last bay
- T5-L22 · dark
- Booked at
- the Missing delay, 30 min
- Reference
- EV-2461
Explain
A sentence that teaches how Janus works or what a control will do is on demand, never inline (rule 7): not a sub-line, a lead, a section meta, a notice body or text beside a button. explain() is an ⓘ button named by its summary that opens the words in a popover anchored to it; on a desk, resting the pointer on it previews the popover. Beside a section title use section({ explain }); for a whole page, PageHeader.explain() (a popover, or with panel the page help panel, a drawer).
- Call
explain(summary, body, { id, label, first, attrs }) · section({ title, explain }) · PageHeader(title).explain(summary, body, { panel })- Keyboard
- Tab reaches the button; Enter or Space opens; Escape or a click outside closes and returns focus.
- Name
- The button is named by the summary ("How the pick window works"); the popover is a region named by the same words.
- Live region
- None.
- Target
- 24 px on a desk (44 under a coarse pointer, 56 on the station scale).
Pick window
How the pick window works
A reel is taken when the sensor sees the bay empty within the pick window. After it, the bay stays lit until the reel is gone.
8 s · policy
People
14 people · 1 needs attention
Hint
Help for a person's first weeks is the explain() control with its summary as a visible label, so a new person sees that help is there; the explanation itself stays in the popover and is never re-inlined. The person's Settings › Hints decides it: labelled (always), labelled until they have opened it once in this browser (the first weeks, the default), or absent (off). The page passes its session's value; the kit decides what renders.
- Call
hint(summary, body, { hints: 'always' | 'first' | 'off', id, attrs })- Keyboard
- Tab reaches the button; Enter or Space opens the popover; Escape closes it and returns focus.
- Name
- Its label (the summary).
- Live region
- None.
- Target
- 24 px on a desk (44 under a coarse pointer, 56 on the station scale).
Why 8 s?
The placement window is 8 s. If it runs out, the put-in keeps watching: a late sensor answer still confirms it.
Why 8 s?
The placement window is 8 s. If it runs out, the put-in keeps watching: a late sensor answer still confirms it.
(nothing renders)
Entity head
An entity head carries at most 2 meta items, its state and its place. .meta([state, place]) renders the first two items that are not identifiers; an item that holds an identifier (an Activity entry, a permit, a controller, a command key, an IP or MAC address, a decision point), and every item after the second, goes to the head's Technical details, with .tech(rows). A single part is the free form (a skeleton, a page's own line): rendered as given, and the density gate reads it.
- Call
PageHeader(title).crumbs(trail).meta([state, place, …]).tech([[key, value], …]).actions(…).render()- Keyboard
- Technical details is a summary: Tab, then Enter or Space.
- Name
- The h1 names the thing; the meta line is text.
- Live region
- None.
- Target
- 36 px.
SR-02 · High-capacity rack B
Technical details
CTRL-B02 · 10.2.0.22Permit PMT-260924-01 · Dávid Horváth until 18:00
- Shape
- 6 tiers × 32 bays · 2 boards per tier
- Floor label
- POS-L2-03
Session chips
A session or role fact (Support session, View only) is a chip in the top bar, never a page banner: it is said on every page without taking the page's first viewport. The chip opens a popover with the words. Under 560 px the chip keeps its icon and its name.
- Call
sessionChip({ id, icon, label, title, body })- Keyboard
- The chip is a button: Enter or Space opens the popover, Esc closes it.
- Name
- The chip's word; the popover is a region named by its first line.
- Live region
- None.
- Target
- 32 px, 44 under a coarse pointer.
Station home
A station's home in DESIGN.md's order, which a page fills and can't reorder: the rack as the status (rackNotes), the one thing yours now (focusCard, whose action is the only primary), waiting work and history as counts behind folds, then the one start tile. Beside the rack from 900 px of its own width; one column, in the same order, below.
- Call
stationHome({ rack, focus, folds: [{ summary, body, id, hidden }], start: ActionTileOpts, startAlso, startTitle, level, id })- Keyboard
- Reading order is the slot order: the rack's pins, the focus action, the folds, the start tile.
- Name
- The start region is named by its heading ("Scan to start"); the rest by their own components.
- Live region
- None: the station re-renders a slot from the live topic.
- Target
- Station scale: 56 px, the primary 64–72.
Take out R-2026-0012.
Pull WO-260714-07
Ready · lights when you press Next
2 more yours
- Put back R-2026-0057Due back from SMT Line 2
Recent
Stored R-2026-0042 at SR-01-T3-L14 · 14:32
Scan to start
Take out R-2026-0012.
Nothing waits for you
New work sent to you shows here.
Scan a reel to start
Technical details
- Gate
- tests/density/
- Rules
- DESIGN.md › What shows when, 1–12
- Voice
- DESIGN.md › Voice and copy, 1–15
Behaviours
app.js owns these contracts on every page. Page scripts import helpers from /assets/kit.js and never re-implement them. Every demo is a real form or link that htmx sends; its route answers the way a product write does (a fragment, or 204 and a toast).
Close on done
A form in a dialog closes its dialog once the write succeeded. A rejected write answers 4xx with the fields and their errors, and the dialog stays open.
- Markup
<form data-close-on-done>[data-close]data-close="#id" data-open="#id"- Keyboard
- Enter submits; Esc closes; focus returns to the button that opened it.
- Name
- The dialog is named by its title (aria-labelledby).
- Live region
- The result shows on the page; the toast confirms it in #toasts (polite).
- Target
- 44 px buttons in the foot.
SR-02 is called Paste line.
Dialogs the server opens
A dialog rendered with data-modal="open" is shown modally on load and after every swap. A link to #id opens that dialog; closing drops the fragment and the URL params named in data-clears.
- Markup
dialog[data-modal="open"][data-clears="kd"]<a href="#dialog-id">- Keyboard
- Focus moves into the dialog; Esc closes; focus returns to the control that asked for it.
- Name
- Dialog title (h2) via aria-labelledby.
- Live region
- None: the dialog itself is the answer.
- Target
- Openers are 44 px buttons or links in text.
Focus after a swap
After htmx swaps content in, focus goes to [data-focus] in the new content (the element, or the id it names), then to [autofocus] when focus was lost. Otherwise the control that caused the swap keeps focus: it is found again by id.
- Markup
<div data-focus="new-row-id">[data-focus][autofocus]stable ids on controls- Keyboard
- Tab order continues from the focused element.
- Name
- A row that takes focus gets tabindex="-1" and is read by its content.
- Live region
- None needed: focus says what changed.
- Target
- Unchanged.
- R-2026-0042Put in at 14:32
- R-2026-0109Put in at 14:32
Gates
A [data-gated] submit is disabled while its form is invalid. Kit constraints add at least n boxes (fieldset[data-min-checked]), a typed phrase (input[data-phrase], paste blocked) and a changed-from-saved gate (data-gated="changed"). Gated forms start fresh on every opening of their dialog (reset and re-gated on close; when that disables the invoker, focus goes to the nearest enabled control in its section). data-gate-scope="#sel" makes a gated button count only the form controls inside that element (an opener waiting for a changed tick while the dialog it opens holds the required acknowledgement).
- Markup
[data-gated][data-gated="changed"][data-gate-scope="#sel"]fieldset[data-min-checked="1"]input[data-phrase][data-match="#id"]minlength- Keyboard
- The disabled submit is skipped by Tab; the fields say what is missing.
- Name
- The unmet reason is the submit’s aria-describedby (“Reason: …”).
- Live region
- A typed phrase says “Phrase matches.” once, through the one announcer.
- Target
- 44 px submit; 24 px check boxes with their whole label as target.
Copy
data-copy is the value itself (a site path becomes a full link; an <a data-copy> with no value copies its link). data-copy-from names the element whose text or value is copied. data-copied is the toast.
- Markup
[data-copy="/activity.html#EV-2487"]<a href data-copy>[data-copy-from="#id"][data-copied="…"]- Keyboard
- Enter or Space on the button.
- Name
- The visible words, or an aria-label that contains them (“Copy link to EV-2495”).
- Live region
- The toast says what was copied (#toasts, polite).
- Target
- 44 px buttons; 32 px icon buttons in dense rows.
Site clock and live durations
The site clock is the one time authority (Opus.now(), kit.js now()): it starts at 14:32 when the session’s first page opened. [data-clock] shows it; [data-ends] and [data-since] hold the time left or gone, in site time.
- Markup
[data-clock][data-clock="s"][data-ends="22:00"][data-since="14:24"]data-format="min-s|m:ss"- Keyboard
- Not interactive.
- Name
- The sentence around it says what the time is for.
- Live region
- None: clocks never announce themselves.
- Target
- n/a
- Now
- · with seconds
- Shift B ends 22:00
- 7 h 28 min left
- Feeder 14 is late
- by 8 min
- The offer lapses 14:39:40
- 7 min 40 s left
Countdown
A countdown counts its seconds down from when it appears (a new one from the server restarts it). At a quarter left it turns amber; at zero data-on-zero runs: “submit” (its form) or “#id” (a form is submitted, anything else is clicked).
- Markup
[data-countdown="30"][data-on-zero="#form-id"]data-totalopus:zero event- Keyboard
- The switch-off stays a button; nothing depends on waiting.
- Name
- role="timer", aria-label “n seconds left”.
- Live region
- Off: a timer never reads out each second; the page says what happened at zero.
- Target
- 44 px Switch off.
SR-01-T3-L14 is lit amber. It switches off by itself.
Busy label
A submit with a busy label says it is working while its request runs, and a second press does nothing. The label stays until the answer lands.
- Markup
btn(label, { busyLabel })[data-busy-label="Sending…"]- Keyboard
- Focus stays on the button.
- Name
- The label becomes the busy words; aria-busy="true" and aria-disabled="true" while it runs.
- Live region
- The toast confirms when it is done.
- Target
- 44 px.
Toasts
Toasts confirm a result the page can’t show on its own. The server sends them with HX-Trigger (or the shell’s one-time flash after a write that navigates); buttons can raise one with data-toast="text|tone". Undo rides along when the server offers it.
- Markup
HX-Trigger {"toast":{text,tone,undo}}[data-toast="Label reported.|ok"]toast(text, tone, opts)- Keyboard
- Dismiss is a button; hover or focus pauses the timer.
- Name
- “Dismiss”; Undo says what it undoes by its place in the toast.
- Live region
- #toasts is polite; a bad toast is role="alert" and stays until dismissed.
- Target
- 28 px dismiss inside a 44 px toast row.
Pick grids and tab strips
A rack drawn with pick is an ARIA grid with one tab stop: arrows move, Home/End go to the ends of a tier, Enter, Space or a click picks the bay (Ctrl, ⌘ or Shift adds to a multi grid). The picked bay wears aria-selected and keeps the tab stop; the grid dispatches rack:pick { id, cell, selected }. Page scripts listen for rack:pick and never handle the click themselves. A tab strip that scrolls sideways on a narrow screen (viewNav, tabs) keeps its current tab in sight on load and after a swap; only the strip scrolls.
- Markup
rackGlyph(spec, { pick: true, multi? })rack:pick { id, cell, selected }viewNav / tabs (scrolling strips)- Keyboard
- Tab reaches the grid once; arrows move between bays; Enter/Space picks. Tab strips: Tab between links.
- Name
- The grid is named by its label; each bay is named "SR-01-T2-L03, occupied".
- Live region
- None: the picked bay is the state (aria-selected); the page says what the pick did.
- Target
- Bays are grid cells (exempt from 24 px); tab links are 44 px tall.
Announce, fill, since, print, fold, counters
Small contracts: [data-announce] in swapped content is said once through the one live region (a status line inside a region every write replaces). [data-fill][data-fill-target] puts a value in a field and focuses it. [data-since-s="N"] counts up from N seconds before the render, on the site clock. [data-print] prints (the shell hides its chrome in print). details[data-open-above="560"] (disclosure({ openAbove })) starts folded on narrow screens. [data-count-for] (textarea({ count })) shows n / max. A dialog nothing opened returns focus to its data-return-focus. [data-required-when="name=a,b"] makes a control required, and shows its field, only for those choices.
- Markup
[data-announce][data-fill="R-2026-0042"][data-fill-target="#uid"][data-since-s="95"][data-format="m:ss"][data-print]details[data-open-above="560"][data-count-for="id"]dialog[data-return-focus][data-required-when="rv=gone,sensor"]- Keyboard
- Fill and print are buttons.
- Name
- Their words.
- Live region
- announce() only (one polite region).
- Target
- Buttons 44 px.
Live regions
An element with an id and data-live="topic,…" re-renders from its page's own URL (or data-live-src) when one of its topics fires on the live feed (GET /events). Bursts coalesce (300 ms, one fetch per URL); the swap morphs, so focus, scroll and what is typed stay, and it says nothing. A region being typed in or holding an open dialog waits until focus leaves. A page in a Preview state (?state=) is a fixture: it ignores the floor simulation, and the server refuses its writes. While the connection is lost the regions carry [data-stale] (values dim, [data-asof] words get the time of the last update); the stop signal says so (below). Topics: review · alerts · rack:<id> · people · task · decision · scan · floor · reel · activity.
- Markup
[data-live="review,rack:SR-01"][id]data-live-src="/url"[data-asof]live(topics, fn)live:<topic>live:state- Keyboard
- Nothing new: the region keeps its controls and focus.
- Name
- The region's own content.
- Live region
- None on the region (it would speak on every change).
- Target
- Unchanged.
Review queue · 12 open · decide one in another tab and this follows
Stop signal
Neo 0026 §18 and architecture/system-structure.md §6.4–6.5: when the connection is lost, nothing is booked, and the screen says so. One state on every page, on <html data-link>: connected (nothing shows), reconnecting (the stream was lost, or it is back and the page is catching up; a blip under 3 s says nothing) and stopped (gone 5 s, a named heartbeat silent that long, or no network: at once). While not connected every htmx POST is held back with the signal's words and every [data-needs-live] region is inert. Opening the connection never lifts the signal: a fresh render does (the page read again, every live region re-rendered), and only then is anything admitted. The words are preloaded with the page (liveState()'s data-say-*), so a device with no network still says them. Three forms: the pill in the shell's top bar (on a phone it hangs full width under the bar), the strip over a handheld's task bar (56 px), the veil over a kiosk's task pane (must-read size). The veil arrives at --m-enter and leaves at once, only after the fresh render: the page is true again the moment it goes. Reduced motion: it appears at once.
- Markup
liveState({ form: 'pill' | 'strip' | 'veil', body })[data-stop-signal]html[data-link="connected|reconnecting|stopped"][data-needs-live]opus:net { link, online, asOf }live:heartbeat- Keyboard
- Nothing to press: a region that needs the connection is inert while it is lost.
- Name
- Its words: "Not connected — nothing is being booked", then what the device is doing ("Reconnecting…" or "Offline") and "as of 14:32".
- Live region
- None on the signal: stopping and coming back are each said once through the one announcer.
- Target
- Not a target. The strip is 56 px tall on the station scale.
Take out T2-L28
The bay blinks blue.
Scan the reel.
Put in · SR-01
Station writes (no outbox)
R1 keeps no client-side queue (Neo 0033 §7, 0058 §1): a station sends a write now, or not at all. kit.js record(action, values, label, { key, movement }) is the one station write (stationWrite() is record() with the movement rule): send() reads <html data-link> first, so nothing leaves a station that is not connected (Not booked); the write's intent (its Idempotency-Key and words) is kept in the terminal session before it is sent; an answer lost is Outcome unknown, never resent, asked about through POST /writes/reconcile until Janus says Recorded or Not recorded (its id then spent), and no new movement is sent meanwhile. The capture journal it replaces is parked: kiosk.html?state=journal.
- Markup
record(action, values, label, { key, movement }) → { outcome: accepted | rejected | not-booked | unknown | blocked | preview, receipt, reason }stationWrite()send()reconcile()intents()intentOf(key)settleIntents()intent:changeintent:settledIdempotency-Key- Keyboard
- Nothing of its own: the station's controls are the write's.
- Name
- The page names what happened in its own words; lastActions() lists them.
- Live region
- A settled Outcome unknown is said once (station-kit.js).
- Target
- Station controls.
See the stop signal and last actions under Domain blocks; the kiosk, handheld and phone camera use them.
kit.js for page scripts
Page scripts import these from /assets/kit.js (the same module instance as app.js). They never re-declare them, keep their own clock, or build HTML strings the kit renders.
$(sel, root?) · $$(sel, root?)One element, or an array of them.on(type, sel, (e, el) => …) → offA delegated listener on the document (on(type, fn) listens plainly).onSwap((roots) => …) → offAfter new content lands, whatever the swap style (outerHTML included). roots are the new elements.now() → DateThe site clock: Wed 24 Sep 2026, 14:32 at the session’s first page, running in real time.every(ms, fn) → stopfn now and every ms; whole seconds tick with the site clock.fmtInt(n) · pad(n, w = 2) · dur(min)12 400 · 07 · “7 h 28 min”, the same words the server writes.toast(text, tone, opts) → dismissThe one toast region. opts: { undo, ttl, html }; html: true only for trusted page copy.announce(text)The one polite live region for page messages that have no visible place.ack(frame, tone) · seedKeypad(target, value, fresh) · identify(rackEl)Station helpers: the frame edge flash for a read, a keypad value set from script (the next digit starts over), and the rack Identify pattern.keypads()Wires every kit keypad ([data-keypad]; app.js calls it). A key dispatches a cancelable opus:keypad { key, step, target } first; digits, Clear, Delete and steps then type into the target and fire its input event.legacyHash(map) → redirectedOld #state links → the ?param URL the server renders ({ weeks: { tab: 'weeks' } }).live(topics, fn) → off · liveState() · liveRetry()The live feed: one EventSource per browser shared by its tabs, resumed after the last event, reconnecting with backoff. fn gets { topic, data, sim, id }.record(action, values, label) → { outcome, key, receipt, reason }A station record of what a person did: accepted (with the server's receipt) · kept (in the outbox, sent later) · rejected · unknown · preview. A page claims only what the outcome says. outboxList() · outboxAttention() · outboxFlush() read and send what is kept.send(action, values, { key, actor, device }) → { status, outcome, refusal, events }The one write outside htmx (record() and stationWrite() use it): a POST whose answer the script must read (Janus-Outcome, Janus-Refusal).