Rail

canon

The 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 …

Covers 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 mousedown so 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 a title from openLabel (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 an onCopy handler, 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 carries aria-label naming the item.
  • The status dot .rr-dot is role="img" with aria-label and title from RRRelationItem.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-status holds 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 sets aria-busy while 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" and aria-label (from title/label) regardless of viewport — MultiValuePopover does not swap roles for the mobile variant. The grab handle and the scrim are both aria-hidden="true", purely decorative.
  • Nothing announces a successful save, a failed save, or an added / removed value (see Known limits).

Focus

  • .rr-ev sets outline: none and 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-edit draws 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 with opacity, never visibility: 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-acts and 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-900 on the panel = 17.59:1 light, 14.53:1 dark.
  • Label .rr-row .k uses the --label token 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-type are --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 and aria-hidden, no text or target sits on it, so it carries no contrast requirement. Sheet scrim .phe-scrim uses --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-300 on 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 and aria-label, not the colour; on an 8px dot the earlier --ic-lime was 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-muted limit 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