feat(ui): compact, tooltip-driven pod control toolbar

Collapse the five full-width control buttons (audio, mic, background
music, test voice, share) into a compact icon pill with hover/focus
tooltips, plus a labelled Share primary CTA. Row now flex-wraps instead
of a fixed 2-col grid, so it stays on one line on desktop and wraps
cleanly on narrow screens. State is encoded visually (filled = engaged,
ghost = idle, primary = needs action). Background music gets a distinct
Music2 icon instead of reusing the audio Volume2 icon. Handlers are
unchanged — presentation only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-28 10:44:07 -07:00
parent faa9769470
commit 63a411490c
+100 -41
View File
@@ -14,6 +14,7 @@ import {
MicIcon, MicIcon,
MicOffIcon, MicOffIcon,
MonitorUpIcon, MonitorUpIcon,
Music2Icon,
PhoneCallIcon, PhoneCallIcon,
PhoneOffIcon, PhoneOffIcon,
PanelLeftIcon, PanelLeftIcon,
@@ -26,6 +27,7 @@ import {
VolumeXIcon, VolumeXIcon,
WorkflowIcon, WorkflowIcon,
XIcon, XIcon,
type LucideIcon,
} from 'lucide-react'; } from 'lucide-react';
import type { Room, RemoteTrack, RemoteTrackPublication } from 'livekit-client'; import type { Room, RemoteTrack, RemoteTrackPublication } from 'livekit-client';
import type { import type {
@@ -637,47 +639,61 @@ export function PodView({
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center"> <div className="flex flex-wrap items-center gap-2">
<Button <div className="flex items-center gap-0.5 rounded-xl border bg-card/80 p-1 shadow-sm backdrop-blur-sm">
variant={audioBlocked ? 'default' : 'outline'} <PodControlButton
onClick={() => void enableSound()} label={audioBlocked ? 'Enable audio' : 'Audio on'}
disabled={!room} icon={audioBlocked ? VolumeXIcon : Volume2Icon}
> onClick={() => void enableSound()}
{audioBlocked ? ( disabled={!room}
<VolumeXIcon data-icon="inline-start" /> active={!audioBlocked}
) : ( attention={audioBlocked}
<Volume2Icon data-icon="inline-start" /> />
)} <PodControlButton
{audioBlocked ? 'Enable audio' : 'Audio on'} label={micOn ? 'Mic on' : 'Enable mic'}
</Button> icon={micOn ? MicIcon : MicOffIcon}
<Button onClick={() => void toggleMic()}
variant={micOn ? 'default' : 'outline'} disabled={!room}
onClick={() => void toggleMic()} active={micOn}
disabled={!room} />
> <PodControlButton
{micOn ? ( label={
<MicIcon data-icon="inline-start" /> beat.on
) : ( ? beat.mine
<MicOffIcon data-icon="inline-start" /> ? 'Stop background music'
)} : `Stop music (${beat.by})`
{micOn ? 'Mic on' : 'Enable mic'} : 'Background music'
</Button> }
<Button variant="outline" onClick={onToggleBeat} disabled={!room}> icon={Music2Icon}
<Volume2Icon data-icon="inline-start" /> onClick={onToggleBeat}
{beat.on ? (beat.mine ? 'Stop music' : `Stop (${beat.by})`) : 'Background Music'} disabled={!room}
</Button> active={beat.on}
<Button />
variant="outline" <PodControlButton
onClick={() => void playPodManVoiceTest()} label={testingVoice ? 'Sending voice…' : 'Test PodMan voice'}
disabled={!room || testingVoice} icon={RadioTowerIcon}
> onClick={() => void playPodManVoiceTest()}
<RadioTowerIcon data-icon="inline-start" /> disabled={!room || testingVoice}
{testingVoice ? 'Sending voice' : 'Test PodMan voice'} active={testingVoice}
</Button> busy={testingVoice}
<Button onClick={toggleScreen} disabled={!room}> />
<MonitorUpIcon data-icon="inline-start" /> </div>
{sharing ? 'Stop sharing' : 'Share screen'}
</Button> <Tooltip>
<TooltipTrigger asChild>
<Button
onClick={toggleScreen}
disabled={!room}
variant={sharing ? 'destructive' : 'default'}
>
<MonitorUpIcon data-icon="inline-start" />
{sharing ? 'Stop sharing' : 'Share screen'}
</Button>
</TooltipTrigger>
<TooltipContent>
{sharing ? 'Stop sharing your screen' : 'Share your screen with PodMan'}
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</section> </section>
@@ -1693,3 +1709,46 @@ function initials(name: string): string {
.slice(0, 2) .slice(0, 2)
.toUpperCase(); .toUpperCase();
} }
/**
* Compact, icon-only control for the pod toolbar. The label is hidden by
* default and surfaced on hover/focus via tooltip, so the row stays tight and
* responsive. `active` fills the button (engaged state), `attention` promotes
* it to the primary colour (needs a user action, e.g. audio is blocked).
*/
function PodControlButton({
label,
icon: Icon,
onClick,
disabled,
active = false,
attention = false,
busy = false,
}: {
label: string;
icon: LucideIcon;
onClick: () => void;
disabled?: boolean;
active?: boolean;
attention?: boolean;
busy?: boolean;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant={attention ? 'default' : active ? 'secondary' : 'ghost'}
aria-pressed={active}
aria-label={label}
onClick={onClick}
disabled={disabled}
>
<Icon className={cn(busy && 'animate-pulse')} />
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
);
}