Navigation Architecture
Last updated: 2026-10-07 (task-discovery section; earlier architecture retained)
How the dashboard shell navigation is structured: the data source of truth, the four nav surfaces, and the collapse/flyout mechanics. All nav is role- and platform-mode-filtered and white-label (no third-party branding).
Task-based discovery — 2026-10-07
Local implementation; full release checks and production acceptance remain open. Spec · verification · user guide.
case-tool-tasks.ts owns eight static editorial paths and their aliases. Matching normalizes text and requires meaningful phrase tokens; it performs no case analysis, network call or mutation. case-tools.ts applies curated visibility before matching, then ranks exact labels/slugs (100), lexical matches (60), and task steps (40 minus step index). Results can carry a task explanation. Case tools may regroup those results by lane.
Case tools renders task starters and search/reset within the existing case workspace. command-palette-content.tsx uses the shared matcher and a single displayed/keyboard result order, bounded selection, Home/End, active-option IDs, scroll-into-view and a retry state. Stale remote results are hidden during a new query. Selected text uses the foreground token; the accent stays in the outline to preserve contrast.
The generated index was repaired separately in 6400fe9f; keep display headings and curated labels intact. Task paths do not widen catalog visibility or replace destination authorization. There are no new APIs, migrations, environment variables or stored search history in this increment.
Source of truth — src/lib/nav/nav-config.ts
A single module defines the IA and the access rules:
PRIMARY_NAV: NavItem[]— the primary sidebar items (Home, Work, Clients,PAGE_TABS: Record<sectionKey, PageTab[]>— the sub-pages for each section,getSectionKey(pathname)— maps any path to its section key (exact tab match →getActivePrimaryHref(pathname)— which primary item to highlight.getVisiblePageTabs(tabs, { role, platformMode })+canShowPageTab(...)—isRouteActive(pathname, href)— active-state matching (/dashboardis exact;
Communications, Documents, Money, AI & Automate, Insights, Integrations) plus SETTINGS_NAV and SUPER_ADMIN_ITEM.
each tab carrying optional adminOnly / attorneyPlus / superAdminOnly / stage gating.
tab-prefix match → top-level route-prefix match → /dashboard/* falls back to insights). Case-detail pages return null (they use their own nav).
RBAC + platform-mode filtering. ADMIN_ONLY_PATHS is the authoritative set of admin-gated analytics routes.
everything else matches the href or a sub-path).
The dashboard's analytics mega-menu has its own grouping in src/lib/nav/dashboard-sections.ts (DASHBOARD_SECTIONS + getVisibleDashboardSections), since the Home section surfaces the 100+ /dashboard/* analytics pages grouped into 8 buckets rather than a flat PAGE_TABS list.
The Home dashboard now leads with SETTLEMENT_ENGINE_ROOMS (src/lib/nav/settlement-engine.ts): intake, medical truth, cited demand, negotiation, IOLTA. Each room opens /cases?room=<key> so the list matches the count (IOLTA includes settled matters waiting on a fail-closed debit). nextSettlementMove() names the next case-file CTA for every live status. Active Treatment opens cited demand to prepare; Demand Prep opens it to send. openCaseHref() uses that move when a room list or pipeline card is opened. Intake and Active Treatment land on medical chronology, which now writes a treatment, bill, or record on that page. The firm-level /trust ledger stays reachable from Money nav. Advanced analytics stay behind an explicit toggle. Sibling pages are hidden, not deleted.
Case workspace menus and tool index
case-nav-sections.ts presents seven menus: Intake, Evidence, Medical, Demand, Negotiation, Litigation and IOLTA. Intake includes overview, client communications and audit; Demand includes AI review; IOLTA includes money and disbursement. The underlying groups in case-detail-config.ts remain the route catalog. These menus are separate from Home's five settlement rooms and do not widen permissions or tool visibility.
Regenerate the tool index with npm run nav:case-tool-index, then check freshness with npm run nav:case-tool-index:check. The shared case-tool-heading.mjs parser preserves static h1, PageTitle and PageHeader wording; curated labels still take precedence. Verify parser behavior with node --test scripts/lib/case-tool-heading.test.mjs. The 6400fe9f receipt records 605 indexed tools and 109 valid navigation links; this index repair is newer than live 0ce1ccbe. See routing/index verification.
The four nav surfaces
| Surface | Component | Notes | |---|---|---| | Primary sidebar | components/layout/sidebar.tsx | Renders PRIMARY_NAV; supports a collapsed icon-rail mode | | Sidebar flyouts | components/layout/sidebar-nav-item.tsx | Per-item hover flyout of that section's PAGE_TABS | | Inner section rail | components/layout/inner-nav.tsx | Desktop contextual column of the active section's tabs | | Dashboard mega-menu | app/(dashboard)/dashboard/_components/dashboard-nav.tsx | Click-flyout chips for DASHBOARD_SECTIONS |
The shell layout (app/(dashboard)/layout.tsx) composes them: [primary sidebar] [InnerNav] [main]. On mobile the primary sidebar is a slide-over and the inner rail is replaced by the horizontal PageTabs strip (lg:hidden).
Flyout mechanics (sidebar)
SidebarNavItem renders the primary link plus an optional hover flyout:
- Portal, not absolute. The flyout is rendered with
createPortalto - Hover bridge.
onMouseEnteropens;onMouseLeaveschedules close after - No button-semantics warning. Sub-links are plain
<Link>s (an earlier
document.body and position: fixed, computed from the item's getBoundingClientRect() (left = rect.right + 6). This is required because the sidebar uses overflow-y-auto (→ overflow-x clipped), which would otherwise clip an in-flow flyout. CSS group-hover + a portal cannot be combined, hence the computed fixed position.
130ms; the flyout panel carries the same handlers so moving onto it keeps it open.
PopoverClose render={<Link>} raised a Base UI native-button a11y warning); flyout panels are solid bg-[oklch(0.17_0.008_265)] for legibility.
Collapsed icon rail
- State lives in
app/(dashboard)/layout.tsx:sidebarCollapsed, persisted to - The desktop
<aside>animatesw-[272px] ↔ w-[76px];Sidebarreceives - In collapsed mode
Sidebarreturns an icon-rail variant (logo, ⌘K search icon - The expanded footer gains a
‹collapse toggle. Mobile always renders the full
localStorage under casesolo-sidebar-collapsed, lazy-initialised in useState(() => …). Safe from SSR mismatch because the sidebar only renders client-side after the auth gate (it is never in the server HTML).
collapsed + onToggleCollapse.
that dispatches the palette keydown, centered nav icons, avatar, expand »). SidebarNavItem with collapsed hides the label/inline chevron and always opens a hover flyout carrying the label (and sub-items when present) so each icon stays self-describing.
(expanded) sidebar — collapse is desktop-only.
Adding to the nav
- New section page: add a
PageTabto the section'sPAGE_TABSarray (with - New dashboard analytics report: add a
DashboardLinkto the relevant - New top-level section: add to
PRIMARY_NAV, add aPAGE_TABS[key], map the
any RBAC flags). It appears in the inner rail, the sidebar flyout, and the command palette automatically.
DASHBOARD_SECTIONS entry (set adminOnly to match ADMIN_ONLY_PATHS).
route prefix in ROUTE_PREFIX_SECTIONS, and add a SECTION_LABELS entry.
Related: Dashboard Visual System for the surface, elevation, and typography primitives these components use.