Session bar
The strip that says whether capture is running: a toned state pill, an optional degradation notice, the elapsed duration and the controls that change it.
statusconversation
Install
pnpm dlx shadcn@4.23.0 add @ansora/session-barExamples
Usage
Five things this component cannot enforce and you must:
- After a page reload the bar comes back as
paused, neverlive. A reload tears down the MediaRecorder and the stream, and permission may need re-granting. A bar readingliveafter a reload claims capture that is not happening — the worst failure available here. endedis not a toast. It stays until you unmount it.- A list of bars sets
announce="off"on every row. Forty polite regions on the receptionist's active-call list is a screen-reader denial of service.announcedefaults topolitebecause a single bar changes at most five times an hour. - Do not put a destructive control (Ukončit) in an overflow menu. Wrong on its own merits, and the geometry gate never opens a menu, so it would ship unmeasured too.
meteris renderedaria-hiddenandinert. Anything focusable you pass there stops being focusable — deliberately, because hidden-but-operable is worse than either alone. A control does not belong in that slot;actionsis the slot for controls.
state says what capture is DOING; notice says what is degraded about it. That is two facts, not one: "offline but still recording to the device" is live + a warn notice, and "the far end dropped while my microphone is fine" is the same shape in the video product. Collapsing them into one enum forces a lie in both.
A live region announces CHANGES made while it is already in the document; its initial contents are not announced. Mount SessionBar at the instant capture starts — the obvious reading of "render it when the session begins" — and a screen-reader user hears nothing at all for live, the one state this whole component exists to report. Render the bar before capture starts (connecting is what that state is for), or accept that the first thing announced is the SECOND state change. No component can detect the difference for you.
The live region is the visible pair of pills — the state pill plus the optional notice — and it carries aria-atomic="true". It is deliberately NOT the bar root, because the root holds the ticking LiveDuration: an atomic region around that would be re-read every second, and a non-atomic one would read bare numbers. Keep any clock of your own out of that pair for the same reason. Nothing in this design system verifies what a screen reader actually says — no test here builds an accessibility tree — so treat that as structure, not as a measured result.
Positioning is yours: the bar ships no sticky, fixed or z-*. Pass className="sticky top-0 z-20" where the screen wants it. Note that @ansora/app-shell's header is NOT sticky, so a bar sticking to the viewport top pins after the header scrolls away.
Placement: put it in a client SessionHost wrapper of your own inside AppShell's children (children pass through a client component as a server-rendered subtree), or in AppShell's headerActions slot. Both work because actions is a ReactNode rather than onPause/onStop — a function prop cannot cross a Server→Client boundary. AppShell deliberately gets no sessionBar prop: it would force your layout to become "use client".
duration also carries tick, hours and max straight through to @ansora/live-duration, and the two long cases above need them. A list of bars sets tick="minute" as well as announce="off": at the default "second" every row re-renders once a second, which is exactly the cost the shared ticker's quantization exists to avoid — forty rows then cost forty re-renders a second instead of forty a minute. A visit that can run over an hour sets hours, or 1:02:34 renders as 62:34. What does NOT pass through is the announcement policy: role, aria-live and aria-atomic are never in LiveDurationProps, so neither this component nor its caller can make the number talk.
duration.asOf is the SERVER's clock reading, passed down with the rest of the payload — never new Date() inside a client component, which is evaluated once during SSR and again during hydration. duration.label is the invisible name of what is being timed; the visible pill text is stateLabel and the visible notice text is notice.text.
If a @ansora/save-status is on the same screen, one of you has to stay quiet about the network. A dropped connection ends capture and fails a save, so both regions speak, politely and in a queue, about one cause. SaveStatus has no announce switch, so the lever is here: pass announce="off" when this bar is not the component that owns the network story. The four announcement policies in this design system and how they compose are written down in one place: the ansora-visual-language skill, references/accessibility-qa.md §12.
import { Button } from "@/components/ui/button"
import { SessionBar } from "@/components/ansora/session-bar"
<SessionBar
state="live"
stateLabel="Nahrávám"
notice={{ tone: "warn", text: "Bez připojení — ukládám do zařízení." }}
duration={{ from: startedAt, asOf: renderedAt, label: "Délka záznamu" }}
actions={
<>
<Button size="sm" variant="outline">Pozastavit</Button>
<Button size="sm">Ukončit</Button>
</>
}
/>
Files
registry/ansora/components/session-bar.tsx→@components/ansora/session-bar.tsx
Dependencies
@ansora/base@ansora/status-pill@ansora/live-duration