docs(20): create phase plan for FAB and full-screen catalog search

This commit is contained in:
2026-04-06 07:49:30 +02:00
parent d602f27f14
commit d020b4b63d
3 changed files with 618 additions and 2 deletions

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Key types and contracts the executor needs -->
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<T>(path: string): Promise<T>;
```
From src/client/stores/uiStore.ts (existing pattern):
```typescript
export const useUIStore = create<UIState>((set) => ({ ... }));
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Tag service, route, route registrations, and tests</name>
<files>
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
</files>
<read_first>
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
</read_first>
<behavior>
- 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)
</behavior>
<action>
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.
</action>
<verify>
<automated>bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts</automated>
</verify>
<acceptance_criteria>
- 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`
</acceptance_criteria>
<done>GET /api/tags returns all tags. GET /api/global-items is registered and reachable. All tests green.</done>
</task>
<task type="auto">
<name>Task 2: UIStore extension + useTags hook + useGlobalItems tag support</name>
<files>
src/client/stores/uiStore.ts,
src/client/hooks/useTags.ts,
src/client/hooks/useGlobalItems.ts
</files>
<read_first>
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
</read_first>
<action>
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<Tag[]>("/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.
</action>
<verify>
<automated>bun run lint && bun run build</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>UIStore has FAB menu + catalog search state. useTags hook fetches tags. useGlobalItems supports tag filtering. Lint and build pass.</done>
</task>
</tasks>
<verification>
- `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)
</verification>
<success_criteria>
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)
</success_criteria>
<output>
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- From Plan 01 outputs (UIStore extensions) -->
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<Tag[]>;
```
From src/client/hooks/useGlobalItems.ts (after Plan 01):
```typescript
export function useGlobalItems(query?: string, tags?: string[]): UseQueryResult<GlobalItem[]>;
```
From src/client/hooks/useFormatters.ts:
```typescript
export function useFormatters(): { weight: (grams: number) => string; price: (cents: number) => string; };
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: FabMenu component and CatalogSearchOverlay component</name>
<files>
src/client/components/FabMenu.tsx,
src/client/components/CatalogSearchOverlay.tsx
</files>
<read_first>
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
</read_first>
<action>
**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<string[]>([])`. 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.
</action>
<verify>
<automated>bun run lint && bun run build</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>FabMenu renders with animated mini menu. CatalogSearchOverlay renders with search, tag chips, result cards, loading/empty states. Both components compile and lint clean.</done>
</task>
<task type="auto">
<name>Task 2: Wire FabMenu and CatalogSearchOverlay into root layout</name>
<files>
src/client/routes/__root.tsx
</files>
<read_first>
src/client/routes/__root.tsx,
src/client/components/FabMenu.tsx,
src/client/components/CatalogSearchOverlay.tsx,
src/client/stores/uiStore.ts
</read_first>
<action>
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 `<FabMenu>` 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 && <FabMenu isSetupsPage={isSetupsPage} />}
```
5. Render CatalogSearchOverlay unconditionally (it manages its own visibility via UIStore):
```
<CatalogSearchOverlay />
```
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.
</action>
<verify>
<automated>bun run lint && bun run build</automated>
</verify>
<acceptance_criteria>
- `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 `<FabMenu` component usage
- `src/client/routes/__root.tsx` contains `<CatalogSearchOverlay` component usage
- `src/client/routes/__root.tsx` contains public route check: `location.pathname.startsWith("/users/")`
- `bun run lint` passes
- `bun run build` succeeds
</acceptance_criteria>
<done>Root layout renders FabMenu on all authenticated routes and CatalogSearchOverlay globally. Old single-action FAB removed. Build passes.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Visual verification of FAB and catalog search</name>
<files>none</files>
<action>
Human verifies the FAB mini menu and catalog search overlay work correctly across pages and viewports.
</action>
<what-built>
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.
</what-built>
<how-to-verify>
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
</how-to-verify>
<verify>Human confirms all 12 verification steps pass</verify>
<done>User approves FAB and catalog search overlay behavior across all pages and viewports.</done>
<resume-signal>Type "approved" or describe any issues</resume-signal>
</task>
</tasks>
<verification>
- `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)
</verification>
<success_criteria>
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
</success_criteria>
<output>
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-02-SUMMARY.md`
</output>