// 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;