---
phase: 08-search-filter-and-candidate-status
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - src/client/components/CategoryFilterDropdown.tsx
  - src/client/routes/collection/index.tsx
autonomous: true
requirements: [SRCH-01, SRCH-02, SRCH-03, SRCH-04, SRCH-05, PLAN-01]

must_haves:
  truths:
    - "User can type in a search field on the gear tab and see items filtered instantly by name as they type"
    - "User can select a category from a searchable dropdown (with Lucide icons) to filter items on both gear and planning tabs"
    - "User can combine text search with category filter to narrow results"
    - "User sees 'Showing X of Y items' when filters are active on the gear tab"
    - "User clears search text and resets category dropdown individually (no combined clear button)"
    - "When filters are active, items display as a flat grid without category group headers"
    - "Empty filter results show 'No items match your search' message"
    - "Planning tab category filter shows Lucide icons alongside category names"
  artifacts:
    - path: "src/client/components/CategoryFilterDropdown.tsx"
      provides: "Shared searchable category filter dropdown with Lucide icons"
      exports: ["CategoryFilterDropdown"]
      min_lines: 60
    - path: "src/client/routes/collection/index.tsx"
      provides: "Search/filter toolbar in CollectionView, CategoryFilterDropdown in PlanningView"
      contains: "CategoryFilterDropdown"
  key_links:
    - from: "src/client/components/CategoryFilterDropdown.tsx"
      to: "src/client/hooks/useCategories.ts"
      via: "categories prop passed from parent (useCategories data)"
      pattern: "categories"
    - from: "src/client/routes/collection/index.tsx (CollectionView)"
      to: "src/client/components/CategoryFilterDropdown.tsx"
      via: "CategoryFilterDropdown in sticky toolbar"
      pattern: "<CategoryFilterDropdown"
    - from: "src/client/routes/collection/index.tsx (PlanningView)"
      to: "src/client/components/CategoryFilterDropdown.tsx"
      via: "CategoryFilterDropdown replacing native select"
      pattern: "<CategoryFilterDropdown"
    - from: "src/client/routes/collection/index.tsx (CollectionView)"
      to: "useItems data"
      via: "useMemo filter chain on searchText + categoryFilter"
      pattern: "filteredItems"
---

<objective>
Add search/filter toolbar to the gear tab and a shared icon-aware category filter dropdown to both gear and planning tabs. Users can search items by name, filter by category, see result counts, and clear filters individually.

Purpose: Help users find items quickly as collections grow, and upgrade the planning tab's plain `<select>` to a searchable icon-aware dropdown.
Output: Sticky search/filter toolbar on gear tab, shared CategoryFilterDropdown component on both tabs.
</objective>

<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-search-filter-and-candidate-status/08-CONTEXT.md
@.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md

@src/client/routes/collection/index.tsx
@src/client/components/CategoryPicker.tsx
@src/client/hooks/useCategories.ts
@src/client/hooks/useItems.ts
@src/client/lib/iconData.tsx

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From src/client/hooks/useItems.ts:
```typescript
// useItems() returns items with these fields:
interface ItemWithCategory {
  id: number; name: string; weightGrams: number | null;
  priceCents: number | null; categoryId: number;
  notes: string | null; productUrl: string | null;
  imageFilename: string | null; createdAt: string; updatedAt: string;
  categoryName: string; categoryIcon: string;
}
```

From src/client/hooks/useCategories.ts:
```typescript
// useCategories() returns:
interface CategoryItem {
  id: number; name: string; icon: string; createdAt: string;
}
```

From src/client/lib/iconData.tsx:
```typescript
export function LucideIcon({ name, size, className }: {
  name: string; size?: number; className?: string;
}): JSX.Element;
```

From src/client/routes/collection/index.tsx:
```typescript
// CollectionView currently:
// - Uses useItems() for all items
// - Groups items by categoryId into Map
// - Renders CategoryHeader + grid per category group
// - No search or filter state

// PlanningView currently:
// - Has categoryFilter useState<number | null>(null)
// - Uses a native <select> for category filtering (lines 277-291)
// - Filters threads by activeTab and categoryFilter
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create CategoryFilterDropdown component</name>
  <files>src/client/components/CategoryFilterDropdown.tsx</files>
  <action>
    Create `src/client/components/CategoryFilterDropdown.tsx` -- a searchable dropdown showing categories with Lucide icons. This is a FILTER dropdown, NOT the form-based `CategoryPicker` (which handles creation). Keep them separate per user decision.

    **Props:**
    ```typescript
    interface CategoryFilterDropdownProps {
      value: number | null;           // selected category ID, null = "All categories"
      onChange: (value: number | null) => void;
      categories: Array<{ id: number; name: string; icon: string }>;
    }
    ```

    **Structure:**
    - **Trigger button**: Shows "All categories" with a chevron-down icon when `value` is null. Shows the selected category's `LucideIcon` (size 14) + name when a category is selected. Style: `px-3 py-2 border border-gray-200 rounded-lg text-sm bg-white` (matching search input height). Include a small clear "x" button on the right when a category is selected (clicking it calls `onChange(null)` without opening the dropdown).
    - **Dropdown panel**: Opens below the trigger, `position: absolute`, `z-20`, white bg, border, rounded-lg, shadow-lg, max-height with overflow-y-auto. Width matches trigger or has a reasonable min-width (~220px).
    - **Search input inside dropdown**: Text input at top of dropdown, placeholder "Search categories...", filters the category list as user types. Auto-focused when dropdown opens.
    - **Option list**: "All categories" as first option (selecting calls `onChange(null)` and closes). Then each category: `LucideIcon` (size 16) + category name. Highlight the currently selected option with a subtle bg color. Hover state on each option.
    - **Click-outside dismiss**: Use `containerRef` + `useEffect` mousedown listener pattern (same as `CategoryPicker`). Also close on Escape keydown.
    - **State reset**: Clear internal search text when dropdown closes.

    **Do NOT:**
    - Reuse or modify `CategoryPicker.tsx`
    - Add category creation capability
    - Use Zustand for dropdown open/closed state (use local `useState`)
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint</automated>
  </verify>
  <done>CategoryFilterDropdown.tsx exists with searchable dropdown, Lucide icons per option, "All categories" first option, click-outside dismiss, clear button on trigger, and Escape to close. Lint passes.</done>
</task>

<task type="auto">
  <name>Task 2: Add search/filter toolbar to CollectionView and replace select in PlanningView</name>
  <files>src/client/routes/collection/index.tsx</files>
  <action>
    Modify `src/client/routes/collection/index.tsx` to add search and filtering to `CollectionView` and upgrade `PlanningView`'s category filter.

    **CollectionView changes:**

    1. Add filter state at the top of `CollectionView`:
       ```typescript
       const [searchText, setSearchText] = useState("");
       const [categoryFilter, setCategoryFilter] = useState<number | null>(null);
       ```

    2. Add `useCategories` hook: `const { data: categories } = useCategories();`

    3. Add filtered items computation with `useMemo`:
       ```typescript
       const filteredItems = useMemo(() => {
         if (!items) return [];
         return items.filter((item) => {
           const matchesSearch = searchText === "" ||
             item.name.toLowerCase().includes(searchText.toLowerCase());
           const matchesCategory = categoryFilter === null ||
             item.categoryId === categoryFilter;
           return matchesSearch && matchesCategory;
         });
       }, [items, searchText, categoryFilter]);
       ```
       Import `useMemo` from React, import `useCategories` from hooks.

    4. Compute filter state:
       ```typescript
       const hasActiveFilters = searchText !== "" || categoryFilter !== null;
       ```

    5. Add sticky toolbar ABOVE the existing item grid rendering (after loading/empty checks, before the grouped items). The toolbar only shows when there are items:
       ```jsx
       <div className="sticky top-0 z-10 bg-white/95 backdrop-blur-sm border-b border-gray-100 -mx-4 px-4 py-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8 mb-6">
         <div className="flex gap-3 items-center">
           <div className="relative flex-1">
             <input
               type="text"
               placeholder="Search items..."
               value={searchText}
               onChange={(e) => setSearchText(e.target.value)}
               className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
             />
             {searchText && (
               <button onClick={() => setSearchText("")} className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
                 {/* small x icon */}
               </button>
             )}
           </div>
           <CategoryFilterDropdown
             value={categoryFilter}
             onChange={setCategoryFilter}
             categories={categories ?? []}
           />
         </div>
         {hasActiveFilters && (
           <p className="text-xs text-gray-500 mt-2">
             Showing {filteredItems.length} of {items?.length ?? 0} items
           </p>
         )}
       </div>
       ```

    6. Conditional rendering based on filter state:
       - **When `hasActiveFilters` is true**: Render `filteredItems` as a flat grid (no category grouping, no `CategoryHeader`). If `filteredItems.length === 0`, show "No items match your search" centered text message.
       - **When `hasActiveFilters` is false**: Keep existing category-grouped rendering exactly as-is (the `groupedItems` Map pattern), but use `filteredItems` as the source (which equals all items when no filters).

    **PlanningView changes:**

    1. Import `CategoryFilterDropdown` from `../../components/CategoryFilterDropdown`.
    2. Replace the native `<select>` element (lines ~277-291) with:
       ```jsx
       <CategoryFilterDropdown
         value={categoryFilter}
         onChange={setCategoryFilter}
         categories={categories ?? []}
       />
       ```
    3. Remove the `useCategories` hook call if it's already called earlier, or keep it -- just make sure categories data is available.

    **Important per user decisions:**
    - Search matches item names ONLY (not category names) -- the dropdown handles category filtering
    - No debounce on search input (per CONTEXT.md, <1000 items)
    - No combined "clear all" button -- user clears search and dropdown individually
    - Filters naturally reset on tab switch because `CollectionView` unmounts when tab changes (conditional rendering in `CollectionPage`). Verify this is the case -- if `CollectionView` stays mounted, add a `key={tab}` prop to force remount.
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint && bun test</automated>
  </verify>
  <done>Gear tab has a sticky search/filter toolbar with text input and CategoryFilterDropdown side by side. Typing filters items by name instantly. Selecting a category filters by category. Both filters combine. "Showing X of Y items" appears when filters are active. Empty results show message. Flat grid renders when filters active (no category headers). Planning tab uses CategoryFilterDropdown with Lucide icons instead of native select. All tests and lint pass.</done>
</task>

</tasks>

<verification>
1. `bun run lint` -- no lint errors
2. `bun test` -- all tests pass
3. Start dev server, navigate to gear tab:
   - Sticky toolbar visible with search input + category dropdown
   - Type in search: items filter by name instantly
   - Select a category from dropdown (icons visible): items filter by category
   - Both filters combine correctly
   - "Showing X of Y items" text appears when filters active
   - Empty results show "No items match your search"
   - Filtered items show as flat grid (no category headers)
   - Clear search text: category filter still applies
   - Select "All categories": search filter still applies
   - Switch to planning tab: filters reset
   - Switch back to gear tab: filters reset (clean state)
4. Navigate to planning tab:
   - Category filter dropdown shows Lucide icons alongside names
   - Searchable within the dropdown
   - "All categories" as first option
   - Selecting a category shows icon + name in trigger button
</verification>

<success_criteria>
- Search input filters items by name on every keystroke (no debounce)
- CategoryFilterDropdown shows icons, is searchable, has "All categories" option
- Filters combine (text AND category)
- Result count displayed when filters active
- Flat grid (no category headers) when any filter active
- "No items match your search" on empty results
- Filters reset on tab switch
- Planning tab uses shared CategoryFilterDropdown instead of native select
- Lint and tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/08-search-filter-and-candidate-status/08-02-SUMMARY.md`
</output>
