import { useMemo, useState } from 'react'; import { useT } from '../i18n'; import type { Dict } from '../i18n/types'; import type { DesignSystemSummary, Surface } from '../types'; import { Icon } from './Icon'; interface Props { systems: DesignSystemSummary[]; selectedId: string | null; onSelect: (id: string) => void; onPreview: (id: string) => void; } type SurfaceFilter = 'all' | Surface; const SURFACE_PILLS: { value: SurfaceFilter; labelKey: keyof Dict; icon: 'grid' | 'image' | 'video' | 'music' | null }[] = [ { value: 'all', labelKey: 'common.all', icon: null }, { value: 'web', labelKey: 'ds.surfaceWeb', icon: 'grid' }, { value: 'image', labelKey: 'ds.surfaceImage', icon: 'image' }, { value: 'video', labelKey: 'ds.surfaceVideo', icon: 'video' }, { value: 'audio', labelKey: 'ds.surfaceAudio', icon: 'music' }, ]; function surfaceOf(system: DesignSystemSummary): Surface { return system.surface ?? 'web'; } const CATEGORY_ORDER = [ 'Starter', 'AI & LLM', 'Developer Tools', 'Productivity & SaaS', 'Backend & Data', 'Design & Creative', 'Fintech & Crypto', 'E-Commerce & Retail', 'Media & Consumer', 'Automotive', ]; export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview }: Props) { const t = useT(); const [filter, setFilter] = useState(''); const [category, setCategory] = useState('All'); const [surfaceFilter, setSurfaceFilter] = useState('all'); // Pre-scope by surface so the category dropdown only lists categories // that exist within the active surface — avoids ghost options that // would yield zero rows. const surfaceScoped = useMemo( () => surfaceFilter === 'all' ? systems : systems.filter((s) => surfaceOf(s) === surfaceFilter), [systems, surfaceFilter], ); const surfaceCounts = useMemo(() => { const counts: Record = { all: systems.length, web: 0, image: 0, video: 0, audio: 0, }; for (const s of systems) counts[surfaceOf(s)]++; return counts; }, [systems]); const categories = useMemo(() => { const cats = new Set(); for (const s of surfaceScoped) cats.add(s.category || 'Uncategorized'); const ordered: string[] = []; for (const c of CATEGORY_ORDER) if (cats.has(c)) ordered.push(c); for (const c of [...cats].sort()) if (!ordered.includes(c)) ordered.push(c); return ['All', ...ordered]; }, [surfaceScoped]); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); return surfaceScoped.filter((s) => { if (category !== 'All' && (s.category || 'Uncategorized') !== category) return false; if (!q) return true; return ( s.title.toLowerCase().includes(q) || s.summary.toLowerCase().includes(q) ); }); }, [surfaceScoped, filter, category]); // The category metadata coming from each design system is authored in // English. We translate the well-known buckets (All / Uncategorized) but // pass the rest through unchanged so user-facing labels stay aligned with // the underlying tags. const renderCategory = (c: string) => { if (c === 'All') return t('ds.categoryAll'); if (c === 'Uncategorized') return t('ds.categoryUncategorized'); return c; }; return (
{t('ds.surfaceLabel')} {SURFACE_PILLS.map((p) => ( ))}
setFilter(e.target.value)} />
{filtered.length === 0 ? (
{t('ds.emptyNoMatch')}
) : (
{filtered.map((s) => { const active = s.id === selectedId; return (
onSelect(s.id)} >
{s.title} {active ? ( {t('ds.badgeDefault')} ) : null}
{s.summary || s.category}
{s.swatches && s.swatches.length > 0 ? (
{s.swatches.map((c, i) => ( ))}
) : null}
); })}
)}
); }