Files
GearBox/.planning/phases/27-top-nav-restructure-and-search-bar-rethink/27-RESEARCH.md

32 KiB

Phase 27: Top Nav Restructure & Search Bar Rethink - Research

Researched: 2026-04-10 Domain: React navigation restructure, TanStack Router file-based routing, Tailwind CSS v4 responsive layout, Framer Motion animations Confidence: HIGH

Summary

This phase replaces the minimal TotalsBar (54 lines, logo + user menu only) with a full persistent navigation bar, adds a mobile bottom tab bar, removes the landing page hero section, and elevates Setups to a top-level route. All the required building blocks already exist in the codebase: UserMenu, AuthPromptModal, CatalogSearchOverlay, FabMenu, and LucideIcon. No new libraries are needed.

The core technical challenge is the conditional routing behavior: nav links are visible to anonymous users, but clicking Collection or Setups while anonymous must intercept navigation and fire openAuthPrompt() from uiStore instead of calling navigate(). TanStack Router's <Link> component does not support onClick preventDefault-style interception in a clean way — the pattern is to render a <button> styled as a link that calls openAuthPrompt() for anon users, or use <Link> with an onClick that short-circuits navigation.

The setups route currently has no index page — src/client/routes/setups/ contains only $setupId.tsx. A new src/client/routes/setups/index.tsx must be created, which simply renders <SetupsView> (the component already exists at src/client/components/SetupsView.tsx). The collection route must drop the "setups" tab from its TAB_ORDER and TAB_LABELS constants and update the Zod search schema to remove "setups" as a valid enum value.

Primary recommendation: Build a single TopNav.tsx component to replace TotalsBar in __root.tsx, and a BottomTabBar.tsx for mobile. Both live in src/client/components/. Use Tailwind md: breakpoint to switch between them. Add a setups/index.tsx route. Surgical edits to collection/index.tsx and routes/index.tsx.

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: Persistent top nav bar replaces the current TotalsBar. Contains: logo ("GearBox" with package icon), section links (Home, Collection, Setups), a catalog search bar, and user avatar (authenticated) or "Sign in" link (anonymous).
  • D-02: All nav links are visible to both authenticated and anonymous users. Clicking Collection or Setups while anonymous triggers the existing AuthPromptModal instead of navigating.
  • D-03: Active section is visually indicated in the nav (current page highlighting).
  • D-04: Setups is elevated to a top-level nav section with its own route. It is no longer a tab inside Collection.
  • D-05: Collection page keeps pill tab navigation but drops to two tabs: Gear and Planning. The Setups tab is removed from Collection.
  • D-06: Threads (Planning) remain nested inside Collection — not elevated to top-level.
  • D-07: The nav bar includes a persistent search input/button that always triggers global catalog search via the existing CatalogSearchOverlay, regardless of which page the user is on.
  • D-08: Collection and Setups pages retain their existing inline search/filter inputs for local filtering. The nav search bar is always catalog-global.
  • D-09: The landing page hero section (heading, subtitle, search bar, "Go to Collection" link) is removed entirely. The nav search bar replaces it as the catalog search entry point.
  • D-10: With the hero removed, the landing page starts directly with content sections: Popular Setups, Recently Added Items, Trending Categories. No introductory text or hero area.
  • D-11: The "Go to Collection" link from the hero is no longer needed — Collection is now a persistent nav link.
  • D-12: On mobile (narrow screens), the top bar shows only the logo and user avatar/sign-in.
  • D-13: Navigation moves to a fixed bottom tab bar with 4 items: Home, Collection, Setups, Search. Each uses a Lucide icon with a short label below.
  • D-14: Tapping the Search tab icon opens the CatalogSearchOverlay.
  • D-15: The bottom tab bar replaces the FAB on mobile — the FAB is hidden when the bottom tab bar is visible (search and add-to-collection flows are now accessible via the tab bar and overlay).
  • D-16: No changes to the CatalogSearchOverlay UI or behavior. Same full-page takeover below the nav bar. Same tag filtering, grid/list toggle, weight/price range filters, manual entry fallback.
  • D-17: The overlay is now triggered from the nav search bar (desktop) or bottom tab bar search icon (mobile) instead of from the landing page hero or FAB menu.

Claude's Discretion

  • Exact responsive breakpoint for switching between top nav and bottom tab bar
  • Nav link styling (text links, pill buttons, underline indicators)
  • Search bar appearance in nav (full input field vs compact icon that expands)
  • Bottom tab bar icon choices (specific Lucide icons for each section)
  • Animation for bottom tab bar / overlay transitions
  • Whether the "GearBox" logo text is hidden on mobile top bar to save space
  • FAB behavior on desktop (keep as-is or consolidate into nav)

Deferred Ideas (OUT OF SCOPE)

  • Blended local+global search — When searching from Collection, show local gear first then global catalog results. Future phase.
  • Setup page redesign — Revisit the Setups page layout. Backlog item.
  • Add manufacturer entity with brand details
  • Fix item image not showing on collection overview
  • Add cursor pointer to all clickable links
  • Investigate slow image loading
  • Fix storage service tests </user_constraints>

Standard Stack

Core

Library Version Purpose Why Standard
@tanstack/react-router ^1.167.0 File-based routing, <Link>, useLocation, useMatchRoute Already in use; active route detection built-in
framer-motion ^12.38.0 Bottom tab bar entry animation, tab transitions Already in use throughout app
tailwindcss ^4.2.1 Responsive layout (md:hidden, md:flex) Already in use; v4 in project
zustand ^5.0.11 openCatalogSearch(), openAuthPrompt() from uiStore Already controls all overlay state
lucide-react ^0.577.0 Bottom tab bar icons via LucideIcon wrapper Already the app icon system

Supporting

No new packages required. All needed tools are already installed.

Installation: No new packages needed.

Architecture Patterns

src/client/
├── components/
│   ├── TopNav.tsx           # NEW — replaces TotalsBar (desktop nav bar)
│   ├── BottomTabBar.tsx     # NEW — mobile fixed bottom tab bar
│   ├── TotalsBar.tsx        # DELETED — replaced by TopNav
│   ├── FabMenu.tsx          # MODIFIED — hidden on mobile (md:block only)
│   └── ...existing...
├── routes/
│   ├── __root.tsx           # MODIFIED — swap TotalsBar for TopNav, add BottomTabBar
│   ├── index.tsx            # MODIFIED — remove HeroSection
│   ├── collection/
│   │   └── index.tsx        # MODIFIED — drop "setups" tab
│   └── setups/
│       ├── index.tsx        # NEW — renders SetupsView
│       └── $setupId.tsx     # unchanged

Pattern 1: Active Route Detection with useMatchRoute

TanStack Router's useMatchRoute hook returns a truthy match object when the current route matches. Use it to drive active link styling in TopNav.

// In TopNav.tsx
const matchRoute = useMatchRoute();
const isHome = !!matchRoute({ to: "/" });
const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });

This pattern is already used in __root.tsx (see isDashboard, isSetupsPage). Confidence: HIGH (source: existing codebase).

For Collection and Setups nav links, render them differently based on auth state. For anonymous users, an onClick prevents navigation and fires the modal instead.

// In TopNav.tsx
const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user;
const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);

// For each protected nav link:
{isAuthenticated ? (
  <Link to="/collection" className={linkClass(isCollection)}>Collection</Link>
) : (
  <button
    type="button"
    onClick={openAuthPrompt}
    className={linkClass(false)}
  >
    Collection
  </button>
)}

AuthPromptModal already subscribes to showAuthPrompt from uiStore and renders itself. No props needed. Confidence: HIGH (source: existing AuthPromptModal.tsx, uiStore.ts).

Pattern 3: Desktop/Mobile Layout Split with Tailwind

The breakpoint choice (Claude's discretion) should be md (768px) based on the existing app pattern (sm:px-6 lg:px-8 used throughout). Desktop: top nav links + search visible. Mobile: only logo + avatar in top bar, nav in bottom tab bar.

// TopNav: hide nav links and search on mobile
<nav className="hidden md:flex items-center gap-6">
  {/* nav links */}
</nav>
<div className="hidden md:flex ...">
  {/* search bar */}
</div>

// BottomTabBar: only show on mobile
<div className="fixed bottom-0 left-0 right-0 md:hidden z-20 ...">
  {/* tab items */}
</div>

Confidence: HIGH (source: existing Tailwind patterns in codebase).

Pattern 4: FAB Hidden on Mobile

In __root.tsx, the showFab condition already gates FAB rendering. Add a CSS class to limit it to md: and above:

// FabMenu: add className prop or wrap in __root.tsx
{showFab && (
  <div className="hidden md:block">
    <FabMenu isSetupsPage={isSetupsPage} />
  </div>
)}

Alternatively, add className support to FabMenu to accept hidden md:block. Confidence: HIGH.

Pattern 5: New setups/index.tsx Route

The setups/ directory has only $setupId.tsx. Create setups/index.tsx to make /setups a valid TanStack Router route. Render <SetupsView> directly (the component already exists). The route is currently public (no auth wall at the route level — SetupsView handles auth for mutations). The isPublicRoute check in __root.tsx does NOT include /setups (only /setups/), so adding /setups as a nav destination for anonymous users will require adding it to isPublicRoute OR relying on the AuthPromptModal pattern (D-02) where anonymous users never reach the route.

// src/client/routes/setups/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { SetupsView } from "../../components/SetupsView";

export const Route = createFileRoute("/setups/")({
  component: SetupsPage,
});

function SetupsPage() {
  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
      <SetupsView />
    </div>
  );
}

Confidence: HIGH (source: existing route pattern in collection/index.tsx).

Pattern 6: Collection Tab Simplification

Remove "setups" from TAB_ORDER and TAB_LABELS in collection/index.tsx. Update the Zod search schema catch default. Any existing bookmarked URLs with ?tab=setups will gracefully fall through to the catch("gear") default.

const TAB_ORDER = ["gear", "planning"] as const;
const TAB_LABELS: Record<(typeof TAB_ORDER)[number], string> = {
  gear: "Gear",
  planning: "Planning",
};
const searchSchema = z.object({
  tab: z.enum(["gear", "planning"]).catch("gear"),
});

Confidence: HIGH.

Pattern 7: Search Bar in TopNav (Desktop)

The search bar in the nav should be a clickable element that calls openCatalogSearch("collection"). Use the same clickable div pattern from the existing HeroSection:

// In TopNav.tsx
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);

<div
  onClick={() => openCatalogSearch("collection")}
  role="button"
  tabIndex={0}
  onKeyDown={(e) => e.key === "Enter" && openCatalogSearch("collection")}
  className="flex items-center gap-2 px-3 py-1.5 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:border-gray-300 transition-all"
>
  <LucideIcon name="search" size={16} className="text-gray-400" />
  <span className="text-sm text-gray-400 hidden lg:block">Search catalog...</span>
</div>

Confidence: HIGH (source: existing index.tsx HeroSection pattern and uiStore.ts).

Pattern 8: Bottom Tab Bar with Framer Motion

// BottomTabBar.tsx
import { AnimatePresence, motion } from "framer-motion";
// Simple entry animation on mount
<motion.div
  initial={{ y: 20, opacity: 0 }}
  animate={{ y: 0, opacity: 1 }}
  transition={{ duration: 0.2, ease: "easeOut" }}
  className="fixed bottom-0 left-0 right-0 md:hidden z-20 bg-white border-t border-gray-100 pb-safe"
>

Icons (Claude's discretion — recommendations):

  • Home: home
  • Collection: package
  • Setups: layers (or briefcase)
  • Search: search

Each tab has icon + label, active state highlighted with text-gray-900 vs text-gray-400. Confidence: HIGH (source: existing framer-motion usage, LucideIcon wrapper).

Anti-Patterns to Avoid

  • Using <Link> with e.preventDefault() for auth interception: TanStack Router links fire navigation before React event handlers can intercept reliably. Use conditional render (Link vs button) instead.
  • Putting auth logic in the setups route loader: D-02 says anon users see the nav link but get the auth modal when clicking. The route itself should be reachable (for future public setups browsing). Gate creation/edit actions in the page, not the route guard.
  • Importing lucide-react icons directly: The project pattern uses <LucideIcon name="..." /> via src/client/lib/iconData.tsx. Never import from lucide-react directly in components.
  • Duplicating search trigger logic: There is one openCatalogSearch() function in uiStore. Both the desktop nav search and mobile bottom tab bar Search icon call the same function. Don't create a second overlay or a second state.
  • Editing routeTree.gen.ts manually: It is auto-generated by TanStack Router. Adding setups/index.tsx will auto-update it on next bun run dev.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Active route detection Custom location.pathname.startsWith() checks useMatchRoute from @tanstack/react-router Already used in __root.tsx; handles nested routes correctly
Auth-gated nav Custom auth middleware or route guards Conditional render (Link vs button) + openAuthPrompt() AuthPromptModal already exists and is wired to uiStore
Search overlay trigger New search overlay or new state openCatalogSearch("collection") from uiStore Overlay already exists at CatalogSearchOverlay.tsx (849 lines — no changes needed)
Icon rendering Direct SVG or lucide-react imports LucideIcon from lib/iconData.tsx Project convention; ensures curated icon set consistency
Mobile nav animations CSS transitions Framer Motion (already installed) Consistent with existing animation patterns in FabMenu

Key insight: Every primitive needed (auth state, overlay state, icon system, animation library, modal components) already exists. This phase is pure composition and restructuring — zero new dependencies.

Common Pitfalls

Pitfall 1: setups/index.tsx Route Not Recognized

What goes wrong: Creating setups/index.tsx but the route tree auto-generation hasn't run, leaving /setups as a 404 during development. Why it happens: TanStack Router generates routeTree.gen.ts at build/dev startup. File creation during a running dev server may not immediately trigger regeneration depending on Vite config. How to avoid: Restart the dev server after creating setups/index.tsx. Verify the new route appears in routeTree.gen.ts. Warning signs: Console error "Route not found: /setups" or blank page at /setups.

Pitfall 2: isPublicRoute Check Missing /setups

What goes wrong: Anonymous users trigger the AuthPromptModal when clicking Setups nav, but if they somehow navigate directly to /setups (e.g., back button after login), __root.tsx redirects them to /login because /setups is not in isPublicRoute. Why it happens: The current isPublicRoute check only includes /setups/ (with trailing slash, for setup detail pages). The new /setups index is not covered. How to avoid: Add location.pathname === "/setups" to the isPublicRoute check in __root.tsx, OR restrict the Setups nav link to authenticated users only (contradicts D-02). Given D-02 says all links are visible to anon users (but trigger AuthPromptModal), the safest approach is to keep the auth interception at the nav level AND make /setups a public route so direct navigation doesn't hard-redirect. Warning signs: Anon user clicks Setups, modal appears, logs in, back-navigates, gets sent to /login again.

Pitfall 3: FAB Bottom Position Conflicts with Bottom Tab Bar

What goes wrong: On mobile, the FAB (bottom-6 right-6) overlaps with the bottom tab bar if both are visible simultaneously. Why it happens: D-15 says FAB is hidden when bottom tab bar is visible. If the hidden md:block wrapper is applied incorrectly or forgotten, both render. How to avoid: In __root.tsx, wrap <FabMenu> with <div className="hidden md:block">. Verify on mobile viewport that FAB is gone. Warning signs: FAB overlapping tab bar on narrow screens.

Pitfall 4: CatalogSearchOverlay z-index Fighting Bottom Tab Bar

What goes wrong: The CatalogSearchOverlay renders below the bottom tab bar, making the tab bar visible on top of the search overlay. Why it happens: CatalogSearchOverlay and BottomTabBar both use high z-index. Current overlay z-index needs checking. How to avoid: Ensure BottomTabBar uses z-20 and CatalogSearchOverlay uses z-30 or higher. The overlay already uses fixed inset-0 — verify its z-index is above the tab bar. Warning signs: Bottom tab bar visible when search overlay is open.

Pitfall 5: Collection URL with ?tab=setups Breaks After Tab Removal

What goes wrong: Existing links, bookmarks, or tests that reference /collection?tab=setups stop working after the tab is removed. Why it happens: The Zod catch("gear") will handle it gracefully in the router (redirects to gear tab), but E2E tests may assert on the old three-tab structure. How to avoid: Update e2e/dashboard.spec.ts and e2e/collection.spec.ts — the existing tests assert on "Collection, Planning, and Setups card headings" and the old tab structure. Update or remove those assertions. Warning signs: E2E test failures asserting Setups tab inside Collection.

Pitfall 6: useAuth() During SSR / Hydration Flash

What goes wrong: Nav renders "Sign in" on first paint even for authenticated users, causing a flash. Why it happens: useAuth() is async (React Query). auth.isLoading is true on first render. The existing TotalsBar has this same behavior — it's acceptable in this app. How to avoid: Match existing TotalsBar behavior. Don't add special hydration handling unless this becomes a visible problem. The flash is consistent with current UX. Warning signs: Nav flickers from "Sign in" to avatar on page load. Acceptable if it matches current behavior.

Code Examples

TopNav.tsx skeleton (desktop + mobile top bar)

// src/client/components/TopNav.tsx
// Source: derived from TotalsBar.tsx pattern + __root.tsx matchRoute pattern
import { Link, useMatchRoute } from "@tanstack/react-router";
import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { UserMenu } from "./UserMenu";

export function TopNav() {
  const { data: auth } = useAuth();
  const isAuthenticated = !!auth?.user;
  const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
  const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
  const matchRoute = useMatchRoute();

  const isHome = !!matchRoute({ to: "/" });
  const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
  const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });

  function navLinkClass(active: boolean) {
    return `text-sm font-medium transition-colors ${
      active ? "text-gray-900" : "text-gray-500 hover:text-gray-700"
    }`;
  }

  function NavLinkOrButton({
    label,
    to,
    active,
    isProtected,
  }: {
    label: string;
    to: string;
    active: boolean;
    isProtected: boolean;
  }) {
    if (isProtected && !isAuthenticated) {
      return (
        <button
          type="button"
          onClick={openAuthPrompt}
          className={navLinkClass(false)}
        >
          {label}
        </button>
      );
    }
    return (
      <Link to={to} className={navLinkClass(active)}>
        {label}
      </Link>
    );
  }

  return (
    <div className="sticky top-0 z-10 bg-white border-b border-gray-100">
      <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div className="flex items-center justify-between h-14">
          {/* Logo */}
          <Link
            to="/"
            className="flex items-center gap-2 text-lg font-semibold text-gray-900 hover:text-gray-600 transition-colors"
          >
            <LucideIcon name="package" size={20} className="text-gray-500" />
            <span>GearBox</span>
          </Link>

          {/* Desktop nav links (hidden on mobile) */}
          <nav className="hidden md:flex items-center gap-6">
            <Link to="/" className={navLinkClass(isHome)}>Home</Link>
            <NavLinkOrButton label="Collection" to="/collection" active={isCollection} isProtected />
            <NavLinkOrButton label="Setups" to="/setups" active={isSetups} isProtected />
          </nav>

          {/* Desktop search + user (hidden on mobile, user avatar shown on mobile) */}
          <div className="flex items-center gap-3">
            {/* Search bar — desktop only */}
            <div
              onClick={() => openCatalogSearch("collection")}
              role="button"
              tabIndex={0}
              onKeyDown={(e) => e.key === "Enter" && openCatalogSearch("collection")}
              className="hidden md:flex items-center gap-2 px-3 py-1.5 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:border-gray-300 transition-all"
            >
              <LucideIcon name="search" size={16} className="text-gray-400" />
              <span className="text-sm text-gray-400 hidden lg:inline">Search catalog...</span>
            </div>
            {/* User menu / sign-in */}
            {isAuthenticated ? (
              <UserMenu />
            ) : (
              <Link to="/login" className="text-xs text-gray-500 hover:text-gray-700 transition-colors">
                Sign in
              </Link>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

BottomTabBar.tsx skeleton

// src/client/components/BottomTabBar.tsx
// Source: FabMenu.tsx framer-motion pattern + uiStore
import { Link, useMatchRoute } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";

export function BottomTabBar() {
  const { data: auth } = useAuth();
  const isAuthenticated = !!auth?.user;
  const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
  const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
  const matchRoute = useMatchRoute();

  const isHome = !!matchRoute({ to: "/" });
  const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
  const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });

  const tabClass = (active: boolean) =>
    `flex flex-col items-center gap-0.5 py-2 px-3 text-xs font-medium transition-colors ${
      active ? "text-gray-900" : "text-gray-400"
    }`;

  return (
    <motion.div
      initial={{ y: 20, opacity: 0 }}
      animate={{ y: 0, opacity: 1 }}
      transition={{ duration: 0.2, ease: "easeOut" }}
      className="fixed bottom-0 left-0 right-0 md:hidden z-20 bg-white border-t border-gray-100"
    >
      <div className="flex justify-around items-center">
        <Link to="/" className={tabClass(isHome)}>
          <LucideIcon name="home" size={20} />
          <span>Home</span>
        </Link>
        {isAuthenticated ? (
          <Link to="/collection" className={tabClass(isCollection)}>
            <LucideIcon name="package" size={20} />
            <span>Collection</span>
          </Link>
        ) : (
          <button type="button" onClick={openAuthPrompt} className={tabClass(false)}>
            <LucideIcon name="package" size={20} />
            <span>Collection</span>
          </button>
        )}
        {isAuthenticated ? (
          <Link to="/setups" className={tabClass(isSetups)}>
            <LucideIcon name="layers" size={20} />
            <span>Setups</span>
          </Link>
        ) : (
          <button type="button" onClick={openAuthPrompt} className={tabClass(false)}>
            <LucideIcon name="layers" size={20} />
            <span>Setups</span>
          </button>
        )}
        <button
          type="button"
          onClick={() => openCatalogSearch("collection")}
          className={tabClass(false)}
        >
          <LucideIcon name="search" size={20} />
          <span>Search</span>
        </button>
      </div>
    </motion.div>
  );
}

__root.tsx changes

// Replace:
import { TotalsBar } from "../components/TotalsBar";
// With:
import { TopNav } from "../components/TopNav";
import { BottomTabBar } from "../components/BottomTabBar";

// In RootLayout return:
// Replace: <TotalsBar {...totalsBarProps} />
// With:    <TopNav />

// Wrap FabMenu to hide on mobile:
{showFab && (
  <div className="hidden md:block">
    <FabMenu isSetupsPage={isSetupsPage} />
  </div>
)}

// Add after FabMenu:
<BottomTabBar />

collection/index.tsx tab removal

// Remove "setups" from TAB_ORDER, TAB_LABELS, and Zod schema
// Remove SetupsView import
// Remove tab === "setups" conditional render
const TAB_ORDER = ["gear", "planning"] as const;
const TAB_LABELS: Record<(typeof TAB_ORDER)[number], string> = {
  gear: "Gear",
  planning: "Planning",
};
const searchSchema = z.object({
  tab: z.enum(["gear", "planning"]).catch("gear"),
});

routes/index.tsx hero removal

// Remove HeroSection function entirely
// Remove HeroSection from LandingPage render
// Remove Search import from lucide-react
function LandingPage() {
  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <PopularSetupsSection />
      <RecentItemsSection />
      <TrendingCategoriesSection />
    </div>
  );
}
// Remove: openCatalogSearch from useUIStore (no longer needed in this file)
// Remove: useAuth import (no longer needed in this file)

State of the Art

Old Approach Current Approach When Changed Impact
Hero-based catalog entry Nav-persistent search bar This phase Catalog accessible from any page, not just landing page
Setups as Collection tab Setups as top-level route This phase Setups gets own URL, bookmarkable, mobile tab bar includes it
FAB for all mobile actions Bottom tab bar for nav, FAB only on desktop This phase Standard mobile pattern — iOS/Android tab bar convention
TotalsBar (logo + user) TopNav (logo + links + search + user) This phase Full navigation affordance for multi-section app

No deprecated patterns: The transition follows standard React + TanStack Router conventions throughout.

Open Questions

  1. layers icon availability in the curated LucideIcon set

    • What we know: iconData.tsx exports a curated subset of 119 Lucide icons. The EMOJI_TO_ICON_MAP doesn't include layers.
    • What's unclear: Whether layers is in the exported set. The full icons object from lucide-react is imported, so any icon name should work via LucideIcon (it passes the name to the icons lookup) — but the comment says "119 curated" icons.
    • Recommendation: Check iconData.tsx for the full export or simply try layers — if it fails silently, use briefcase or grid-2x2 as fallback. The planner should note this as a quick verify step.
  2. Body padding-bottom for bottom tab bar

    • What we know: The bottom tab bar is fixed bottom-0 so it overlays page content. On mobile, the last content may be obscured by the tab bar.
    • What's unclear: The exact height of the tab bar (approximately 60-64px with icons + labels + padding).
    • Recommendation: Add pb-20 md:pb-0 to the root <div className="min-h-screen bg-gray-50"> in __root.tsx to prevent content being hidden behind the tab bar.
  3. openCatalogSearch mode parameter from TopNav

    • What we know: openCatalogSearch takes "collection" | "thread". From the nav, it should always be "collection".
    • What's unclear: Whether calling it in "collection" mode when on a thread detail page is correct behavior (D-07 says it's always catalog-global).
    • Recommendation: Always pass "collection" from the nav. The mode only affects what happens after the user selects a catalog item (add to collection vs add to thread). A user on a thread page who opens search from the nav would get the "add to collection" flow, not "add to thread" — this is a reasonable simplification per D-07 and D-08.

Environment Availability

Step 2.6: SKIPPED (no external dependencies — purely client-side React component restructuring, no new CLI tools, services, or runtimes required).

Validation Architecture

Test Framework

Property Value
Framework Playwright (E2E) + Bun test runner (unit)
Config file playwright.config.ts (E2E), built-in Bun test runner
Quick run command bun test tests/
Full suite command bun run test:e2e

Phase Requirements → Test Map

Behavior Test Type Automated Command File Exists?
Top nav renders logo, Home/Collection/Setups links, search E2E smoke bunx playwright test e2e/dashboard.spec.ts Partial — needs update
Clicking Collection while anon triggers AuthPromptModal E2E bunx playwright test e2e/dashboard.spec.ts Wave 0
Mobile bottom tab bar shows 4 items E2E (mobile viewport) bunx playwright test e2e/dashboard.spec.ts Wave 0
Landing page has no hero section E2E bunx playwright test e2e/dashboard.spec.ts Partial — existing test checks for heading, needs update
/setups route renders SetupsView E2E bunx playwright test e2e/collection.spec.ts Wave 0
Collection page has only Gear and Planning tabs E2E bunx playwright test e2e/collection.spec.ts needs update

Sampling Rate

  • Per task commit: bun test tests/ (unit only — fast)
  • Per wave merge: bun run test:e2e (full E2E suite)
  • Phase gate: Full E2E suite green before /gsd:verify-work

Wave 0 Gaps

  • e2e/dashboard.spec.ts — update existing tests: remove assertions about hero heading "Discover Gear", add assertion for top nav presence, update "GearBox heading" to check nav bar (not h1)
  • e2e/dashboard.spec.ts — add: anon user clicking Collection nav triggers auth modal
  • e2e/dashboard.spec.ts — add: mobile viewport bottom tab bar test (use Playwright page.setViewportSize)
  • e2e/collection.spec.ts — update: remove Setups tab assertions, add /setups route navigation test

(Note: tests/ unit tests cover service-level logic — no unit tests needed for this pure UI restructuring phase. E2E tests are the validation layer.)

Sources

Primary (HIGH confidence)

  • Existing codebase: TotalsBar.tsx, __root.tsx, FabMenu.tsx, uiStore.ts, AuthPromptModal.tsx, UserMenu.tsx, collection/index.tsx, routes/index.tsx — direct source inspection
  • Existing codebase: SetupsView.tsx, setups/$setupId.tsx — route structure verified

Secondary (MEDIUM confidence)

  • TanStack Router file-based routing conventions — inferred from existing route structure (collection/index.tsx, setups/$setupId.tsx)
  • Framer Motion v12 entry animation pattern — inferred from FabMenu.tsx usage

Tertiary (LOW confidence)

  • None — all findings backed by direct codebase inspection

Metadata

Confidence breakdown:

  • Standard stack: HIGH — all libraries already in use, versions from package.json
  • Architecture: HIGH — patterns derived directly from existing code
  • Pitfalls: HIGH — derived from direct code analysis (isPublicRoute, z-index, tab removal implications)

Research date: 2026-04-10 Valid until: 2026-05-10 (stable codebase; no external API dependencies)