// Filter sidebar: status, priority, due, tags + scope. function FilterPanel({ filters, setFilters, tagsList, counts }) { const toggle = (key, val) => { const cur = new Set(filters[key] || []); cur.has(val) ? cur.delete(val) : cur.add(val); setFilters({ ...filters, [key]: [...cur] }); }; const isActive = (key, val) => (filters[key] || []).includes(val); return ( ); } // ---- Tag filter with autocomplete (handles hundreds of tags) ---- function TagFilter({ tagsList, selected, onToggle }) { const [q, setQ] = React.useState(""); const [focused, setFocused] = React.useState(false); const inputRef = React.useRef(null); if (!tagsList.length) return null; const ql = q.trim().toLowerCase(); const filtered = ql ? tagsList.filter(t => t.toLowerCase().includes(ql)).slice(0, 50) : tagsList.slice(0, 12); // top dozen by frequency when not searching const exact = ql && tagsList.includes(ql); const TOTAL = tagsList.length; return (
Tags {TOTAL}
{/* Selected tags chips (always visible if any) */} {selected.length > 0 && (
{selected.map(t => ( ))}
)} {/* Search field */}
setQ(e.target.value)} onFocus={() => setFocused(true)} onBlur={() => setTimeout(() => setFocused(false), 120)} onKeyDown={(e) => { if (e.key === "Enter" && filtered.length > 0) { onToggle(filtered[0]); setQ(""); } else if (e.key === "Escape") { setQ(""); e.currentTarget.blur(); } }} /> {q && }
{/* Results / suggestions */}
{filtered.length === 0 ? (
No tags match "{q}"
) : ( <> {filtered.map(t => { const isSelected = selected.includes(t); return ( ); })} {!ql && tagsList.length > 12 && ( +{tagsList.length - 12} more — type to find )} )}
); } function TagHighlight({ text, match }) { const i = text.toLowerCase().indexOf(match); if (i < 0) return `#${text}`; return ( <> #{text.slice(0, i)}{text.slice(i, i + match.length)}{text.slice(i + match.length)} ); } window.FilterPanel = FilterPanel;