/** * PluginsPanel — search, filter, sort and browse the plugin catalog. */ import { useMemo } from 'react'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { inferCapabilities, normalizeCategory } from '@shared/utils/extensionNormalizers'; import { Puzzle, Search } from 'lucide-react'; import { SearchInput } from '../common/SearchInput'; import { CapabilityChips } from './CapabilityChips'; import { CategoryChips } from './CategoryChips'; import { PluginCard } from './PluginCard'; import { PluginDetailDialog } from './PluginDetailDialog'; import type { EnrichedPlugin, PluginCapability, PluginFilters, PluginSortField, } from '@shared/types/extensions'; interface PluginsPanelProps { pluginFilters: PluginFilters; pluginSort: { field: PluginSortField; order: 'asc' | 'desc' }; selectedPluginId: string | null; updatePluginSearch: (search: string) => void; toggleCategory: (category: string) => void; toggleCapability: (capability: PluginCapability) => void; toggleInstalledOnly: () => void; setSelectedPluginId: (id: string | null) => void; clearFilters: () => void; hasActiveFilters: boolean; setPluginSort: (sort: { field: PluginSortField; order: 'asc' | 'desc' }) => void; } const SORT_OPTIONS: { value: string; label: string }[] = [ { value: 'popularity:desc', label: 'Popular' }, { value: 'name:asc', label: 'Name A-Z' }, { value: 'name:desc', label: 'Name Z-A' }, { value: 'category:asc', label: 'Category' }, ]; /** Pure function: filter + sort the catalog */ function selectFilteredPlugins( catalog: EnrichedPlugin[], filters: PluginFilters, sort: { field: PluginSortField; order: 'asc' | 'desc' } ): EnrichedPlugin[] { let result = catalog; // Search if (filters.search) { const q = filters.search.toLowerCase(); result = result.filter( (p) => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q) || p.pluginId.toLowerCase().includes(q) ); } // Categories if (filters.categories.length > 0) { result = result.filter((p) => filters.categories.includes(normalizeCategory(p.category))); } // Capabilities if (filters.capabilities.length > 0) { result = result.filter((p) => { const caps = inferCapabilities(p); return filters.capabilities.some((fc) => caps.includes(fc)); }); } // Installed only if (filters.installedOnly) { result = result.filter((p) => p.isInstalled); } // Sort const direction = sort.order === 'asc' ? 1 : -1; result = [...result].sort((a, b) => { switch (sort.field) { case 'popularity': return (b.installCount - a.installCount) * direction; case 'name': return a.name.localeCompare(b.name) * direction; case 'category': return a.category.localeCompare(b.category) * direction; default: return 0; } }); return result; } export const PluginsPanel = ({ pluginFilters, pluginSort, selectedPluginId, updatePluginSearch, toggleCategory, toggleCapability, toggleInstalledOnly, setSelectedPluginId, clearFilters, hasActiveFilters, setPluginSort, }: PluginsPanelProps): React.JSX.Element => { const catalog = useStore((s) => s.pluginCatalog); const loading = useStore((s) => s.pluginCatalogLoading); const error = useStore((s) => s.pluginCatalogError); const filtered = useMemo( () => selectFilteredPlugins(catalog, pluginFilters, pluginSort), [catalog, pluginFilters, pluginSort] ); const selectedPlugin = useMemo( () => selectedPluginId ? (catalog.find((p) => p.pluginId === selectedPluginId) ?? null) : null, [catalog, selectedPluginId] ); const sortValue = `${pluginSort.field}:${pluginSort.order}`; return (
{/* Search + Sort + Installed only row */}
{/* Filters */}
Categories {hasActiveFilters && ( )}
Capabilities
{/* Result count */} {!loading && !error && filtered.length > 0 && (

{filtered.length} plugin{filtered.length !== 1 ? 's' : ''}

)} {/* Content */} {loading && (
{Array.from({ length: 6 }, (_, i) => (
))}
)} {error && (
{error}
)} {!loading && !error && filtered.length === 0 && (
{hasActiveFilters ? ( ) : ( )}

{hasActiveFilters ? 'No plugins match your filters' : 'No plugins available'}

{hasActiveFilters ? 'Try adjusting your search or filter criteria' : 'Check back later for new plugins'}

{hasActiveFilters && ( )}
)} {!loading && !error && filtered.length > 0 && (
{filtered.map((plugin) => ( ))}
)} {/* Detail dialog */} setSelectedPluginId(null)} />
); };