Patterns
Navigation
The sidebar shell's two nav pieces: a primary icon rail + contextual sidebar (side nav), and the persistent header bar above it (top nav).
Side nav
A 64px primary icon rail (one icon per top-level section, always visible) next to a 286px contextual sidebar (only the active section's own children), next to the page's main content. Clicking a rail icon either switches the contextual sidebar in place (a section with no page of its own yet) or navigates to that section's real page (Home, Projects) - see goToSection in app/pages/dashboard/page.tsx for the real logic this demo simplifies.
Side nav - anatomy
Main content for “Home”
A record page has no contextual sidebar
The deep dive of a collection (a project, an agreement, a nomination, a user, a role, a permission, a vocabulary, a report) uses the whole width beside the rail (contract section 3.7). Where you are is the breadcrumb, whose section crumb switches to another record of the collection; the record's actions are in its card. A list, and a create or edit form (whose sections are the contextual sidebar), keep it.
Items in the contextual sidebar
Every navigation item in the contextual sidebar carries an icon to the left of its label (contract section 3.11), so the column scans by shape as well as by word, and the same item has the same icon in the mobile menu.
- All and My. All comes first and takes the section's own icon from the rail (All projects the folder, All requests the licence file); My is that list narrowed to you and takes the person icon.
- Area and view tabs take the icon of what they hold: All users, All roles and All permissions in User Management; My BioData and the Flora and Fauna Dashboard on Home.
- Actions carry their action: Export CSV the download arrow, Create report the bar chart.
- Built with
Tab icon,ActionRowand, for the extra items in the mobile menu,MobileNavItem. A vertical tab list with an item that has no icon failsnpm run check:contracts.
Top nav
A 64px header: logo (37px, aspect ratio locked) + product name + breadcrumb on the left, primary actions + profile on the right. Every canonical page (/pages/dashboard, /pages/project-detail, /pages/observation-detail, ...) renders this same bar, just with its own section/current crumb. The right side is one Add menu (a project, a data licence request, a data sharing agreement - only what the persona can create), the global search, and the account control.
Top nav - anatomy
Shared shell components: a contract
The header and the icon rail are shared components, never rebuilt per screen, and they are the same for every persona (a guest differs only in its account controls). This is a non-negotiable design system contract, enforced by npm run check:contracts.
AppHeader(app/pages/_shared/app-header.tsx): logo lockup, wordmark, breadcrumb with the role-driven org pill (DEW for BioData roles, ORG for privileged roles, none otherwise), global search, the Add menu, and the profile menu or Log in / Sign up. A screen passes only its mobile-nav trigger and its breadcrumb.CreateMenuandlib/create-menu.ts: what the Add menu offers each persona, filtered by the role-access matrix. To let a persona create something new, add one entry there.PrimaryRail(primary-rail.tsx) andsectionIcons(nav-icons.ts): column 1, its one icon per section, and the Terms / Privacy / Help links as icons at its foot. Home's task badge is decided once, inside the rail.- Never hand-roll a
<header>, a primary rail, an icon map, or copy the profile menu into a screen. The check fails on any of them underapp/pages(labs inapp/proto, the marketing landing page and the auth flow are exempt).
The breadcrumb on a deep-dive page
On a record page (a project, a report, a data licence request, an agreement, a nomination, a user, role or permission) the section crumb is a switcher: the section's name with an up-down caret, opening one popup so a person can move to another record of the same collection without going back to the list, searching, and clicking in. The crumb after it is the record's name. On the list page itself the crumb is plain.
Breadcrumb switcher (Projects)
- The popup: the design system
ComboBoxas a search box at the top, with its list directly under it (A-Z, name and ID, about six rows then scrolling inside), the current record ticked, and a bar fixed at the foot, "View all <things>", which goes to the list. Search matches the name or the ID. Escape closes it and focus returns to the crumb. It lists only the records the signed-in role may see. - One piece, reused:
BreadcrumbSwitcher(app/pages/_shared/breadcrumb-switcher.tsx), through a thin wrapper per collection (ProjectSwitcher,ReportSwitcherand one for each of DLA, DSA, nominations and user management). A collection never builds its own. - One name: the section is called the same in the breadcrumb, the rail and column 2 ("Reports"). Who sees what is the content's rule, not part of the name.
- The rule is CONTRACTS §4.6, item 5.
Notes
- Not fully reusable - most of it is page-local chrome, duplicated per screen on purpose. The icon rail and contextual sidebar have no shared component (each real page keeps its own copy, since the IA and interaction details - which sections get a two-peer-tab split, which are inert placeholders - still differ per screen). The header's
Breadcrumbis the one genuinely shared piece (components/scaffold/breadcrumb.tsx) and is used directly, unmodified, in the demo above - not recreated. - Header height (64px) and the logo lockup's height (37px, aspect ratio locked via
w-auto) are the current, confirmed spec, real DEW-sourced measurements. - Option-2 (the top-nav shell alternative - header + primary nav bar with no icon rail) has been sunset and its code deleted - the sidebar shell is the one documented direction. Designers present numbered options to stakeholders while a screen is still being explored; once one is chosen the others are removed.
- Figma source: -HI-FI- Dashboard Explorations. This page documents the shell in code only - equivalent documentation on the Figma file itself (the project's source of truth) is still outstanding, flagged directly by the user as their own follow-up, not done here.