From 9bb8f8faa2003fd5fe72da1e027c0b8baa00a131 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 10 Apr 2026 23:26:18 +0200 Subject: [PATCH] =?UTF-8?q?docs(27):=20research=20phase=20=E2=80=94=20top?= =?UTF-8?q?=20nav=20restructure=20and=20search=20bar=20rethink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../27-RESEARCH.md | 656 ++++++++++++++++++ 1 file changed, 656 insertions(+) create mode 100644 .planning/phases/27-top-nav-restructure-and-search-bar-rethink/27-RESEARCH.md diff --git a/.planning/phases/27-top-nav-restructure-and-search-bar-rethink/27-RESEARCH.md b/.planning/phases/27-top-nav-restructure-and-search-bar-rethink/27-RESEARCH.md new file mode 100644 index 0000000..68fff18 --- /dev/null +++ b/.planning/phases/27-top-nav-restructure-and-search-bar-rethink/27-RESEARCH.md @@ -0,0 +1,656 @@ +# 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 `` component does not support `onClick` preventDefault-style interception in a clean way — the pattern is to render a ` +)} +``` + +`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. + +```typescript +// TopNav: hide nav links and search on mobile + +
+ {/* search bar */} +
+ +// BottomTabBar: only show on mobile +
+ {/* tab items */} +
+``` + +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: + +```typescript +// FabMenu: add className prop or wrap in __root.tsx +{showFab && ( +
+ +
+)} +``` + +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 `` 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. + +```typescript +// 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 ( +
+ +
+ ); +} +``` + +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. + +```typescript +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`: + +```typescript +// In TopNav.tsx +const openCatalogSearch = useUIStore((s) => s.openCatalogSearch); + +
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" +> + + Search catalog... +
+``` + +Confidence: HIGH (source: existing `index.tsx` HeroSection pattern and `uiStore.ts`). + +### Pattern 8: Bottom Tab Bar with Framer Motion + +```typescript +// BottomTabBar.tsx +import { AnimatePresence, motion } from "framer-motion"; +// Simple entry animation on mount + +``` + +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 `` 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 `` 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 `` with `
`. 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) + +```typescript +// 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 ( + + ); + } + return ( + + {label} + + ); + } + + return ( +
+
+
+ {/* Logo */} + + + GearBox + + + {/* Desktop nav links (hidden on mobile) */} + + + {/* Desktop search + user (hidden on mobile, user avatar shown on mobile) */} +
+ {/* Search bar — desktop only */} +
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" + > + + Search catalog... +
+ {/* User menu / sign-in */} + {isAuthenticated ? ( + + ) : ( + + Sign in + + )} +
+
+
+
+ ); +} +``` + +### BottomTabBar.tsx skeleton + +```typescript +// 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 ( + +
+ + + Home + + {isAuthenticated ? ( + + + Collection + + ) : ( + + )} + {isAuthenticated ? ( + + + Setups + + ) : ( + + )} + +
+
+ ); +} +``` + +### __root.tsx changes + +```typescript +// Replace: +import { TotalsBar } from "../components/TotalsBar"; +// With: +import { TopNav } from "../components/TopNav"; +import { BottomTabBar } from "../components/BottomTabBar"; + +// In RootLayout return: +// Replace: +// With: + +// Wrap FabMenu to hide on mobile: +{showFab && ( +
+ +
+)} + +// Add after FabMenu: + +``` + +### collection/index.tsx tab removal + +```typescript +// 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 + +```typescript +// Remove HeroSection function entirely +// Remove HeroSection from LandingPage render +// Remove Search import from lucide-react +function LandingPage() { + return ( +
+ + + +
+ ); +} +// 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 `
` 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)