feat(catalog): searchable tag filter in global catalog overlay
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user