feat(catalog): searchable tag filter in global catalog overlay
All checks were successful
CI / e2e (push) Has been skipped
CI / ci (push) Successful in 1m56s
CI / deploy (push) Successful in 16s

Adds a typing-to-filter input above the tag chip list in the filter
sidebar, rendered only when there are more than eight tags. Case-
insensitive substring match; shows "No tags match" when the query
empties the list. Selected tags filtered out of the sidebar remain
active as header pills. Resets with the rest of overlay state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 13:52:47 +02:00
parent 7890de141e
commit d9ec330aca

View File

@@ -23,6 +23,7 @@ export function CatalogSearchOverlay() {
const [debouncedQuery, setDebouncedQuery] = useState("");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [filterOpen, setFilterOpen] = useState(false);
const [tagSearch, setTagSearch] = useState("");
const [viewMode, setViewMode] = useState<ViewMode>("grid");
const [manualEntryMode, setManualEntryMode] = useState(false);
const [savedItemName, setSavedItemName] = useState<string | null>(null);
@@ -87,6 +88,7 @@ export function CatalogSearchOverlay() {
setDebouncedQuery("");
setSelectedTags([]);
setFilterOpen(false);
setTagSearch("");
setWeightMin(0);
setWeightMax(5000);
setPriceMin(0);
@@ -334,24 +336,48 @@ export function CatalogSearchOverlay() {
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">
Tags
</h3>
{tags.length > 8 && (
<input
type="text"
value={tagSearch}
onChange={(e) => setTagSearch(e.target.value)}
placeholder="Filter tags..."
className="w-full px-2 py-1 mb-2 border border-gray-200 rounded-md text-xs text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-300 focus:border-transparent transition-colors"
/>
)}
<div className="space-y-1">
{tags.map((tag) => {
const isActive = selectedTags.includes(tag.name);
return (
<button
key={tag.id}
type="button"
onClick={() => toggleTag(tag.name)}
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-sm transition-colors ${
isActive
? "bg-blue-50 text-blue-700 font-medium"
: "text-gray-600 hover:bg-gray-50"
}`}
>
{tag.name}
</button>
);
})}
{(() => {
const q = tagSearch.trim().toLowerCase();
const filteredTags = q
? tags.filter((t) =>
t.name.toLowerCase().includes(q),
)
: tags;
if (filteredTags.length === 0) {
return (
<p className="text-xs text-gray-400 px-2.5 py-1.5">
No tags match
</p>
);
}
return filteredTags.map((tag) => {
const isActive = selectedTags.includes(tag.name);
return (
<button
key={tag.id}
type="button"
onClick={() => toggleTag(tag.name)}
className={`w-full text-left px-2.5 py-1.5 rounded-lg text-sm transition-colors ${
isActive
? "bg-blue-50 text-blue-700 font-medium"
: "text-gray-600 hover:bg-gray-50"
}`}
>
{tag.name}
</button>
);
});
})()}
</div>
</div>