Building the analyser can fail after getUserMedia has already resolved,
and the capture then outlived its own failure: the microphone stayed
open, and its in-use light on, behind a meter reporting the microphone
as unavailable, until the menu closed.
Cleanup did two jobs under one name. Releasing what has been acquired is
now its own step, which teardown and the failure path both take.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu built the label for a default output by joining a translated
"Default" to the device name, which leaves a translator no say over word
order, spacing or the brackets.
It now interpolates a string of its own. Not `default_named`, whose
markup styles the parenthetical for the settings dialog: a menu item's
label is plain text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With no microphone attached the menu drew a level indicator resting at
zero, announcing "No sound detected" — which is exactly what a working
but silent microphone draws. The two states people most need to tell
apart looked the same.
The level state now distinguishes a microphone that is absent from one
that is merely quiet, and the microphone group names the absence where
the indicator would be, as it already does for a denied permission. A
microphone that is merely slow to open still draws at rest, so the
indicator is on screen as soon as the menu is.
The product spec listed this as an edge case but no requirement carried
it, so no acceptance criterion covered it either. Added as AC28 with the
owner's authorisation, and the product spec's edge case reworded to say
what the menu does rather than that it shows nothing.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md — AC28.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The meter reported a fresh level on every animation frame, so the whole
menu reconciled around sixty times a second for as long as it was open,
silence included.
The level is now rounded to the number of bars the meter draws, and the
previous value is kept whenever that has not moved, so a frame with
nothing new to draw re-renders nothing and silence costs nothing at all.
The capture hook also moved into a component of its own, so a level that
does change redraws the meter rather than the device rows and the slider
beside it.
The bar count is what sets that resolution, and the hook is told it
rather than naming a number of its own.
Three tests count commits per animation frame, an effect with no
dependencies standing in for a paint: silence commits nothing, a steady
tone commits only until the level settles, and the reported level is
always one the meter can draw.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md — no acceptance criterion
changes; AC8 and AC12 hold as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lobby footer reads the same device behaviours as the in-call one, so
the menu already reaches every platform before joining, and stays gated
to desktop during a call. These are the checks that hold it there: the
view model offers the menu pre-join on desktop, Android and iOS, and the
microphone button stands alone with no chevron once a call starts on
mobile.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 5 — AC18, AC19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu gains a third group with the sound-effect volume slider, reading and
writing the same stored value as the slider in settings, so the next effect
plays at the new level. The menu is bounded to the height Radix reports for it
and only the device lists scroll; the heading and the slider stay in place.
Every control is now reachable by keyboard alone. Radix swallows Tab inside
its menus so that the arrow keys walk the items; the audio menu keeps Tab
from it, letting the browser move focus from the device rows to the meter and
on to the slider, while the menu's focus trap keeps that order inside the
menu. The slider's own keys stop at the slider so the menu does not treat
them as navigation. The meter keeps its live-region text on blur: removing it
handed the focus trap an empty active element mid-Tab and pulled focus back
into the menu.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 4 — AC4, AC16, AC17,
AC20, AC21 (manual), AC22, AC27.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The meter sits at the foot of the microphone group and follows the selected
microphone. Its capture starts when the menu opens and stops when it closes,
muted or not, so the microphone is only held while the user is looking at the
level. A denied permission shows a hint in place of the meter.
Keyboard navigation marks the current menu item with a focus border; pointer
use gets the hover background only, since the menu moves DOM focus to
whatever the pointer is over.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 3 — AC9, AC10, AC12,
AC14, AC23, AC24 (manual), AC26.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
useMicrophoneLevel opens a capture of the given microphone for as long as it
is enabled, reports a smoothed 0..1 level per animation frame, and tears the
capture down on disable, device change or unmount, including while the
capture is still being acquired. A denied permission and a microphone that
cannot be opened are reported as distinct states.
AudioLevelMeter renders that state as a row of bars, focusable and announcing
its state to a screen reader only while focused, with a hint in place of the
meter when permission is denied and a greyed-out meter when the microphone is
unavailable. Nothing renders it yet; the audio menu picks it up next.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 2 — AC8, AC11, AC13,
AC15.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chevron beside the microphone button now opens a menu headed "Audio
controls" that lists the microphones and, below a separator, the audio
outputs. Selecting either switches the device and keeps the menu open.
Where the browser offers no choice of output, or only one, the speaker
group still names the output in use as a non-selectable row.
The footer view model derives the outputs from the same MediaDevices
helper as the device lists, so the menu reaches both surfaces at once:
in a call on desktop, and before joining on every platform.
Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 1 — AC1, AC2, AC3,
AC5, AC6, AC7, AC25.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec, its product sidecar and its notes, as of the exploration branch
fkwp/feature/audio_quick_menu_exploration at 39d2ffbf where it reached status
implement, plus the drift log this implementation appended: the drift check
against a fresh main, the re-derived slicing plan, the owner's decision to
land the feature as one PR, and the findings from building it.
The commits that follow are its slices, in the order the plan gives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FEATURES_SPEC/AGENTS.md describes how an exploration is distilled into a
feature spec and how a spec is turned into PRs against a current main; the
product spec template is the sidecar a brainstorming session fills in.
The folder is excluded from oxfmt: a product spec is immutable once a spec
leaves draft, and the process documents are prose in their own style.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codifies what is currently tribal knowledge or only discoverable by reading CI
config: the view model / view contract, the rule that nothing reads the page,
Clean Code ordering, the three test layers, and reuse-before-you-build.
The root AGENTS.md is the always-loaded minimum and routes to docs/agents/, so
an agent loads only the detail its task needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>