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>
72 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|