// OVE — sticky app header: logo, audio source controls (segmented), the // demo-track chip, What's New!, theme tick switch, BPM readout and the beat // indicator. (v7.6.1: the Set Beat button moved out — tap-tempo lives in the // Beat tab, and Space works anywhere.) 'use client' import { useRef, useSyncExternalStore } from 'react' import { AudioWaveform, Disc3, FileAudio, Link2, Mic, Moon, Music2, Square, Sun } from 'lucide-react' import { useTheme } from 'next-themes' import { Switch } from '@/components/ui/switch' import { BeatDot } from '@/components/ava/beat-dot' import { LinkDialog } from '@/components/ava/link-dialog' import { WhatsNew } from '@/components/ava/whats-new-dialog' import { useAudioSource } from '@/components/ava/audio-source' import { useAvaStore } from '@/lib/store' import { getStoredDarkBase } from '@/lib/theme' import { APP_NAME, APP_SHORT } from '@/lib/version' import { cn } from '@/lib/utils' function SourceSegmentButton({ active, onClick, icon: Icon, label, title, tone = 'default', }: { active: boolean onClick: () => void icon: React.ComponentType<{ className?: string }> label: string title: string tone?: 'default' | 'stop' }) { return ( ) } function SourceControls() { const source = useAvaStore((s) => s.source) const sourceTitle = useAvaStore((s) => s.sourceTitle) const openLinkDialog = useAvaStore((s) => s.setLinkDialogOpen) const { startDemo, startMic, startFile, stop } = useAudioSource() const fileInputRef = useRef(null) return (
void startDemo()} icon={Disc3} label="Demo" title="Built-in demo track — Neural Mecha Pulse (mastered industrial EDM, plays once through)" /> void startMic()} icon={Mic} label="Mic" title="Live microphone / line-in" /> fileInputRef.current?.click()} icon={FileAudio} label="File" title="Play an audio file (loops)" /> openLinkDialog(true)} icon={Link2} label="Link" title="Stream from a link (YouTube, SoundCloud, direct MP3…)" />
{source === 'demo' && (
{sourceTitle ?? 'Demo track'}
)} { const file = e.target.files?.[0] if (file) void startFile(file) e.target.value = '' }} />
) } function BpmReadout() { const bpm = useAvaStore((s) => s.stats?.beat.bpm ?? 0) return (
{bpm > 0 ? bpm.toFixed(1) : '—'}
BPM
) } /** * Light-switch tick: left position (unchecked) keeps the dark theme, flipping * right (checked) switches the page to the white theme. The dark side is * whichever dark base the visitor picked on the Settings tab (royal purple * by default); both choices persist per visitor. The useSyncExternalStore * mount guard returns false on the server / first client render and true * after hydration, so the checkbox never causes a hydration mismatch. */ const emptySubscribe = () => () => {} function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme() const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false) const isLight = mounted && resolvedTheme === 'light' return (
setTheme(v ? 'light' : getStoredDarkBase())} aria-label={isLight ? 'Switch to dark theme' : 'Switch to light theme'} className="data-[state=checked]:bg-emerald-400 data-[state=unchecked]:bg-zinc-600" />
) } export function AppHeader() { return (
{/* logo */}

{APP_SHORT}

{APP_NAME.toUpperCase()}

{/* source controls — full row on mobile, after logo on desktop */}
{/* what's new + theme switch + BPM readout + beat indicator — always visible */}
) }