Rail
canonThe record panel: context actions, 34px key-value rows edited in place (several values = several rows), tags, description and relations.
Merged 14 September 2026 (AD, RD-1): Key-value row, Editable field and Multi-value were separate pages; they are parts of this one card. The app copy of the row (KeyValueRow, .pr-row) is being replaced by the rail row and deleted. A value with href/onOpen never opens the editor on click and keeps a permanently visible pencil (AD FA10-re); errors are the caller’s toast. The multi-value editor popover lives in the app (components/multivalue/*, ds/multi-value.css). Mobile sheet merged 15 September 2026 (MR !3539): below 720px `MultiValuePopover` renders as a bottom sheet instead of an anchored popover (`.phe-scrim`, `.phe-grab`, `.phe-sheethead`, `.phe-sheetfoot` in `ds/responsive.css`) so it can be closed and confirmed without a keyboard — see "Mobile" in rail.usage.md / rail.a11y.md. Stories rebuilt from the real person rail 15 September 2026 (acrm !3544): four quiet icon actions, Detail záznamu / Procesy / Seznamy, no empty section header; the rail has no shortcuts, so its „With icon & kbd" story and both its baselines are gone. Meeting attendees composition (owner 25.–27. Sep 2026, acrm !4014/!3985, all in `apps/web/src/ds/meeting-record.css` — not yet ported into record-rail.css): field `type: 'number'` for a small bounded count paired with `useConfirm` (speaker count, range 1–20 per the diarization service, raised from 10 on acrm !3985); a relation row's right-aligned talk-share bar (56px `--line-200` track, 4ch tabular-nums percentage) — candidate for a named "share bar" element; an unassigned-voice row with `--sp-3` icon-to-text / `--sp-4` name-to-action spacing; and a scoped exception (`.rr-list--attendees`) that keeps the removable reference's × contextual on touch instead of the global `ds.css` always-visible rule, so it never sits permanently over the share bar. Touch (AD 28 Sep 2026, acrm !4111): the description measures its real clamp (.rr-desc-v.is-clamped, ResizeObserver) and shows the chevron only then; on hover: none copy and the chevron are always visible with 44px hit areas, and copy ↔ pencil keep a 30px gap so two 44px targets never overlap (the rule beats --sp-2). Overrides live in apps/web/src/ds/responsive.css. Registry repaired 17 Oct 2026 (review F4/F11): attached css detail-field.css.
Used in 29 files · 20 areas · 79× — components/PersonaProfilePanel, components/clen-tymu, components/company, components/deal, components/decisions, components/dialer …
@acrm/ui <RecordRail> <EditableField> <useInlineEdit> <RailRows> <RailRow>.rr-row .rr-sec-h .rr-relrow .rr-dot +25SourceStoryLegacy card Record detail rail.htmlLegacy card Multi-value editor.htmlCovers the whole card: the panel, its rows, editing a value in place and fields with several values (merged 14 Sep 2026, RD-1).
Keyboard
- Every interactive part of the rail is a real control: action buttons, section headers that fold, value hover-zones (
.rr-ev, focusable), the pencil where it is a button, the copy button, „Zobrazit všechny hodnoty“ and the relation rows. - Tab walks actions → fields (top to bottom) → relations. Each value of a multi-value field is its own stop.
- Enter on a focused value opens the editor; Escape cancels and returns focus to the value. Enter in a single-line editor commits; blur commits too, and a latch makes sure Enter-then-blur cannot save twice or Escape-then-blur resurrect a cancelled edit. A multi-line editor (
textarea) keeps Enter for new lines and commits on blur. - A value that is a link or an action follows it on Enter; its editor is reached through the pencil, which is a real button in the tab order.
- Folding a section keeps focus on the header; folded content is removed from the tab order.
- There is no arrow-key movement between rows — the rail is a list of fields, not a grid.
- Multi-value editor: Tab walks grip → value → type → remove for every row and ends on the add input; Enter in the add input appends a value. Remove reacts to
mousedownso the popover cannot close under the pointer — Enter/Space on the focused button still works. Reordering is drag-only (see Known limits). - Below 720px the editor is a bottom sheet:
.phe-sheetfoot's "Done" is an ordinary focusable<button class="acrm-btn primary">, reachable by Tab like the rest of the popover's rows, and the scrim closes the sheet on tap. There is no sheet-specific Escape behaviour beyond what the popover already does (onEscape ?? onClose).
ARIA
- The pair is not a definition list: the label is a
<span>next to the value, and the reading order (label then value) carries the pairing. Label and icon are not focusable. - Values that lead somewhere are
<a>or<button>with atitlefromopenLabel(Otevřít web,Zavolat), so the action is announced instead of being guessed from the value. - The copy affordance is a button named by the field (
Kopírovat Telefon); it is only rendered when the rail has anonCopyhandler, so there is never a dead control. - A non-editable row has no editing affordance at all, so nothing promises an interaction that will not happen. The empty placeholder is text inside the value slot — a screen reader reads the invitation, not a blank.
- Read mode is a focusable element with the value as its text; edit mode replaces it with a labelled control.
- Relation rows are references (
EntityRef); their remove control carriesaria-labelnaming the item. - The status dot
.rr-dotisrole="img"witharia-labelandtitlefromRRRelationItem.dot.title("Přijal(a)", "Možná", "Odmítl(a)") — it is the only carrier of the answer, so it must never be rendered without a title. - The icon slot
.rr-statusholds whatever the caller passes; the caller owns its accessible name (delivery ✓✓ comes from Mail with its own title). - Multi-value: the remove button is named for its value (
Odebrat +420 605 123 456); the editor setsaria-busywhile a change is being written; the dimming of further values is visual only — every value is read in full. - The sheet keeps the popover's own
role="dialog"andaria-label(fromtitle/label) regardless of viewport —MultiValuePopoverdoes not swap roles for the mobile variant. The grab handle and the scrim are botharia-hidden="true", purely decorative. - Nothing announces a successful save, a failed save, or an added / removed value (see Known limits).
Focus
.rr-evsetsoutline: noneand supplies its own ring —box-shadow: 0 0 0 2px var(--accent)— because the row is 34px and a 2px outline with offset would touch the neighbouring row.- The open editor
.rr-editdraws an accent border plus a 3px 20 % accent halo and no outline. - Cancelling or committing returns focus to the value, so the user keeps their place in the rail. Focus never moves on hover; the pencil and copy icons appear, but nothing takes focus.
- Everything else in the rail (buttons, links, pencil, copy, editor controls) keeps the foundation ring from
ds.css. - The decorative pencil is a
<span>and is not focusable; on action rows, where it is the only way to edit, it is a real button. - Row actions (
.rr-acts, e.g. „Odebrat vazbu“, accept / reject on a CRM change row) are hidden withopacity, nevervisibility: they stay in the Tab order and keyboard focus reveals them via:focus-within(acrm !4532, 3 Oct 2026 — before, they were unreachable from the keyboard). A contract test guards both.rr-actsand the list's.il-acts. - The sheet variant does not trap or move focus any differently from the desktop popover — no additional focus management runs for it; Tab can still walk out of the sheet into whatever the popover is portalled into (the host dialog, or the page behind the scrim on
<body>), same as the popover's own known limits.
Contrast
Computed from packages/ui/src/tokens/tokens.css:
- Value
.rr-row .v--ink-900on the panel = 17.59:1 light, 14.53:1 dark. - Label
.rr-row .kuses the--labeltoken in a muted tone — below AA for small text, and it is the muted half of a pair whose value carries the meaning. - Empty placeholder
.v.ph--fg-muted= 2.85:1 light, 3.63:1 dark — below AA; it is an invitation, not content. - The editing ring, the editor border and the value link use
--accent= 4.44:1 light, 3.83:1 dark — passes the 3:1 non-text minimum. - The green saved check uses the positive tone; it is a confirmation, never the only carrier of state.
- Second and further values of a multi-value field and the value type
.phe-typeare--fg-muted— the same limit, deliberately, so the primary value reads first. This is the card's main accessibility cost. - Pencil, copy, grip and remove glyphs
--fg-muted— below the 3:1 non-text minimum in light theme; they are secondary affordances next to a value that is itself the target. - Sheet grab handle
.phe-grab--line-200— decorative andaria-hidden, no text or target sits on it, so it carries no contrast requirement. Sheet scrim.phe-scrimuses--ov-scrim, the same overlay tone as every other modal scrim.
Touch
- Every icon action in a row (copy, pencil, description chevron) has a hit area of at least 44 × 44 px on
hover: none; copy and pencil sit 30 px apart so their areas never overlap (see usage → Touch). - Nothing is reachable only by hover: copy and the chevron of a clamped description are always visible on touch, reordering goes through a menu instead of a drag grip.
Known limits
- The "unknown" dot is
--ink-300on the rail surface — well below the 3:1 non-text minimum. It is deliberate (it must read as "nothing came back yet", not as a state), and the meaning is in the tooltip andaria-label, not the colour; on an 8px dot the earlier--ic-limewas indistinguishable from green (AD, 6 Sep 2026). - No live region: a save is confirmed only by a green check that fades after ~1.1 s, a failed save reverts silently in the row (the screen's toast is the only announcement, and it must be one), and adding or removing a value is silent.
- There is no "saving" state — a slow save shows nothing until it settles.
- Labels, placeholders and secondary values sit at the
--fg-mutedlimit in light theme. - The pencil appears on hover; on touch it is permanently visible only for action rows, so on a touch device an ordinary row is edited by tapping the value itself, which is not signposted. Copy is hover-revealed on desktop and permanently visible below 720px and on touch.
- A long label is cut by the 108px column with an ellipsis and no tooltip.
- Multi-value: reordering by grip has no keyboard alternative, and removing a row drops focus to the body.
- The sheet's grab handle (
.phe-grab) is a purely visual affordance — there is no swipe-to-dismiss gesture wired to it; the sheet only closes through the "Done" button or a scrim tap.
Source: docs/record-rail/rail.a11y.md · GitLab