import React, { useEffect, useMemo, useState } from 'react'; import { THEME, tempColor, pHColor, saltColor } from '../theme.js'; import { MediaConfBar, OxygenConfArc, IntervalBar, MonoTag, SourceBadge } from './Primitives.jsx'; function ModeStrip({ mode, setMode }) { const focused = mode === 'focused'; return (
); } const FILTERS = [ { id: 'all', label: 'All', test: () => true }, { id: 'thermo', label: 'Thermophiles', sub: '>55°C', test: (m) => m.T_opt > 55 }, { id: 'psychro', label: 'Psychrophiles', sub: '<15°C', test: (m) => m.T_opt < 15 }, { id: 'anaerobe', label: 'Anaerobes', test: (m) => (m.O2 || '').toLowerCase().includes('anaerobe') }, { id: 'halo', label: 'Halotolerant', sub: '>3% NaCl', test: (m) => m.salt > 3 }, ]; function QuickFilters({ active, setActive, count }) { return (
{FILTERS.map((f) => ( ))}
showing {count} candidates · sorted by top-medium confidence
); } function PhenoMicro({ label, value, unit = '', color }) { // Synthetic interval: ±5% of scale to show the primitive without per-row data // (Catalog has point estimates only; real intervals show in Predict + Detail) return (
{label} {value}{unit}
); } function FeaturedCard({ m, onSelect }) { return (
onSelect(m)} style={{ background: THEME.paper, border: `1px solid ${THEME.rule}`, padding: '16px 18px', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 12, transition: 'border-color 120ms', }} onMouseEnter={(e) => (e.currentTarget.style.borderColor = THEME.ink)} onMouseLeave={(e) => (e.currentTarget.style.borderColor = THEME.rule)}>
{m.accession} · {m.phylum}
{m.name}
O₂
{m.O2}
Try this medium
{m.top_medium_id}
{m.top_medium_name}
); } function TableRow({ m, onSelect, isLast }) { return ( onSelect(m)} style={{ cursor: 'pointer', borderBottom: isLast ? 'none' : `1px solid ${THEME.ruleSoft}`, }} onMouseEnter={(e) => (e.currentTarget.style.background = '#ede5cd')} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}> {m.accession} {m.name} {m.phylum} {m.top_medium_id} {m.top_medium_name} {m.T_opt.toFixed(0)}°C {m.pH.toFixed(1)}
{m.O2}
{m.salt.toFixed(1)}% {m.completeness.toFixed(0)} ); } export default function Catalog({ data, onSelect }) { const [mode, setMode] = useState('focused'); const [filter, setFilter] = useState('all'); const [search, setSearch] = useState(''); const filtered = useMemo(() => { let list = data || []; if (mode === 'focused') list = list.filter((m) => m.truly_uncultured); if (search) { const q = search.toLowerCase(); list = list.filter((m) => m.name.toLowerCase().includes(q) || m.accession.toLowerCase().includes(q)); } const f = FILTERS.find((x) => x.id === filter)?.test || (() => true); list = list.filter(f); return [...list].sort((a, b) => b.top_confidence - a.top_confidence); }, [data, mode, filter, search]); const featured = filtered.slice(0, 6); const rest = filtered.slice(6, 86); return (
setSearch(e.target.value)} placeholder="search organism name…" style={{ flex: 1, border: 'none', background: 'transparent', outline: 'none', font: `400 13px ${THEME.font}`, color: THEME.ink }} />
{featured.length > 0 && (
Top {featured.length} picks by media confidence
{featured.map((m) => )}
)} {rest.length > 0 && (
Remaining {filtered.length - 6}
{['Accession', 'Organism', 'Phylum', 'Try this medium', 'Conf.', 'T', 'pH', 'O₂', 'Salt', 'CheckM'].map((h) => ( ))} {rest.map((m, i) => )}
{h}
{filtered.length > 86 && (
showing first 80 of {filtered.length - 6} remaining · use search and filters to narrow
)}
)}
); }