nexus/ui/src/components/frame/GlobalMicButton.tsx
Nexus Dev 45d2a9ff24 refactor(nexus): wire frame buttons to phase 14 contexts
GlobalMicButton drops the state and onClick scaffolding props from
phase 8 and now consumes VoiceContext directly — click calls
toggleListening(). It also reflects hasQueuedVoice via the idle dot
color and an updated aria-label so the assistant-bound queue is
observable from any route.

CmdKButton replaces the synthetic Meta+K keydown shim with a direct
setOpen(true) call on the CommandPaletteContext. The shim comment
block is gone; no more document dispatch side effects on click.

Tests updated to render inside their respective providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:23:04 +00:00

80 lines
2.5 KiB
TypeScript

import { cn } from "@/lib/utils";
import { useVoice } from "../../context/VoiceContext";
/**
* GlobalMicButton — Phase 14 wiring of the top-strip voice affordance.
*
* Consumes `VoiceContext` directly; the `state` and `onClick` scaffolding
* props from Phase 8 are gone. Click cycles through
* idle → listening → (on stop) speaking → idle
* via `VoiceContext.toggleListening()`.
*
* Per docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2 the three visual
* states are:
* - idle: forest-green dot, no animation
* - listening: volt fill, 1.5s pulse loop + expanding volt ring
* - speaking: silver fill, no pulse
*
* Literal hex `#166534` (forest) and `#a0a0a0` (silver) are used because
* MIGRATION-PLAN.md §3 proposes these as new CSS variables but has not yet
* shipped them. Volt uses the `text-primary`/`bg-primary` semantic token.
*/
export function GlobalMicButton() {
const { state, toggleListening, hasQueuedVoice } = useVoice();
const label =
state === "listening"
? "Voice — listening"
: state === "speaking"
? "Voice — processing"
: hasQueuedVoice
? "Voice (queued)"
: "Voice";
return (
<button
type="button"
onClick={() => {
void toggleListening();
}}
aria-label={label}
title={label}
data-state={state}
data-queued={hasQueuedVoice ? "true" : undefined}
className={cn(
"relative inline-flex h-8 w-8 items-center justify-center rounded-[8px]",
"border border-border bg-card transition-colors",
"hover:border-primary",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
)}
>
{state === "idle" && (
<span
aria-hidden="true"
className={cn(
"h-2 w-2 rounded-full",
hasQueuedVoice ? "bg-primary" : "bg-[#166534]",
)}
/>
)}
{state === "listening" && (
<>
<span
aria-hidden="true"
className="h-2 w-2 rounded-full bg-primary animate-pulse"
/>
<span
aria-hidden="true"
className="absolute inset-0 rounded-[8px] border-2 border-primary opacity-60 animate-ping"
/>
</>
)}
{state === "speaking" && (
<span
aria-hidden="true"
className="h-2 w-2 rounded-full bg-[#a0a0a0]"
/>
)}
</button>
);
}