Navigation
The shared sidebar/nav components from @repo/ui shown on a synthetic NavNode[] fixture. All four variants consume the same global NavNode interface from @repo/config — pick one for the whole app via the selector in the header.
Tree
Default. Hierarchical, sections always expanded, ~240px wide.Stacked
Collapsible section blocks. Toggle headers to fold/unfold.Top bar
Horizontal app bar. Sections as scrollable tabs with underline accent.SubNav (in-page sub-navigation)
A composable secondary nav. Drop it inside any Next.js layout.tsx (or a page) to give a sub-tree its own tabs — without the outer shell needing to know. Supports both route-based tabs (`/places/ksfo/albums`) and search-param tabs (`?filter=picks`). Active state resolves from the URL.
Route-based tabs
Each tab links to a different page; active matches longest pathname prefix.Search-param tabs
Each tab toggles a `?filter=` query, with counts and a disabled state.How nav trees get populated
Every app in next-web follows the same pipeline: PageConfig map + section subset → buildNavTree(pages, sections) → NavNode[]. Dynamic rows (Payload collections, CSV ingest, etc.) are merged into the same pages map server-side in each app's lib/nav-builder.ts. Below: the source shape, the result rendered, and the same pipeline with one dynamic row class appended.
Authoring shape
The universal PageConfig / SECTIONS schema apps author against.// Author pages as PageConfig (@repo/config)
const pages: Record<string, PageConfig> = {
"/": { title: "Explore", section: "Library", order: 0 },
"/albums": { title: "All Albums", section: "Albums", order: 0 },
...
}
// Pick the sections this app cares about
const sections = { Library: SECTIONS.Library, Albums: SECTIONS.Albums }
// Build the NavNode[] every variant consumes
const navTree = buildNavTree(pages, sections)
// → [{ id: "library", label: "Library",
// children: [{ id: "explore", path: "/", ... }] }, ...]
// Merge dynamic rows the same way (server-side, see nav-builder.ts)
for (const album of await fetchAlbums()) {
pages[`/albums/${album.slug}`] = {
title: album.title, section: "Albums", order: i++
}
}