Files
GearBox/.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md

20 KiB

Phase 20: FAB & Full-Screen Catalog Search - Research

Researched: 2026-04-06 Domain: React UI components (FAB menu, full-screen overlay, tag filtering), Hono API endpoint Confidence: HIGH

Summary

Phase 20 is a client-heavy UI phase with a small server addition (GET /api/tags). The work involves: (1) upgrading the existing single-action FAB in __root.tsx to a mini menu with multiple actions, (2) building a full-screen catalog search overlay managed by UIStore, and (3) adding tag chip filtering connected to the existing GET /api/global-items?q=...&tags=... endpoint.

All patterns needed already exist in the codebase. The FAB lives in __root.tsx (lines 257-278), the overlay pattern is established in CreateThreadModal (fixed inset-0 z-50), the search with debounce exists in global-items/index.tsx, the card pattern is in GlobalItemCard.tsx, and Framer Motion is already a dependency for animations. The only net-new server work is a lightweight tags endpoint and registering the existing global-items route (currently unregistered in index.ts).

Primary recommendation: Extend existing patterns -- UIStore state slices, Framer Motion AnimatePresence for FAB menu animation, reuse debounce/search/card patterns from global-items page. No new libraries needed.

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: FAB globally visible on all pages (not just collection view). Position: fixed bottom-6 right-6
  • D-02: Tapping FAB opens mini menu: 2-3 labeled icon buttons fan out vertically above FAB. Backdrop dims slightly. Tapping backdrop or FAB again closes menu
  • D-03: Menu options: "Add to Collection" (package icon) + "Start New Thread" (search icon). On setups page only, third option "New Setup"
  • D-04: Both "Add to Collection" and "Start New Thread" open same full-screen catalog search overlay, with mode parameter ("collection" or "thread") stored in UIStore
  • D-05: "New Setup" triggers existing setup creation flow (no catalog search)
  • D-06: FAB should not appear on login page or public profile/setup pages (only authenticated routes)
  • D-07: Full-screen overlay (fixed inset-0 z-50) managed by UIStore state (catalogSearchOpen, catalogSearchMode), not a route change
  • D-08: Layout: back arrow (top-left, closes overlay) + large search input + context indicator text
  • D-09: Below search bar: horizontal scrollable row of tag chips for quick filtering
  • D-10: Results area: grid of compact cards showing brand + model, weight, price, owner count, and "Add" button
  • D-11: Search queries existing GET /api/global-items?q=...&tags=... endpoint
  • D-12: Empty state: helpful message when no results, with "Add Manually" link (Phase 22 wires this)
  • D-13: Loading state: skeleton cards matching result grid pattern
  • D-14: Tags fetched from new GET /api/tags endpoint (returns all tags, lightweight)
  • D-15: Horizontal scrollable row of rounded-full chips, tapping toggles active state, multiple tags (AND filtering)
  • D-16: Active chips: bg-blue-100 text-blue-700; inactive: bg-gray-100 text-gray-500
  • D-17: Quick-access chips show common/popular tags first
  • D-18: Reuse/adapt GlobalItemCard component pattern
  • D-19: Each card has "Add"/"+" button -- stub in Phase 20 (Phase 21 wires action)
  • D-20: Cards responsive: 1 column mobile, 2-3 on desktop
  • D-21: New UIStore state: catalogSearchOpen, catalogSearchMode ("collection" | "thread" | null)
  • D-22: New UIStore actions: openCatalogSearch(mode), closeCatalogSearch()
  • D-23: FAB menu state: fabMenuOpen, openFabMenu(), closeFabMenu()
  • D-24: New endpoint GET /api/tags returning { id, name }[]. Public, no auth

Claude's Discretion

  • Animation style for FAB menu (spring, ease, duration)
  • Exact tag chip ordering strategy
  • Card grid gap and sizing details
  • Whether to debounce search input (recommendation: yes, 300ms)
  • Skeleton card count during loading
  • Whether "Add Manually" link is visible in Phase 20 or deferred to Phase 22

Deferred Ideas (OUT OF SCOPE)

  • "Add Manually" link wiring -- Phase 22
  • Actual add-to-collection flow (confirmation step with category picker) -- Phase 21
  • Actual add-to-thread flow (instant candidate creation) -- Phase 21
  • Search result sorting/ordering options
  • Recent searches or search history
  • "Popular items" section when search is empty </user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
CATFLOW-01 FAB shows mini menu with "Add to Collection" and "Start Thread" globally, plus "New Setup" on setups page Existing FAB in __root.tsx (lines 257-278), UIStore pattern for state, Framer Motion for animations, useMatchRoute for setups page detection
CATFLOW-02 Full-screen catalog search with tag chip filtering Existing overlay pattern (CreateThreadModal), debounce pattern (global-items/index.tsx), useGlobalItems hook, tags schema in DB, GlobalItemCard component
</phase_requirements>

Standard Stack

Core (already installed)

Library Version Purpose Why Standard
React 19 UI framework Project standard
framer-motion ^12.38.0 FAB menu animations, overlay transitions Already used in collection/threads for AnimatePresence/motion
zustand (installed) UIStore for FAB and overlay state Project standard for UI state
@tanstack/react-query (installed) Data fetching for tags and global items Project standard
hono (installed) Tags API endpoint Project standard
drizzle-orm (installed) Tags query Project standard

Supporting

Library Version Purpose When to Use
tailwind CSS v4 (installed) All styling Project standard

Alternatives Considered

None -- all required libraries are already in the project.

Architecture Patterns

New Components

src/client/
  components/
    FabMenu.tsx           # FAB + mini menu overlay (extracted from __root.tsx)
    CatalogSearchOverlay.tsx  # Full-screen search overlay
    TagChips.tsx          # Horizontal scrollable tag chip row
    CatalogItemCard.tsx   # Adapted GlobalItemCard with Add button
  hooks/
    useTags.ts            # TanStack Query hook for GET /api/tags
  stores/
    uiStore.ts            # Extended with FAB + catalog search state
src/server/
  routes/
    tags.ts               # New: GET /api/tags
  services/
    tag.service.ts        # New: getAllTags()
  index.ts                # Register /api/tags AND /api/global-items routes

Pattern 1: UIStore State Extension

What: Add FAB menu and catalog search state slices to existing Zustand store When to use: This is the established project pattern for dialog/panel/overlay state Example:

// Extend UIState interface
fabMenuOpen: boolean;
openFabMenu: () => void;
closeFabMenu: () => void;

catalogSearchOpen: boolean;
catalogSearchMode: "collection" | "thread" | null;
openCatalogSearch: (mode: "collection" | "thread") => void;
closeCatalogSearch: () => void;

Pattern 2: FAB Visibility via useMatchRoute

What: Replace current collection-only FAB visibility with auth-aware global visibility When to use: The current FAB uses matchRoute to show only on collection gear tab. Phase 20 changes this to show on all authenticated routes except login and public pages. Example:

// Current logic (to be replaced):
const showFab = isCollection && (!collectionSearch || ...);

// New logic:
const isPublicRoute = location.pathname.startsWith("/users/") || location.pathname === "/login";
const showFab = isAuthenticated && !isPublicRoute;

Pattern 3: Full-Screen Overlay (CreateThreadModal pattern)

What: Fixed inset-0 z-50 overlay managed by UIStore boolean When to use: Established pattern in CreateThreadModal Key difference: Catalog search overlay is full-screen white (not a centered modal with backdrop). Uses bg-white not bg-black/50.

Pattern 4: Debounced Search (global-items/index.tsx pattern)

What: useState + useEffect timer for 300ms debounce on search input When to use: Exact pattern from existing catalog page, copy directly Example:

const [searchInput, setSearchInput] = useState("");
const [debouncedQuery, setDebouncedQuery] = useState("");

useEffect(() => {
  const timer = setTimeout(() => setDebouncedQuery(searchInput), 300);
  return () => clearTimeout(timer);
}, [searchInput]);

Pattern 5: Framer Motion FAB Menu Animation

What: AnimatePresence + motion.div for menu items fanning out vertically When to use: Framer Motion already imported in collection/threads Example:

import { AnimatePresence, motion } from "framer-motion";

// Menu items appear one by one with staggered animation
<AnimatePresence>
  {fabMenuOpen && (
    <motion.div
      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 }}
    >
      {/* menu items */}
    </motion.div>
  )}
</AnimatePresence>

Anti-Patterns to Avoid

  • Route-based overlay: Don't make the catalog search a route. It's managed via UIStore like all other dialogs in the project. Route changes would break the "open from anywhere" requirement.
  • Global state for search query: Don't store the search input in UIStore. Keep it local to CatalogSearchOverlay component (matches existing pattern in global-items/index.tsx).
  • Custom debounce hook: Don't create a generic useDebounce hook. The inline useState+useEffect pattern is already established and simple.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Animations CSS keyframe animations Framer Motion (AnimatePresence + motion) Already used, handles enter/exit, spring physics
Search debounce Custom debounce utility Inline useState+useEffect (300ms) Established pattern, 5 lines, no abstraction needed
Tag data fetching Manual fetch+cache TanStack Query useQuery hook Project standard, handles caching/stale/refetch
Overlay state management useState in __root Zustand UIStore slice Project standard for cross-component dialog state

Common Pitfalls

Pitfall 1: Global-Items Route Not Registered

What goes wrong: The GET /api/global-items endpoint returns 404 because globalItemRoutes exists in src/server/routes/global-items.ts but is NOT registered in src/server/index.ts. Why it happens: Phase 19 created the route file but the route registration line app.route("/api/global-items", globalItemRoutes) is missing from index.ts. How to avoid: First task must register the global-items route in index.ts. Also register the new tags route. Warning signs: 404 errors when searching in the overlay.

Pitfall 2: Z-Index Layering Conflicts

What goes wrong: FAB menu appears behind existing overlays, or catalog search overlay doesn't cover everything. Why it happens: Project uses layered z-indexes: z-20 (FAB), z-30 (backdrop), z-40 (panels), z-50 (modals). How to avoid: FAB stays at z-20, FAB menu backdrop at z-30, FAB menu items at z-40. Catalog search overlay at z-50 (same as modals -- it replaces the entire screen). When catalog search is open, FAB should be hidden. Warning signs: Visual stacking issues on mobile.

Pitfall 3: FAB Visible on Public Routes

What goes wrong: Unauthenticated users or public profile visitors see the FAB. Why it happens: Current FAB visibility check is collection-only. New global visibility needs explicit exclusion of public routes and unauthenticated state. How to avoid: Check both isAuthenticated AND !isPublicRoute. Public routes: /login, /users/*.

Pitfall 4: Scroll Lock When Overlay Is Open

What goes wrong: Background page scrolls while full-screen overlay is visible. Why it happens: Fixed overlay doesn't prevent body scroll on mobile. How to avoid: Add overflow-hidden to body when overlay is open, or use overflow-y-auto on the overlay container itself and ensure it captures all scroll events.

Pitfall 5: Tag Query Parameter Format

What goes wrong: Tag filtering sends wrong format to the API. Why it happens: API expects ?tags=tag1,tag2 (comma-separated names). Easy to accidentally send IDs or wrong separator. How to avoid: Tags endpoint returns { id, name }[]. When building the query, join selected tag names with commas. The existing searchGlobalItems service already parses this format.

Pitfall 6: Search Fires on Overlay Mount

What goes wrong: Opening the overlay triggers an immediate search for all items with empty query. Why it happens: useGlobalItems fires on mount with no query, fetching the entire catalog. How to avoid: Either pass enabled: false until user types something, or accept the initial load as intentional (shows browsable catalog). The CONTEXT.md implies browse-first is acceptable since D-09 shows tag filtering.

Code Examples

Tags Service (new file)

// src/server/services/tag.service.ts
import { db as prodDb } from "../../db/index.ts";
import { tags } from "../../db/schema.ts";

type Db = typeof prodDb;

export async function getAllTags(db: Db = prodDb) {
  return db.select({ id: tags.id, name: tags.name }).from(tags);
}

Tags Route (new file)

// src/server/routes/tags.ts
import { Hono } from "hono";
import { getAllTags } from "../services/tag.service.ts";

type Env = { Variables: { db?: any } };
const app = new Hono<Env>();

app.get("/", async (c) => {
  const db = c.get("db");
  const allTags = await getAllTags(db);
  return c.json(allTags);
});

export { app as tagRoutes };

useTags Hook (new file)

// src/client/hooks/useTags.ts
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";

interface Tag {
  id: number;
  name: string;
}

export function useTags() {
  return useQuery({
    queryKey: ["tags"],
    queryFn: () => apiGet<Tag[]>("/api/tags"),
    staleTime: 5 * 60 * 1000, // Tags rarely change, cache 5 min
  });
}

Extended useGlobalItems (tag support)

// Update src/client/hooks/useGlobalItems.ts
export function useGlobalItems(query?: string, tags?: string[]) {
  const params = new URLSearchParams();
  if (query) params.set("q", query);
  if (tags && tags.length > 0) params.set("tags", tags.join(","));
  const qs = params.toString();

  return useQuery({
    queryKey: ["global-items", query ?? "", tags ?? []],
    queryFn: () =>
      apiGet<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
  });
}

Skeleton Card (loading state)

// Reuse existing skeleton pattern from global-items/index.tsx
function SkeletonCard() {
  return (
    <div className="bg-white rounded-xl border border-gray-100 overflow-hidden animate-pulse">
      <div className="aspect-[4/3] bg-gray-100" />
      <div className="p-4 space-y-2">
        <div className="h-3 bg-gray-100 rounded w-16" />
        <div className="h-4 bg-gray-100 rounded w-32" />
        <div className="flex gap-1.5">
          <div className="h-5 bg-gray-100 rounded-full w-14" />
          <div className="h-5 bg-gray-100 rounded-full w-14" />
        </div>
      </div>
    </div>
  );
}

State of the Art

Old Approach Current Approach When Changed Impact
FAB only on collection gear tab FAB globally visible Phase 20 Must update visibility logic in __root.tsx
Single-action FAB (opens add panel) Multi-action FAB menu Phase 20 FAB click behavior changes from direct action to menu toggle
No catalog search overlay Full-screen overlay with tag filtering Phase 20 New UIStore state slices, new component tree

Validation Architecture

Test Framework

Property Value
Framework bun:test (service/route) + Playwright (E2E)
Config file bunfig.toml (bun test) / playwright.config.ts (E2E)
Quick run command bun test tests/routes/tags.test.ts
Full suite command bun test

Phase Requirements -> Test Map

Req ID Behavior Test Type Automated Command File Exists?
CATFLOW-01 FAB menu renders with correct options E2E bun run test:e2e Wave 0
CATFLOW-01 FAB visible on authenticated routes, hidden on public E2E bun run test:e2e Wave 0
CATFLOW-02 GET /api/tags returns all tags unit bun test tests/routes/tags.test.ts Wave 0
CATFLOW-02 Tag service returns tags from DB unit bun test tests/services/tag.service.test.ts Wave 0
CATFLOW-02 GET /api/global-items?tags=... filters correctly unit bun test tests/routes/global-items.test.ts Exists (Phase 19)
CATFLOW-02 Search overlay opens/closes via UIStore manual-only Visual verification N/A

Sampling Rate

  • Per task commit: bun test tests/routes/tags.test.ts && bun test tests/services/tag.service.test.ts
  • Per wave merge: bun test
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • tests/services/tag.service.test.ts -- covers tag retrieval
  • tests/routes/tags.test.ts -- covers GET /api/tags endpoint
  • Verify tests/routes/global-items.test.ts covers tag filtering (may already exist from Phase 19)

Open Questions

  1. Global-items route registration

    • What we know: The route file exists at src/server/routes/global-items.ts but is NOT registered in src/server/index.ts (no app.route("/api/global-items", ...) line)
    • What's unclear: Whether this was intentional (staged for Phase 20) or an oversight from Phase 19
    • Recommendation: Register it as part of Phase 20 server setup task, alongside the new tags route
  2. Owner count in search results

    • What we know: D-10 says cards show "owner count". The searchGlobalItems service returns basic fields but NOT owner count. Only getGlobalItemWithOwnerCount includes it (single item fetch).
    • What's unclear: Whether to add owner count to search results (requires a join/subquery) or show it only on detail pages
    • Recommendation: For Phase 20, omit owner count from search cards to keep search fast. The card already shows brand, model, weight, price, category. Owner count can be added in a follow-up if needed, or when clicking through to detail.

Project Constraints (from CLAUDE.md)

  • Styling: Tailwind CSS v4, tabs for indentation, double quotes (Biome)
  • State management: Zustand for UI state only, server data in React Query
  • API pattern: Hono routes with Zod validation, delegate to service functions
  • Services: Pure functions taking db instance, no HTTP awareness
  • Route registration: app.route("/api/...", routes) in src/server/index.ts
  • Path alias: @/* maps to ./src/*
  • Testing: Bun test runner for unit/integration, Playwright for E2E
  • Branching: Feature branch off Develop, merge back when complete

Sources

Primary (HIGH confidence)

  • src/client/routes/__root.tsx -- Current FAB implementation, route matching patterns
  • src/client/stores/uiStore.ts -- Zustand store structure, all existing state slices
  • src/client/components/CreateThreadModal.tsx -- Full-screen overlay pattern
  • src/client/components/GlobalItemCard.tsx -- Card component with badge pattern
  • src/client/routes/global-items/index.tsx -- Debounce pattern, skeleton cards, search UI
  • src/server/routes/global-items.ts -- Existing search endpoint with tag support
  • src/server/services/global-item.service.ts -- Search service with ILIKE + tag AND filtering
  • src/db/schema.ts -- Tags and globalItemTags table definitions
  • src/server/index.ts -- Route registration (global-items NOT registered)
  • package.json -- framer-motion ^12.38.0 confirmed installed

Metadata

Confidence breakdown:

  • Standard stack: HIGH -- all libraries already installed, no new dependencies
  • Architecture: HIGH -- all patterns exist in codebase, pure extension
  • Pitfalls: HIGH -- identified from direct code inspection (missing route registration, z-index layering, public route exclusion)

Research date: 2026-04-06 Valid until: 2026-05-06 (stable -- all internal code patterns)