- Drop the title prop: no caller passes it, and the menu's name is for screen
readers only.
- One line each for the output options, the Default placeholder id and the
list's height bounds.
- Keep only the two non-obvious reasons behind the focus tracking.
- Call what moved the focus its source rather than its modality.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Drop "browser settings": Element Desktop grants microphone access through the
app, not a browser.
- Mirror the no-device message, which names no app.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- In a short window the footer fades out when the pointer leaves the window.
The chevron menus are portalled to the document, so they stayed behind, their
trigger faded out beneath them.
- The footer already stays for keyboard focus. A menu's focus is outside it, so
the trigger's aria-expanded tracks it instead.
- Pre-existing: the camera chevron does the same on main today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Two participants, for what only a real call can show: a device switch that
neither drops the media session nor rejoins, and a meter that keeps moving
while muted while the peer is still shown the mute rather than left to infer
it from silence.
- The component harness covers what only a host page shows: every device
reachable in a small container, the list sized against the call rather than
the window, and the menu following the call area when the host resizes it.
- Synthetic devices are patched onto the media element and the audio context
alike, so a browser with no hardware still enumerates and still routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- One story per state that matters: silence, three volumes of speech, denied
permission, no device, many devices, and a selection still settling.
- The geometry the design rests on is asserted rather than eyeballed — the bars'
shape at any width, the icon's two centre lines with the rows above it,
headings that stay while their section is in view, every row reachable by the
keyboard clear of both, and each section headed by a rule edge to edge.
- Each story supplies its own microphone and its own call-sized root, so what it
renders is what the app renders. Browser flags that fake a device were tried
and rejected: they made the suite pass while the same story showed no
microphone in a browser without them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The footer view model now carries the output devices, the current output and
the callback that changes it, alongside the ones it already carried for input.
- That callback is withheld where the platform cannot route audio to a chosen
device, and its absence is what renders the section disabled — an absent
action rather than a flag the view has to interpret.
- An empty output list stays a list rather than becoming an absence, so the
section is still drawn where a platform enumerates nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Two labelled sections, speaker above microphone, with radio selection in both,
the pattern the settings list already uses.
- A device that cannot be chosen is shown disabled rather than hidden, so the
menu keeps one shape across browsers; where a platform lists no output at all,
a disabled Default stands in that section instead of leaving a bare heading.
- The list is bounded by the measured call area rather than the window, so it
stays usable where Element Call is a component in a corner of a host's page,
and follows that area when the host resizes it.
- It scrolls with its section headings pinned and the meter held at its foot,
and keeps both clear of whichever row the keyboard has just reached.
- The focus ring is shown only when the keyboard moved the focus, tracked on the
menu rather than the document, so two calls on one page cannot answer for each
other.
- While a requested device has not arrived, nothing in either section can be
picked, so a second request cannot overtake the first. That holds only while
the device is still offered and only within the menu it was asked in: one
removed before the switch lands never arrives, and waiting for it would leave
every device unselectable for the rest of the call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Draws the level as lit bars, and each failure state as its own message with a
next action rather than as a meter that has stopped.
- A bar and the space beside it keep one size wherever the meter is drawn; what
the width decides is how many bars there are. Bars spread to fill would be a
different shape in every place, and would close into one block where the space
ran short.
- Announces the level on a fixed scale, so it does not rest on hue or on how
many bars happened to fit.
- The hook is a bridge and nothing more: the capture belongs to the state layer.
- The capture is held by a component of its own, so a level that does change
redraws the meter rather than whatever is rendered beside it. A level arrives
many times a second; held by the menu it would reconcile every device row on
the way to the bars. A test counts those renders, because nothing else would
notice the hook moving back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Opens a short-lived capture of a named device and reports a smoothed level.
- Owns the capture, the audio context and the frame loop, and releases all three
idempotently, including when cleanup lands before getUserMedia has resolved.
- Reports denied permission and a missing device as states of their own, so a
caller never has to read silence as an error.
- Gives the microphone back when the audio graph fails to build. That can fail
after getUserMedia has resolved, and the capture would otherwise outlive its
own failure: the device open and its in-use light on, behind a meter saying
it is unavailable.
- Shares the capture stub through the test utilities, silence and a signal
alike, so the meter's tests and its stories drive the same microphone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>