From 7890de141ee6aa92a079b67746b83e6b4fc55171 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 23 Apr 2026 13:46:59 +0200 Subject: [PATCH] docs(plan): tag selector search implementation Six-step inline plan per the approved spec. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-04-23-tag-selector-search.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-23-tag-selector-search.md diff --git a/docs/superpowers/plans/2026-04-23-tag-selector-search.md b/docs/superpowers/plans/2026-04-23-tag-selector-search.md new file mode 100644 index 0000000..3ee9e61 --- /dev/null +++ b/docs/superpowers/plans/2026-04-23-tag-selector-search.md @@ -0,0 +1,235 @@ +# Tag Selector Search Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a typing-to-filter input above the tag chip list in the filter sidebar of `CatalogSearchOverlay` so users with many tags can narrow the visible list. + +**Architecture:** Single-file UI change. Local `tagSearch` state, computed `filteredTags` derived via case-insensitive substring match, conditionally rendered input (only when `tags.length > 8`), muted "No tags match" hint when the filter empties the list, reset on overlay close. No backend, no new files, no new dependencies. + +**Tech Stack:** React 19 + TanStack Query (via existing `useTags`) + Tailwind CSS v4. + +**Spec:** `docs/superpowers/specs/2026-04-23-tag-selector-search-design.md` + +--- + +## File Structure + +**Modify only:** +- `src/client/components/CatalogSearchOverlay.tsx` — add state, reset hook extension, and JSX changes inside the Tags section of the filter sidebar. + +No new files. + +--- + +## Task 1: Implement tag search in the filter sidebar + +**Files:** +- Modify: `src/client/components/CatalogSearchOverlay.tsx` + +### Step 1: Add `tagSearch` local state + +- [ ] Add the following state declaration immediately after the existing `const [filterOpen, setFilterOpen] = useState(false);` (currently line 25). This keeps filter-panel state grouped together. + +```tsx +const [tagSearch, setTagSearch] = useState(""); +``` + +### Step 2: Extend the close-reset effect to reset `tagSearch` + +- [ ] In the existing reset `useEffect` (currently lines 84–98), add `setTagSearch("");` to the list of resets so the filter clears on overlay close/reopen. The updated effect looks like: + +```tsx +// Reset state when overlay closes +useEffect(() => { + if (!catalogSearchOpen) { + setSearchInput(""); + setDebouncedQuery(""); + setSelectedTags([]); + setFilterOpen(false); + setTagSearch(""); + setWeightMin(0); + setWeightMax(5000); + setPriceMin(0); + setPriceMax(100000); + setManualEntryMode(false); + setSavedItemName(null); + setCatalogSubmitted(false); + } +}, [catalogSearchOpen]); +``` + +### Step 3: Replace the Tags section JSX with search-input + filtered list + empty hint + +- [ ] In the filter sidebar, the Tags section currently spans lines 333–356. Replace the existing block with the following. This (a) conditionally renders a small filter input only when `tags.length > 8`, (b) computes a case-insensitive substring filter inline, (c) renders either the filtered chip list or a muted "No tags match" hint. + +Current block to replace (exact): + +```tsx +{/* Tags */} +
+

+ Tags +

+
+ {tags.map((tag) => { + const isActive = selectedTags.includes(tag.name); + return ( + + ); + })} +
+
+``` + +Replacement block: + +```tsx +{/* Tags */} +
+

+ Tags +

+ {tags.length > 8 && ( + 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" + /> + )} +
+ {(() => { + const q = tagSearch.trim().toLowerCase(); + const filteredTags = q + ? tags.filter((t) => t.name.toLowerCase().includes(q)) + : tags; + if (filteredTags.length === 0) { + return ( +

+ No tags match +

+ ); + } + return filteredTags.map((tag) => { + const isActive = selectedTags.includes(tag.name); + return ( + + ); + }); + })()} +
+
+``` + +**Notes:** +- The `(() => { ... })()` IIFE keeps the filter logic local to this block without adding another variable at component top-level. It runs on every render, which is fine — cost is O(n) per keystroke and `tags` is small. +- `q` is trimmed-and-lowercased once so we don't recompute it per tag. +- When `q` is empty, we use `tags` directly to avoid the filter allocation. +- The filter input is only rendered when `tags.length > 8`; below that threshold, the old behavior (no input, no filtering) is preserved. + +### Step 4: Run the linter + +- [ ] Run: + +```bash +bun run lint +``` + +Expected: exit 0 with no errors. Biome should be happy (tabs, double quotes, organized imports — existing conventions). + +If there are complaints about the IIFE or `q` variable, resolve them before continuing. If Biome flags the `no-nested-ternary` or complexity rules, hoist the filter logic into a `useMemo` just above the return statement instead — functionally equivalent, syntactically flatter. + +### Step 5: Start the dev server and verify in a browser + +- [ ] Run (in a separate terminal or background): + +```bash +bun run dev +``` + +- [ ] Open `http://localhost:5173`, sign in if required, and open the catalog search overlay (FAB or top-nav search). Toggle the Filter icon to open the sidebar. + +- [ ] Walk through acceptance criteria from the spec: + + 1. With >8 tags, the "Filter tags..." input appears above the chip list. With ≤8 tags, the input is hidden. + 2. Typing narrows the list immediately (case-insensitive substring). + 3. Selecting a tag, then typing a query that excludes it: the chip disappears from the sidebar but the blue pill in the header row remains. + 4. Typing a query that matches nothing shows "No tags match". + 5. Closing the overlay (Esc / back arrow / clicking outside header) and reopening: the tag search input is empty. + 6. Weight/price range filters, active filter pills, and Clear-all still work as before. + +- [ ] If any criterion fails, fix inline and re-verify. Do not commit broken work. + +### Step 6: Commit + +- [ ] Stage and commit: + +```bash +git add src/client/components/CatalogSearchOverlay.tsx +git commit -m "$(cat <<'EOF' +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) +EOF +)" +``` + +--- + +## Self-Review + +**Spec coverage:** + +| Spec requirement | Task step | +|---|---| +| Add `tagSearch` local state | Task 1 Step 1 | +| Reset `tagSearch` on overlay close | Task 1 Step 2 | +| Render input only when `tags.length > 8` | Task 1 Step 3 (conditional) | +| Case-insensitive substring match | Task 1 Step 3 (`q`/`includes`) | +| Selected tag filtered out stays active via header pill | Existing behavior preserved (no code removed from header pill row) — verified in Step 5 criterion 3 | +| "No tags match" hint on zero results | Task 1 Step 3 (early return) | +| Placeholder `Filter tags...` | Task 1 Step 3 | +| Style matches minimalist look, narrower for sidebar | Task 1 Step 3 (smaller padding, ring-1, text-xs) | +| Closing and reopening shows empty input | Task 1 Step 2 + Step 5 criterion 5 | +| No regressions to weight/price/pills | Step 5 criterion 6 | + +All spec requirements mapped. + +**Placeholders:** none. No "TBD" or "handle edge cases" language; all code is shown in full. + +**Type consistency:** `tagSearch` is a `string`, always used as `tagSearch.trim().toLowerCase()`. `filteredTags` is `Tag[]` same shape as `tags`. Tag shape from `useTags` is `{ id: number; name: string }` — both fields used correctly. + +--- + +## Execution Handoff + +Scope is one file + six small steps. Subagent-driven would be over-engineered. Recommended: inline execution with `superpowers:executing-plans`.