# Navigation & Layout — Implementation Plan

**Date:** 2026-03-04
**Audience:** Senior software engineers
**Scope:** `apps/admin` — checklist section 2 (Navigation & Layout)

---

## 1. Current State

### What exists

| Component | File | Status |
|-----------|------|--------|
| `AppSidebar` | `components/layout/sidebar/app-sidebar.tsx` | Done — renders header, nav groups, footer |
| `NavMain` | `components/layout/sidebar/nav-main.tsx` | Done — collapsible nav + flat nav via `isOverview` flag |
| `NavUser` | `components/layout/sidebar/nav-user.tsx` | Done — `UserButton` + name/email, collapses with sidebar |
| `NAV_LINKS` | `lib/constants/app.constants.ts` | Done — complete route tree (overview, navMain, users) |
| Dashboard layout | `app/(dashboard)/layout.tsx` | Partial — wires `SidebarProvider` + `SidebarInset`; header slot has only `SidebarTrigger` + `Separator` |
| ShadCN UI | `components/ui/sidebar`, `collapsible`, `breadcrumb`, `separator`, `tooltip`, `skeleton` | Installed |

### What is missing

| # | Checklist item | Gap |
|---|----------------|-----|
| 2.1 | Sidebar navigation | Functional but `isActive` is hardcoded in `NAV_LINKS`; active state is not driven by current URL |
| 2.2 | Breadcrumbs | Not implemented; header slot in `(dashboard)/layout.tsx` is empty after `SidebarTrigger` |
| 2.3 | Mobile-responsive shell | ShadCN `Sidebar` handles mobile via `Sheet` — the `SidebarTrigger` in the header is the hamburger. Behaviour needs verification and a polish pass |
| 2.4 | Active route highlighting | Blocked by 2.1 — `isActive` must come from `usePathname()`, not static data |

### Architecture overview (current)

```
(dashboard)/layout.tsx
└── SidebarProvider
    ├── AppSidebar                     ← collapsible="icon"
    │   ├── SidebarHeader (logo)
    │   ├── SidebarContent
    │   │   ├── NavMain (Platform)     ← overview flat links
    │   │   ├── NavMain (Application) ← collapsible groups
    │   │   └── NavMain (Users)        ← flat link
    │   ├── SidebarFooter
    │   │   └── NavUser (UserButton)
    │   └── SidebarRail
    └── SidebarInset
        ├── <header>
        │   ├── SidebarTrigger         ← hamburger on mobile, collapse toggle on desktop
        │   └── Separator
        │   └── [BREADCRUMB SLOT — empty]
        └── <main>{children}</main>
```

---

## 2. Target Architecture

```
(dashboard)/layout.tsx
└── SidebarProvider
    ├── AppSidebar                     ← unchanged structure
    │   ├── SidebarHeader (logo)
    │   ├── SidebarContent
    │   │   ├── NavMain (Platform)     ← active state from usePathname()
    │   │   ├── NavMain (Application) ← active state + auto-opens active group
    │   │   └── NavMain (Users)        ← active state from usePathname()
    │   ├── SidebarFooter
    │   │   └── NavUser
    │   └── SidebarRail
    └── SidebarInset
        ├── <header>
        │   ├── SidebarTrigger
        │   ├── Separator
        │   └── DynamicBreadcrumb      ← NEW: reads pathname → renders ShadCN Breadcrumb
        └── <main>{children}</main>
```

---

## 3. Design Decisions

### D1: Active state via `usePathname()` — not static data

`isActive` booleans in `NAV_LINKS` are unreliable (they are snapshots, not reactive). `NavMain` should call `usePathname()` internally and derive active state at render time.

- Flat links: `isActive = pathname === item.url`
- Collapsible groups: `isActive = pathname.startsWith(item.url)` — also controls `defaultOpen`
- Sub-items: `isActive = pathname === subItem.url`

`isActive` fields in `NAV_LINKS` should be removed to avoid confusion.

### D2: Breadcrumb is a single `DynamicBreadcrumb` component

The breadcrumb reads `usePathname()`, splits into segments, maps each to a human-readable label via a static `ROUTE_LABELS` map, and renders ShadCN's `Breadcrumb` primitive.

**Segment matching rules:**
- Known static segments → label from map (e.g. `"products"` → `"Products"`)
- Dynamic segments (Convex IDs, slugs) → displayed as-is for now; individual detail pages can override with a `<PageBreadcrumbItem>` context pattern in a future iteration

**Home segment:** A `<House />` icon link to `/` instead of the word "Dashboard".

### D3: Mobile shell — no new code needed

ShadCN's `Sidebar` primitive (`collapsible="icon"`) renders as an `offcanvas` sheet on mobile automatically (breakpoint `md`). The `SidebarTrigger` in the layout header is already the hamburger. No structural changes required — the plan documents this explicitly so it is not re-investigated.

### D4: `NAV_LINKS` remains the single source of truth for route structure

Active state logic is moved into the component, but route definitions (titles, URLs, icons, children) stay in `NAV_LINKS`. This keeps route changes in one place.

---

## 4. Implementation Phases

### Phase 1 — Active route highlighting (2.1 / 2.4)

**Goal:** Drive `isActive` from `usePathname()` in `NavMain`. Remove hardcoded `isActive` from `NAV_LINKS`.

#### 1.1 Update `NavMain` to read `usePathname()`

`NavMain` is already a client component (`"use client"`). Add `usePathname()` from `next/navigation`.

**Flat link items (overview / users groups):**
```tsx
const pathname = usePathname();
// ...
<SidebarMenuButton isActive={pathname === item.url}>
```

**Collapsible group items:**
```tsx
const isGroupActive = pathname.startsWith(item.url);
// defaultOpen driven by isGroupActive
<Collapsible defaultOpen={isGroupActive}>
  <SidebarMenuButton isActive={isGroupActive}>
  // ...
  {item.items?.map(subItem => (
    <SidebarMenuSubButton isActive={pathname === subItem.url}>
  ))}
```

**Edge case — Dashboard `/` link:** Use `pathname === "/"` (exact match only) so the Dashboard link is not active on every page.

#### 1.2 Remove `isActive` from `NAV_LINKS`

Remove the `isActive: true` fields from `navMain` items in `app.constants.ts`. The type definition in `NavMain` props should also drop `isActive` from the item shape (or keep it as optional and ignore it — removing is cleaner).

#### Files changed

| File | Action |
|------|--------|
| `src/components/layout/sidebar/nav-main.tsx` | Edit — add `usePathname()`, derive `isActive` for flat and collapsible items |
| `src/lib/constants/app.constants.ts` | Edit — remove `isActive: true` from `navMain` entries |

---

### Phase 2 — Dynamic breadcrumbs (2.2)

**Goal:** Render a context-aware breadcrumb trail in the dashboard header.

#### 2.1 Create route label map

Add to `lib/constants/app.constants.ts`:

```typescript
export const ROUTE_LABELS: Record<string, string> = {
  orders: "Orders",
  products: "Products",
  categories: "Categories",
  images: "Images",
  variants: "Variants",
  customers: "Customers",
  reviews: "Reviews",
  messages: "Messages",
  newsletter: "Newsletter",
  users: "Users",
  settings: "Settings",
  returns: "Returns",
};
```

Dynamic segments (Convex IDs) that don't match a key will be displayed as-is (e.g. `"jx7abc123"` → shown literally). This is acceptable for MVP.

#### 2.2 Create `DynamicBreadcrumb` component

Create `src/components/layout/DynamicBreadcrumb.tsx`:

```
"use client"

reads usePathname()
splits into segments: "/products/categories" → ["products", "categories"]
maps each segment to ROUTE_LABELS[segment] ?? segment
renders:
  - first segment: House icon link to "/"
  - intermediate segments: BreadcrumbLink
  - last segment: BreadcrumbPage (not a link)
  - BreadcrumbSeparator between each item
```

Uses ShadCN components: `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator` from `@/components/ui/breadcrumb`.

**Special case — root `/`:** No segments → render only "Dashboard" as `BreadcrumbPage` (no links needed).

#### 2.3 Wire `DynamicBreadcrumb` into dashboard layout header

Edit `app/(dashboard)/layout.tsx` — the header already has `SidebarTrigger` + `Separator`. Add `<DynamicBreadcrumb />` immediately after:

```tsx
<header className="flex h-12 ...">
  <div className="flex items-center gap-2">
    <SidebarTrigger className="-ml-1" />
    <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
    <DynamicBreadcrumb />
  </div>
</header>
```

#### Files changed

| File | Action |
|------|--------|
| `src/lib/constants/app.constants.ts` | Edit — add `ROUTE_LABELS` export |
| `src/components/layout/DynamicBreadcrumb.tsx` | Create |
| `src/app/(dashboard)/layout.tsx` | Edit — add `<DynamicBreadcrumb />` in header |

---

### Phase 3 — Mobile shell verification (2.3)

**Goal:** Confirm mobile sidebar behaviour works and document it. No new code expected.

#### 3.1 Verify mobile behavior

ShadCN's `Sidebar` primitive uses CSS variables and a `data-mobile` attribute (set by a `useIsMobile()` hook inside the primitive) to switch between:
- **Desktop (`lg:` and above):** Collapsible to icon rail (`collapsible="icon"`)
- **Mobile (below `lg:`):** Renders as a `Sheet` overlay; `SidebarTrigger` toggles it

The existing `(dashboard)/layout.tsx` already has `SidebarTrigger` in the header — this functions as the hamburger on mobile. No structural changes are required.

#### 3.2 What to check during implementation

- `SidebarTrigger` is visible and tappable on mobile viewports
- Sidebar opens as sheet overlay (not push) on mobile
- Nav links close the sheet on tap (ShadCN handles this via `useSidebar().setOpenMobile(false)` called inside `SidebarMenuButton` when `isMobile` is true)
- `NavUser` footer is visible and scrollable on small screens

#### Files changed

None — observation only. Document findings in `MEMORY.md` if a fix is needed.

---

## 5. File Inventory

### New files

| File | Phase | Purpose |
|------|-------|---------|
| `src/components/layout/DynamicBreadcrumb.tsx` | 2 | Context-aware breadcrumb trail |

### Modified files

| File | Phase | Change |
|------|-------|--------|
| `src/components/layout/sidebar/nav-main.tsx` | 1 | Add `usePathname()` — derive `isActive` for flat + collapsible items |
| `src/lib/constants/app.constants.ts` | 1 + 2 | Remove `isActive` from `navMain` entries; add `ROUTE_LABELS` map |
| `src/app/(dashboard)/layout.tsx` | 2 | Add `<DynamicBreadcrumb />` in header |

---

## 6. Breadcrumb Route Reference

| Path | Breadcrumb rendered |
|------|---------------------|
| `/` | Dashboard |
| `/orders` | 🏠 / Orders |
| `/orders/jx7abc` | 🏠 / Orders / jx7abc |
| `/products` | 🏠 / Products |
| `/products/categories` | 🏠 / Products / Categories |
| `/products/images` | 🏠 / Products / Images |
| `/products/variants` | 🏠 / Products / Variants |
| `/products/jx7abc` | 🏠 / Products / jx7abc |
| `/customers` | 🏠 / Customers |
| `/customers/reviews` | 🏠 / Customers / Reviews |
| `/customers/messages` | 🏠 / Customers / Messages |
| `/customers/newsletter` | 🏠 / Customers / Newsletter |
| `/users` | 🏠 / Users |
| `/settings` | 🏠 / Settings |
| `/returns` | 🏠 / Returns |

Dynamic segments (IDs/slugs not in `ROUTE_LABELS`) render their raw value. Future detail pages can introduce a breadcrumb context or slot pattern to override with a fetched entity name.

---

## 7. Implementation Order & Effort

| Phase | Description | Effort |
|-------|-------------|--------|
| 1 | Active route highlighting | ~30 min |
| 2 | Dynamic breadcrumbs | ~45 min |
| 3 | Mobile shell verification | ~15 min |

Total estimated effort: **~1.5 hours**.

Phases 1 and 2 are independent and can be worked in parallel.
