nexus/ui/src/components/frame/GlobalMicButton.tsx
Nexus Dev bfcdf1f598 feat(nexus): add GlobalMicButton scaffold for layout overhaul (phase 8)
Visual-only mic button for the top strip per
docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2. Renders three
specified states (idle / listening / speaking) but Phase 8 only
wires the idle state functionally. Phase 14 will toggle the state
prop from the voice pipeline without changing this component's
signature.

Uses text-primary/bg-primary for volt (already migrated in phases
1-3) and literal #166534 / #a0a0a0 for forest and silver, which
MIGRATION-PLAN.md §3 proposes as new semantic tokens that have not
yet shipped.

Part of Phase 8 of the Nexus layout overhaul (task 4 of 7).

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

72 lines
2.3 KiB
TypeScript

import { cn } from "@/lib/utils";
export type GlobalMicState = "idle" | "listening" | "speaking";
interface GlobalMicButtonProps {
state?: GlobalMicState;
/**
* Phase 14 will wire this to the voice pipeline. In Phase 8 it's a no-op
* by default; callers can override for manual testing.
*/
onClick?: () => void;
}
/**
* GlobalMicButton — Phase 8 visual-only mic button for the top strip.
*
* Per docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2, three states are
* specified:
* - idle: forest-green dot, no animation
* - listening: volt fill, 1.5s pulse loop + expanding volt ring
* - speaking: silver fill, no pulse
*
* Phase 8 renders all three but only `idle` is wired up functionally. The
* listening / speaking visuals are scaffolded so Phase 14 can toggle the
* state prop without changing this component's signature.
*
* 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({ state = "idle", onClick }: GlobalMicButtonProps) {
return (
<button
type="button"
onClick={onClick}
aria-label="Voice"
title="Voice (Phase 14 — not yet wired)"
data-state={state}
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="h-2 w-2 rounded-full 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>
);
}