Skip to content

feat(voice): MicButton, VoiceWaveform, DictationStatusLine and MicPermissionDialog (CL-8480) - #44

Merged
TheGreatAxios merged 7 commits into
mainfrom
cl-8480-voice-family
Sep 21, 2026
Merged

TheGreatAxios merged 7 commits into
mainfrom
cl-8480-voice-family

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

  • Ports the dictation UI from solutions-builder-alpha's dictation.tsx into four presentational, controlled components: MicButton (mic toggle, aria-pressed, green while listening), VoiceWaveform (bar waveform driven by a levels prop), DictationStatusLine (the "Listening:"/starting/denied status row), and MicPermissionDialog (the settings-nudge dialog for a denied permission).
  • No SpeechRecognition, Tauri, or media APIs in react-ui — each component takes state/levels/callbacks only; MicButton's JSDoc documents the expected consumer-side dictation hook shape.
  • Reduced motion for the waveform's transform transition goes through usePrefersReducedMotion.
  • Stories added under stories/primitives/; components picked up by the generated barrel/exports on bun run build.

Test plan

  • bun run typecheck
  • bun run lint
  • bun test
  • bun run build (contrast gate passes, no theme.css changes)
  • bun run dep-guard

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review

Verdict: request changes. Presentational mic family; no capture APIs. No CI. No prior reviews.

MicButton JSDoc says disabled while permission is refused. Code: unusable = disabled || state === unsupported. denied stays enabled and still fires onToggle. VoiceWaveform JSDoc claims aria-hidden; the span does not set it.

A fix for the denied-vs-disabled contract is in flight on this branch.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Verdict: approve. SHA 8730fde.

denied is unusable (disabled, no onToggle). VoiceWaveform has aria-hidden. Tests cover idle/unsupported/denied. bun test 340 pass, build 0.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review (Draper + Emil)

Verdict: request changes (dead-end denied + listening is color-only).

Presentational split (no SpeechRecognition in the library) is right.

  • denied is currently disabled (from an earlier JSDoc-vs-code fix). That leaves a grey mic with no way to reopen the permission dialog. Disable only unsupported. denied stays clickable and fires onToggle; aria-label should say why.
  • Listening is bg-success only; glyph stays Mic. Colour is never the only channel. Swap glyph (or stop square) while listening; don’t paint starting as listening.
  • One live-mic accent (success vs orange waveform). Library copy “Starting the microphone… If asked, allow it.” is overbearing.

Denied-click + label in flight. Glyph if cheap.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Denied is clickable again (a04d3c2); unsupported disabled; listening uses Square; starting is not painted as listening. Same Toaster leftovers as #43 until #42 isolation is on the branch.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanest of the batch. The presentational-only dictation seam is exactly right — no SpeechRecognition or media-device code in the library, consumer owns capture. usePrefersReducedMotion used properly in VoiceWaveform, the DictationState union is tight, aria-pressed / role="alert" are correct, tests included.

Nit: title={label} on a disabled button won't reliably show as a tooltip — harmless to leave. Merge.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Toaster isolation; startingLabel “Starting microphone…”; listening waveform text-success. SHA 0323cd9. bun test 343 pass.

Treat denied as unusable so the control matches its JSDoc and onToggle
does not fire. Set aria-hidden on VoiceWaveform as documented.
unsupported remains disabled. denied fires onToggle so the host can
open MicPermissionDialog, with a blocked aria-label. Listening uses a
stop square; starting no longer shares the listening chrome.
Sonner replays active toasts onto the next Toaster subscriber, so leftover
confirmations from a prior case stacked (Expected 1, Received 2).
Default startingLabel is "Starting microphone…". Listening waveform uses
text-success so it matches MicButton instead of primary-emphasis orange.
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review pass — rebased onto post-prune main + fixes applied

Rebased after the prune (#55): generated exports regenerated. Verified as a real extraction — this family mirrors the dictation UI hand-rolled in solutions-builder (apps/web/src/dictation.tsx, apps/desktop/src/dictation.rs), so it is consumer-backed, not speculative.

Persona review findings — all resolved

  • Focus theft (greybeard): tapping the mic blurred whatever field was committing on blur. onMouseDown now calls preventDefault() so focus stays put.
  • Dishonest states (greybeard): unsupported now says "Dictation unavailable" instead of a generic action label; denied renders MicOff with aria-haspopup="dialog" and no aria-pressed (it opens a permission dialog, not a toggle); the ineffective title attribute is gone.
  • Dead hook (greybeard): VoiceWaveform dropped usePrefersReducedMotion — it's a CSS transition, already covered by the theme-level reduced-motion rule.
  • Hardcoded strings (emil): startingLabel/listeningLabel/errorMessage are all props now.
  • Token discipline (draper): waveform uses text-ok (text-rated), not a fill token.
  • Tests: denied dialog semantics, unsupported label, mousedown default-prevention, denied alert announcement, token class — 11 tests. CHANGELOG entry added; no capture APIs are imported — presentational only.

Validation

232 tests, typecheck, lint, dep-guard, full build, contrast gate — all green at fcca5d9.

@TheGreatAxios
TheGreatAxios merged commit b020b6f into main Sep 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant