diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 331b8a6..d6db296 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -200,7 +200,10 @@ Plans:
3. Full-screen catalog search overlay opens from either add option
4. Search results display catalog items with name, weight, price, owner count
5. Tag chips filter search results
-**Plans**: TBD
+**Plans:** 2 plans
+Plans:
+- [ ] 20-01-PLAN.md — Tags endpoint, global-items route registration, UIStore extension, useTags hook
+- [ ] 20-02-PLAN.md — FabMenu component, CatalogSearchOverlay component, root layout wiring
**UI hint**: yes
### Phase 21: Add-from-Catalog & Thread Integration
@@ -248,6 +251,6 @@ Plans:
| 17. Object Storage | v2.0 | 0/? | Not started | - |
| 18. Global Items & Public Profiles | v2.0 | 4/5 | Complete | 2026-04-05 |
| 19. Reference Item Model & Tags Schema | v2.0 | 3/3 | Complete | 2026-04-05 |
-| 20. FAB & Full-Screen Catalog Search | v2.0 | 0/? | Not started | - |
+| 20. FAB & Full-Screen Catalog Search | v2.0 | 0/2 | Not started | - |
| 21. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
| 22. Manual Entry Fallback | v2.0 | 0/? | Not started | - |
diff --git a/.planning/phases/20-fab-full-screen-catalog-search/20-01-PLAN.md b/.planning/phases/20-fab-full-screen-catalog-search/20-01-PLAN.md
new file mode 100644
index 0000000..8feb67d
--- /dev/null
+++ b/.planning/phases/20-fab-full-screen-catalog-search/20-01-PLAN.md
@@ -0,0 +1,279 @@
+---
+phase: 20-fab-full-screen-catalog-search
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/server/services/tag.service.ts
+ - src/server/routes/tags.ts
+ - src/server/index.ts
+ - src/client/stores/uiStore.ts
+ - src/client/hooks/useTags.ts
+ - src/client/hooks/useGlobalItems.ts
+ - tests/services/tag.service.test.ts
+ - tests/routes/tags.test.ts
+autonomous: true
+requirements:
+ - CATFLOW-01
+ - CATFLOW-02
+
+must_haves:
+ truths:
+ - "GET /api/tags returns all tags from the database"
+ - "GET /api/global-items endpoint is reachable (route registered)"
+ - "UIStore exposes fabMenu and catalogSearch state slices"
+ - "useGlobalItems hook supports optional tags parameter"
+ - "useTags hook fetches and caches tag data"
+ artifacts:
+ - path: "src/server/services/tag.service.ts"
+ provides: "getAllTags function"
+ exports: ["getAllTags"]
+ - path: "src/server/routes/tags.ts"
+ provides: "GET /api/tags endpoint"
+ exports: ["tagRoutes"]
+ - path: "src/client/stores/uiStore.ts"
+ provides: "FAB menu + catalog search state"
+ contains: "fabMenuOpen"
+ - path: "src/client/hooks/useTags.ts"
+ provides: "Tag fetching hook"
+ exports: ["useTags"]
+ - path: "src/client/hooks/useGlobalItems.ts"
+ provides: "Updated hook with tag support"
+ contains: "tags"
+ key_links:
+ - from: "src/server/routes/tags.ts"
+ to: "src/server/services/tag.service.ts"
+ via: "getAllTags import"
+ pattern: "getAllTags"
+ - from: "src/server/index.ts"
+ to: "src/server/routes/tags.ts"
+ via: "app.route registration"
+ pattern: "app\\.route.*tags"
+ - from: "src/server/index.ts"
+ to: "src/server/routes/global-items.ts"
+ via: "app.route registration"
+ pattern: "app\\.route.*global-items"
+---
+
+
+Create the server-side tags endpoint, register the global-items route, extend UIStore with FAB/catalog-search state, and update client hooks for tag-aware searching.
+
+Purpose: Provides the data layer (tags API), state management (UIStore), and hooks that Plan 02's UI components will consume.
+Output: Working GET /api/tags endpoint, registered GET /api/global-items, extended UIStore, useTags hook, updated useGlobalItems hook with tag support.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
+@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
+
+@src/server/index.ts
+@src/server/routes/global-items.ts
+@src/server/services/global-item.service.ts
+@src/db/schema.ts
+@src/client/stores/uiStore.ts
+@src/client/hooks/useGlobalItems.ts
+@src/client/lib/api.ts
+@tests/routes/global-items.test.ts
+@tests/helpers/db.ts
+
+
+
+
+From src/db/schema.ts:
+```typescript
+export const tags = pgTable("tags", {
+ id: serial("id").primaryKey(),
+ name: text("name").notNull().unique(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+});
+```
+
+From src/client/lib/api.ts:
+```typescript
+export async function apiGet(path: string): Promise;
+```
+
+From src/client/stores/uiStore.ts (existing pattern):
+```typescript
+export const useUIStore = create((set) => ({ ... }));
+```
+
+
+
+
+
+
+ Task 1: Tag service, route, route registrations, and tests
+
+ src/server/services/tag.service.ts,
+ src/server/routes/tags.ts,
+ src/server/index.ts,
+ tests/services/tag.service.test.ts,
+ tests/routes/tags.test.ts
+
+
+ src/db/schema.ts,
+ src/server/index.ts,
+ src/server/routes/global-items.ts,
+ src/server/services/global-item.service.ts,
+ tests/routes/global-items.test.ts,
+ tests/helpers/db.ts
+
+
+ - Test: getAllTags returns all tags from the database as { id, name }[] (no createdAt)
+ - Test: getAllTags returns empty array when no tags exist
+ - Test: GET /api/tags returns 200 with array of tag objects
+ - Test: GET /api/global-items still works after route registration (existing tests pass)
+
+
+ 1. Create `src/server/services/tag.service.ts`:
+ - Export `getAllTags(db)` function that selects `{ id: tags.id, name: tags.name }` from the `tags` table, ordered alphabetically by name (per D-17, alphabetical is a reasonable default).
+ - Import `tags` from `../../db/schema.ts`.
+ - Use the same `type Db = typeof prodDb` pattern as other services.
+
+ 2. Create `src/server/routes/tags.ts`:
+ - Single GET "/" handler that calls `getAllTags(db)` and returns `c.json(allTags)`.
+ - Export as `tagRoutes`.
+ - Follow the exact pattern from `src/server/routes/global-items.ts` (Hono + Env type).
+
+ 3. Update `src/server/index.ts`:
+ - Add import: `import { globalItemRoutes } from "./routes/global-items.ts";`
+ - Add import: `import { tagRoutes } from "./routes/tags.ts";`
+ - Register both routes AFTER existing route registrations (before MCP block):
+ `app.route("/api/global-items", globalItemRoutes);`
+ `app.route("/api/tags", tagRoutes);`
+ - CRITICAL: The global-items route file EXISTS but is NOT registered (research pitfall #1). Must add it here.
+ - Add public route skip in auth middleware for tags: `if (c.req.path.startsWith("/api/tags") && c.req.method === "GET") return next();`
+ - Add public route skip for global-items: `if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();`
+
+ 4. Create `tests/services/tag.service.test.ts`:
+ - Use `createTestDb()` from `tests/helpers/db.ts`.
+ - Test: returns empty array when no tags.
+ - Test: returns all tags as `{ id, name }` after inserting test data.
+ - Seed tags using `db.insert(tags).values(...)`.
+
+ 5. Create `tests/routes/tags.test.ts`:
+ - Follow pattern from `tests/routes/global-items.test.ts`.
+ - Use the app with test db injection.
+ - Test: GET /api/tags returns 200 with JSON array.
+ - Test: response contains expected tags after seeding.
+
+
+ bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts
+
+
+ - File `src/server/services/tag.service.ts` exists and exports `getAllTags`
+ - File `src/server/routes/tags.ts` exists and exports `tagRoutes`
+ - `src/server/index.ts` contains `app.route("/api/tags", tagRoutes)`
+ - `src/server/index.ts` contains `app.route("/api/global-items", globalItemRoutes)`
+ - `src/server/index.ts` contains auth skip for `/api/tags` and `/api/global-items` GET requests
+ - All tag tests pass: `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts`
+ - Existing global-items tests still pass: `bun test tests/routes/global-items.test.ts`
+
+ GET /api/tags returns all tags. GET /api/global-items is registered and reachable. All tests green.
+
+
+
+ Task 2: UIStore extension + useTags hook + useGlobalItems tag support
+
+ src/client/stores/uiStore.ts,
+ src/client/hooks/useTags.ts,
+ src/client/hooks/useGlobalItems.ts
+
+
+ src/client/stores/uiStore.ts,
+ src/client/hooks/useGlobalItems.ts,
+ src/client/lib/api.ts,
+ .planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
+
+
+ 1. Extend `src/client/stores/uiStore.ts` per D-21, D-22, D-23:
+ - Add to UIState interface:
+ ```
+ fabMenuOpen: boolean;
+ openFabMenu: () => void;
+ closeFabMenu: () => void;
+ catalogSearchOpen: boolean;
+ catalogSearchMode: "collection" | "thread" | null;
+ openCatalogSearch: (mode: "collection" | "thread") => void;
+ closeCatalogSearch: () => void;
+ ```
+ - Add to store implementation:
+ ```
+ fabMenuOpen: false,
+ openFabMenu: () => set({ fabMenuOpen: true }),
+ closeFabMenu: () => set({ fabMenuOpen: false }),
+ catalogSearchOpen: false,
+ catalogSearchMode: null,
+ openCatalogSearch: (mode) => set({ catalogSearchOpen: true, catalogSearchMode: mode, fabMenuOpen: false }),
+ closeCatalogSearch: () => set({ catalogSearchOpen: false, catalogSearchMode: null }),
+ ```
+ - Note: `openCatalogSearch` also closes the FAB menu (natural flow: tap FAB -> tap option -> menu closes, overlay opens).
+
+ 2. Create `src/client/hooks/useTags.ts`:
+ - Export `useTags()` hook using `useQuery` from `@tanstack/react-query`.
+ - Query key: `["tags"]`.
+ - Query fn: `apiGet("/api/tags")` where `Tag = { id: number; name: string }`.
+ - Set `staleTime: 5 * 60 * 1000` (tags change rarely, cache 5 min).
+ - Export the `Tag` interface as well.
+
+ 3. Update `src/client/hooks/useGlobalItems.ts`:
+ - Modify `useGlobalItems` signature to accept optional `tags?: string[]` parameter.
+ - Build query string using `URLSearchParams`:
+ ```
+ const params = new URLSearchParams();
+ if (query) params.set("q", query);
+ if (tags && tags.length > 0) params.set("tags", tags.join(","));
+ const qs = params.toString();
+ ```
+ - Update query key to include tags: `["global-items", query ?? "", tags ?? []]`.
+ - Update query fn URL: `` `/api/global-items${qs ? `?${qs}` : ""}` ``.
+ - Keep all other exports (`useGlobalItem`, `useLinkItem`, `useUnlinkItem`) unchanged.
+
+
+ bun run lint && bun run build
+
+
+ - `src/client/stores/uiStore.ts` contains `fabMenuOpen` state field
+ - `src/client/stores/uiStore.ts` contains `catalogSearchOpen` state field
+ - `src/client/stores/uiStore.ts` contains `catalogSearchMode` state field
+ - `src/client/stores/uiStore.ts` contains `openCatalogSearch` action
+ - `src/client/stores/uiStore.ts` contains `closeCatalogSearch` action
+ - `src/client/hooks/useTags.ts` exists and exports `useTags`
+ - `src/client/hooks/useGlobalItems.ts` accepts `tags?: string[]` parameter
+ - `bun run lint` passes with no errors
+ - `bun run build` succeeds
+
+ UIStore has FAB menu + catalog search state. useTags hook fetches tags. useGlobalItems supports tag filtering. Lint and build pass.
+
+
+
+
+
+- `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts` -- all green
+- `bun run lint` -- no errors
+- `bun run build` -- succeeds
+- `bun test` -- full suite green (no regressions)
+
+
+
+1. GET /api/tags returns all tags as JSON array
+2. GET /api/global-items is registered and reachable (was missing from index.ts)
+3. UIStore has fabMenuOpen, catalogSearchOpen, catalogSearchMode state
+4. useTags hook works with staleTime caching
+5. useGlobalItems accepts optional tags parameter for filtering
+6. All existing tests pass (no regressions)
+
+
+
diff --git a/.planning/phases/20-fab-full-screen-catalog-search/20-02-PLAN.md b/.planning/phases/20-fab-full-screen-catalog-search/20-02-PLAN.md
new file mode 100644
index 0000000..c7a6df3
--- /dev/null
+++ b/.planning/phases/20-fab-full-screen-catalog-search/20-02-PLAN.md
@@ -0,0 +1,334 @@
+---
+phase: 20-fab-full-screen-catalog-search
+plan: 02
+type: execute
+wave: 2
+depends_on: ["20-01"]
+files_modified:
+ - src/client/components/FabMenu.tsx
+ - src/client/components/CatalogSearchOverlay.tsx
+ - src/client/routes/__root.tsx
+autonomous: false
+requirements:
+ - CATFLOW-01
+ - CATFLOW-02
+
+must_haves:
+ truths:
+ - "FAB is visible on all authenticated pages (collection, threads, setups, dashboard, settings, global-items)"
+ - "FAB is NOT visible on login page or public profile/setup pages"
+ - "Tapping FAB opens a mini menu with 'Add to Collection' and 'Start New Thread' options"
+ - "On setups page, FAB menu also shows 'New Setup' option"
+ - "Tapping 'Add to Collection' opens full-screen catalog search overlay in 'collection' mode"
+ - "Tapping 'Start New Thread' opens full-screen catalog search overlay in 'thread' mode"
+ - "Catalog search overlay has search input with debounce, tag chips, and result cards"
+ - "Tag chips toggle on/off and filter search results via AND logic"
+ - "Result cards show brand, model, weight, price, category, and an Add button (stub)"
+ - "Back arrow closes the catalog search overlay"
+ artifacts:
+ - path: "src/client/components/FabMenu.tsx"
+ provides: "FAB with mini menu"
+ min_lines: 60
+ - path: "src/client/components/CatalogSearchOverlay.tsx"
+ provides: "Full-screen catalog search"
+ min_lines: 100
+ key_links:
+ - from: "src/client/components/FabMenu.tsx"
+ to: "src/client/stores/uiStore.ts"
+ via: "useUIStore (fabMenuOpen, openCatalogSearch)"
+ pattern: "useUIStore"
+ - from: "src/client/components/CatalogSearchOverlay.tsx"
+ to: "src/client/hooks/useGlobalItems.ts"
+ via: "useGlobalItems(query, tags)"
+ pattern: "useGlobalItems"
+ - from: "src/client/components/CatalogSearchOverlay.tsx"
+ to: "src/client/hooks/useTags.ts"
+ via: "useTags()"
+ pattern: "useTags"
+ - from: "src/client/routes/__root.tsx"
+ to: "src/client/components/FabMenu.tsx"
+ via: "FabMenu component render"
+ pattern: "FabMenu"
+ - from: "src/client/routes/__root.tsx"
+ to: "src/client/components/CatalogSearchOverlay.tsx"
+ via: "CatalogSearchOverlay component render"
+ pattern: "CatalogSearchOverlay"
+---
+
+
+Build the FAB mini menu component and full-screen catalog search overlay, then wire them into the root layout.
+
+Purpose: Delivers the primary user-facing UI for Phase 20 -- the global FAB with action menu and the catalog discovery experience with search + tag filtering.
+Output: FabMenu.tsx, CatalogSearchOverlay.tsx, updated __root.tsx with new FAB and overlay rendering.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
+@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
+@.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md
+
+@src/client/routes/__root.tsx
+@src/client/stores/uiStore.ts
+@src/client/components/GlobalItemCard.tsx
+@src/client/components/CreateThreadModal.tsx
+@src/client/hooks/useTags.ts
+@src/client/hooks/useGlobalItems.ts
+@src/client/hooks/useFormatters.ts
+@src/client/routes/global-items/index.tsx
+
+
+
+From src/client/stores/uiStore.ts (after Plan 01):
+```typescript
+// FAB menu state
+fabMenuOpen: boolean;
+openFabMenu: () => void;
+closeFabMenu: () => void;
+
+// Catalog search state
+catalogSearchOpen: boolean;
+catalogSearchMode: "collection" | "thread" | null;
+openCatalogSearch: (mode: "collection" | "thread") => void;
+closeCatalogSearch: () => void;
+```
+
+From src/client/hooks/useTags.ts (after Plan 01):
+```typescript
+export interface Tag { id: number; name: string; }
+export function useTags(): UseQueryResult;
+```
+
+From src/client/hooks/useGlobalItems.ts (after Plan 01):
+```typescript
+export function useGlobalItems(query?: string, tags?: string[]): UseQueryResult;
+```
+
+From src/client/hooks/useFormatters.ts:
+```typescript
+export function useFormatters(): { weight: (grams: number) => string; price: (cents: number) => string; };
+```
+
+
+
+
+
+
+ Task 1: FabMenu component and CatalogSearchOverlay component
+
+ src/client/components/FabMenu.tsx,
+ src/client/components/CatalogSearchOverlay.tsx
+
+
+ src/client/stores/uiStore.ts,
+ src/client/components/GlobalItemCard.tsx,
+ src/client/components/CreateThreadModal.tsx,
+ src/client/routes/global-items/index.tsx,
+ src/client/hooks/useTags.ts,
+ src/client/hooks/useGlobalItems.ts,
+ src/client/hooks/useFormatters.ts,
+ .planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
+
+
+ **FabMenu.tsx** (per D-01 through D-06, D-23):
+
+ 1. Create `src/client/components/FabMenu.tsx` that accepts props: `{ isSetupsPage: boolean }`.
+ 2. Read state from UIStore: `fabMenuOpen`, `openFabMenu`, `closeFabMenu`, `openCatalogSearch`, `catalogSearchOpen`.
+ 3. The main FAB button: `fixed bottom-6 right-6 z-20 w-14 h-14 bg-gray-700 hover:bg-gray-800 text-white rounded-full shadow-lg`. Shows "+" icon normally, rotates to "x" when menu is open.
+ 4. When `fabMenuOpen` is true, render:
+ - A subtle backdrop (`fixed inset-0 z-10 bg-black/20`) that closes menu on click (per D-02).
+ - Menu items stack vertically above the FAB (per D-02), each with an icon + label:
+ - "Add to Collection" with Package icon -- calls `openCatalogSearch("collection")` (per D-04)
+ - "Start New Thread" with Search icon -- calls `openCatalogSearch("thread")` (per D-04)
+ - If `isSetupsPage`, also show "New Setup" with Plus icon -- triggers existing setup creation flow (per D-05). For now, navigate to `/setups` with a query param or call the existing pattern. Read how setups page handles creation and replicate. If unclear, use `navigate({ to: "/setups", search: { action: "create" } })` as a stub.
+ - Menu items positioned `fixed bottom-24 right-6 z-20` (above FAB), each item spaced ~56px apart vertically.
+ 5. Use Framer Motion `AnimatePresence` + `motion.div` for menu entrance/exit:
+ - Items appear with `initial={{ opacity: 0, y: 10, scale: 0.9 }}`, `animate={{ opacity: 1, y: 0, scale: 1 }}`, `exit={{ opacity: 0, y: 10, scale: 0.9 }}`.
+ - Transition: `{ type: "spring", stiffness: 400, damping: 25 }` (per research Pattern 5).
+ - Stagger children slightly (50ms delay between items).
+ 6. Each menu item: white background with shadow, `rounded-full px-4 py-3 flex items-center gap-3`, icon (20x20) + text label (`text-sm font-medium text-gray-700`).
+ 7. Hide FAB entirely when `catalogSearchOpen` is true (per research Pitfall #2 -- overlay covers everything, FAB shouldn't peek through).
+ 8. FAB should NOT appear on login page or public routes (per D-06). This is handled by the parent (__root.tsx), not FabMenu itself.
+
+ **CatalogSearchOverlay.tsx** (per D-07 through D-13, D-14 through D-20):
+
+ 1. Create `src/client/components/CatalogSearchOverlay.tsx`.
+ 2. Read state from UIStore: `catalogSearchOpen`, `catalogSearchMode`, `closeCatalogSearch`.
+ 3. Only render when `catalogSearchOpen` is true. Wrap in `AnimatePresence` for enter/exit.
+ 4. Overlay container: `fixed inset-0 z-50 bg-white flex flex-col` (per D-07). Full white background, not a backdrop modal.
+ 5. Add `overflow-hidden` to document body when overlay is open (per research Pitfall #4). Use `useEffect` to toggle `document.body.style.overflow`.
+
+ **Header section** (per D-08):
+ - Top bar with: back arrow button (left, calls `closeCatalogSearch`), context text ("Adding to Collection" when mode is "collection", "Starting a Thread" when mode is "thread").
+ - Below: large search input (`text-lg px-4 py-3 border-b border-gray-100 w-full`), autofocused, with placeholder "Search the catalog...".
+ - Debounce search input using the established pattern (per research Pattern 4): `useState` for input, `useEffect` with 300ms `setTimeout` for debouncedQuery.
+
+ **Tag chips section** (per D-09, D-14 through D-17):
+ - Below search bar: horizontal scrollable row (`flex gap-2 overflow-x-auto px-4 py-3 no-scrollbar`).
+ - Fetch tags with `useTags()` hook.
+ - Each tag chip: `rounded-full px-3 py-1.5 text-sm font-medium cursor-pointer transition-colors whitespace-nowrap`.
+ - Active state (per D-16): `bg-blue-100 text-blue-700`.
+ - Inactive state (per D-16): `bg-gray-100 text-gray-500`.
+ - Manage selected tags as local state: `useState([])`. Toggle tag name on click. Multiple selections = AND filtering (per D-15).
+ - Pass selected tag names to `useGlobalItems(debouncedQuery, selectedTags)`.
+
+ **Results grid** (per D-10, D-18 through D-20):
+ - Grid layout: `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4 overflow-y-auto flex-1`.
+ - For each result, render a card adapted from `GlobalItemCard` pattern:
+ - Image area (aspect-[4/3] with bg-gray-50 placeholder if no image).
+ - Brand (small, uppercase, gray).
+ - Model name (semibold, truncated).
+ - Badge row: weight (blue), price (green), category (gray) -- same colors as GlobalItemCard.
+ - "Add" button: `bg-gray-700 text-white rounded-lg px-3 py-1.5 text-xs font-medium mt-2`. Per D-19, this is a STUB in Phase 20 -- clicking shows a brief toast or does nothing. Add `onClick` that logs or shows a placeholder message. Do NOT implement actual add-to-collection or add-to-thread flow.
+ - Use `useFormatters()` for weight/price formatting.
+
+ **Loading state** (per D-13):
+ - When query is loading, show 6 skeleton cards matching the grid pattern.
+ - Skeleton card: `animate-pulse` with gray placeholder blocks for image, title, badges.
+ - Copy the skeleton pattern from `src/client/routes/global-items/index.tsx` if available, or create matching pattern.
+
+ **Empty state** (per D-12):
+ - When query returns empty results and not loading: centered message "No items found" with subtle text.
+ - Include a non-functional "Add Manually" text link (`text-blue-600 underline`) that does nothing in Phase 20 (Phase 22 wires it). If Claude judges it premature, omit the link entirely -- this is Claude's discretion per CONTEXT.md.
+
+
+ bun run lint && bun run build
+
+
+ - File `src/client/components/FabMenu.tsx` exists with at least 60 lines
+ - File `src/client/components/CatalogSearchOverlay.tsx` exists with at least 100 lines
+ - FabMenu.tsx imports from `framer-motion` (AnimatePresence)
+ - FabMenu.tsx imports `useUIStore` from stores
+ - FabMenu.tsx renders "Add to Collection" and "Start New Thread" menu items
+ - CatalogSearchOverlay.tsx imports `useTags` and `useGlobalItems`
+ - CatalogSearchOverlay.tsx contains `bg-blue-100 text-blue-700` (active tag chip style per D-16)
+ - CatalogSearchOverlay.tsx contains `bg-gray-100 text-gray-500` (inactive tag chip style per D-16)
+ - CatalogSearchOverlay.tsx contains debounce pattern (setTimeout with 300)
+ - CatalogSearchOverlay.tsx contains skeleton/loading state
+ - CatalogSearchOverlay.tsx contains empty state message
+ - `bun run lint` passes
+ - `bun run build` succeeds
+
+ FabMenu renders with animated mini menu. CatalogSearchOverlay renders with search, tag chips, result cards, loading/empty states. Both components compile and lint clean.
+
+
+
+ Task 2: Wire FabMenu and CatalogSearchOverlay into root layout
+
+ src/client/routes/__root.tsx
+
+
+ src/client/routes/__root.tsx,
+ src/client/components/FabMenu.tsx,
+ src/client/components/CatalogSearchOverlay.tsx,
+ src/client/stores/uiStore.ts
+
+
+ 1. In `src/client/routes/__root.tsx`:
+ - Import `FabMenu` from `../components/FabMenu`.
+ - Import `CatalogSearchOverlay` from `../components/CatalogSearchOverlay`.
+
+ 2. Remove the old FAB button block (lines ~257-278, the `button` with "Add new item" title and "+" SVG). Replace with `` component.
+
+ 3. Compute FAB visibility (per D-06):
+ - Remove old `showFab` logic (which was collection gear tab only).
+ - New logic:
+ ```
+ const isPublicRoute = location.pathname.startsWith("/users/") || location.pathname === "/login";
+ const showFab = isAuthenticated && !isPublicRoute;
+ ```
+ - Determine if on setups page: `const isSetupsPage = !!matchRoute({ to: "/setups", fuzzy: true });`
+
+ 4. Render FabMenu conditionally:
+ ```
+ {showFab && }
+ ```
+
+ 5. Render CatalogSearchOverlay unconditionally (it manages its own visibility via UIStore):
+ ```
+
+ ```
+ Place it after the FabMenu render, before the OnboardingWizard.
+
+ 6. Read `catalogSearchOpen` from UIStore. When catalog search is open, the old `openAddPanel` call from the FAB is no longer relevant -- the FabMenu component handles its own actions. Verify the old `openAddPanel` reference on the FAB button is fully removed.
+
+ 7. Verify the `isSetupDetail` matchRoute and existing `collectionSearch` logic can be cleaned up if they were only used for FAB visibility. Keep any logic still needed for TotalsBar or other features.
+
+
+ bun run lint && bun run build
+
+
+ - `src/client/routes/__root.tsx` imports `FabMenu` from `../components/FabMenu`
+ - `src/client/routes/__root.tsx` imports `CatalogSearchOverlay` from `../components/CatalogSearchOverlay`
+ - `src/client/routes/__root.tsx` does NOT contain the old single-action FAB button (`title="Add new item"`)
+ - `src/client/routes/__root.tsx` contains `
+ Root layout renders FabMenu on all authenticated routes and CatalogSearchOverlay globally. Old single-action FAB removed. Build passes.
+
+
+
+ Task 3: Visual verification of FAB and catalog search
+ none
+
+ Human verifies the FAB mini menu and catalog search overlay work correctly across pages and viewports.
+
+
+ Global FAB with mini menu (Add to Collection, Start Thread, and New Setup on setups page) plus full-screen catalog search overlay with debounced search, tag chip filtering, and result cards.
+
+
+ 1. Start dev server: `bun run dev`
+ 2. Navigate to collection page (/collection) -- verify FAB (gray circle with "+") is visible at bottom-right
+ 3. Click FAB -- verify mini menu appears with "Add to Collection" and "Start New Thread" items, with subtle backdrop
+ 4. Click backdrop -- verify menu closes
+ 5. Click FAB again, then click "Add to Collection" -- verify full-screen white overlay opens with "Adding to Collection" text, search input (autofocused), and tag chips
+ 6. Type a search query -- verify results appear after brief debounce delay (~300ms) as card grid
+ 7. Click a tag chip -- verify it toggles to blue active state and filters results
+ 8. Click the back arrow -- verify overlay closes
+ 9. Navigate to /threads -- verify FAB is still visible
+ 10. Navigate to /setups -- verify FAB menu includes third "New Setup" option
+ 11. Navigate to /login -- verify FAB is NOT visible
+ 12. Check on mobile viewport (use devtools responsive mode) -- verify single column cards, horizontal scrollable tag chips
+
+ Human confirms all 12 verification steps pass
+ User approves FAB and catalog search overlay behavior across all pages and viewports.
+ Type "approved" or describe any issues
+
+
+
+
+
+- `bun run lint` -- no errors
+- `bun run build` -- succeeds
+- `bun test` -- full suite green
+- Visual: FAB visible on authenticated routes, hidden on public routes
+- Visual: Mini menu opens/closes with animation
+- Visual: Full-screen overlay with search, tags, results
+- Visual: Tag filtering works with AND logic
+- Visual: Responsive grid (1 col mobile, 2-3 col desktop)
+
+
+
+1. FAB visible on all authenticated pages with mini menu showing correct options (per D-01 through D-06, CATFLOW-01)
+2. "New Setup" appears only on setups page (per D-03, CATFLOW-01)
+3. Full-screen catalog search overlay opens from both add options (per D-07, CATFLOW-02)
+4. Search with debounce queries existing API (per D-08, D-11, CATFLOW-02)
+5. Tag chips filter results with AND logic (per D-14 through D-17, CATFLOW-02)
+6. Result cards display brand, model, weight, price, with stub Add button (per D-10, D-18 through D-20)
+7. Loading skeletons and empty state present (per D-12, D-13)
+8. Human verification approved
+
+
+