'use client'; import { useState, useMemo, useCallback, useEffect } from 'react'; import { SoundUnit } from './sound-unit'; import { groupSoundsByCategory, getGroupLabel, getSubTabLabel } from '@/lib/utils'; import { playUISound } from '@/lib/ui-audio'; import type { SoundAsset, SoundAssignments, SelectMode } from '@/lib/types'; interface SoundBrowserPanelProps { sounds: SoundAsset[]; assignments: SoundAssignments; onPreview: (path: string) => void; selectMode: SelectMode | null; onSelectModeAssign: (path: string) => void; onClearSelectMode: () => void; } // Strip pack prefix from category: "publisher/name:sc2/terran" → "sc2/terran" function internalCat(category: string): string { const idx = category.indexOf(':'); return idx === -1 ? category : category.slice(idx + 1); } // Extract pack ID: "publisher/name:sc2/terran" → "publisher/name" function packOfCat(category: string): string { const idx = category.indexOf(':'); return idx === -1 ? '' : category.slice(0, idx); } // "publisher/name" → "name" function packShortName(packId: string): string { return packId.split('/')[1] ?? packId; } export function SoundBrowserPanel({ sounds, assignments, onPreview, selectMode, onSelectModeAssign, onClearSelectMode }: SoundBrowserPanelProps) { const [activePack, setActivePack] = useState(null); const [activeGroup, setActiveGroup] = useState('sc2'); const [activeCategory, setActiveCategory] = useState('sc2/terran'); const [search, setSearch] = useState(''); // All unique pack IDs present in the library const allPacks = useMemo(() => { return [...new Set(sounds.map((s) => packOfCat(s.category)))].filter(Boolean).sort(); }, [sounds]); const showPackSelector = allPacks.length > 1; // Sounds visible given the current pack filter const visibleSounds = useMemo(() => { if (!activePack) return sounds; const prefix = activePack + ':'; return sounds.filter((s) => s.category.startsWith(prefix)); }, [sounds, activePack]); const allGroups = useMemo(() => { return [...new Set(visibleSounds.map((s) => internalCat(s.category).split('/')[0]))].sort(); }, [visibleSounds]); // If activeGroup disappears after a pack switch, reset to the first available group useEffect(() => { if (allGroups.length > 0 && !allGroups.includes(activeGroup)) { setActiveGroup(allGroups[0]); } }, [allGroups, activeGroup]); const groupCategories = useMemo(() => { return [...new Set( visibleSounds.filter((s) => internalCat(s.category).split('/')[0] === activeGroup).map((s) => s.category) )].sort(); }, [visibleSounds, activeGroup]); // If activeCategory doesn't belong to current group, use first category of the group const effectiveCategory = groupCategories.includes(activeCategory) ? activeCategory : (groupCategories[0] ?? ''); const assignedPaths = useMemo(() => { const paths = new Set(); Object.values(assignments.global).forEach((p) => p && paths.add(p)); Object.values(assignments.agents).forEach((a) => { Object.values(a.hooks).forEach((p) => p && paths.add(p)); }); return paths; }, [assignments]); const isSearching = search.trim().length > 0; const filteredSounds = useMemo(() => { if (isSearching) { const q = search.toLowerCase(); return visibleSounds.filter((s) => s.filename.toLowerCase().includes(q) || s.category.toLowerCase().includes(q) || s.subcategory.toLowerCase().includes(q) ); } return visibleSounds.filter((s) => s.category === effectiveCategory); }, [visibleSounds, effectiveCategory, search, isSearching]); const grouped = useMemo(() => groupSoundsByCategory(filteredSounds), [filteredSounds]); const showSubTabs = !isSearching && groupCategories.length > 1; // Show pack label on cards when browsing all packs and multiple packs are installed const showPackBadge = showPackSelector && !activePack; const handleGroupChange = useCallback((group: string) => { setActiveGroup(group); playUISound('pageChange', 0.4); }, []); const handleCategoryChange = useCallback((cat: string) => { setActiveCategory(cat); playUISound('pageChange', 0.35); }, []); const handlePackChange = useCallback((pack: string | null) => { setActivePack(pack); playUISound('pageChange', 0.4); }, []); return (
{/* Header */}
SOUND LIBRARY
{/* Search */} setSearch(e.target.value)} onKeyDown={(e) => e.key === 'Escape' && setSearch('')} className="w-full px-3 py-1 text-xs bg-transparent outline-none" style={{ border: `1px solid ${isSearching ? 'var(--sf-cyan)' : 'var(--sf-border)'}`, color: 'var(--sf-cyan)', boxShadow: isSearching ? '0 0 6px rgba(0,229,255,0.15)' : undefined, }} /> {/* SOURCE pack selector — only shown when 2+ packs installed */} {showPackSelector && (
SOURCE {allPacks.map((id) => ( ))}
)} {/* Group tabs */}
{allGroups.map((group) => ( ))}
{/* Sub-tabs (race / platform) */} {showSubTabs && (
{groupCategories.map((cat) => ( ))}
)}
{/* Select mode banner */} {selectMode && (
ASSIGNING → {selectMode.label} click a sound card to assign
)} {/* Sound grid */}
{isSearching && filteredSounds.length > 0 && (
{filteredSounds.length} result{filteredSounds.length !== 1 ? 's' : ''}
)} {Object.entries(grouped).map(([cat, subcats]) => (
{isSearching && (
{internalCat(cat).replace(/\//g, ' › ')}
)} {Object.entries(subcats).map(([subcat, catSounds]) => (
{subcat &&
{subcat.replace(/-/g, ' ')}
}
{catSounds.map((sound) => ( onSelectModeAssign(sound.path) : undefined} packLabel={showPackBadge ? packShortName(packOfCat(sound.category)) : undefined} /> ))}
))}
))} {filteredSounds.length === 0 && (
NO SOUNDS FOUND
)}
); }