App Shell
AppShell implements the two-level navigation architecture (ED-11), structured to match the reference shell in helm-designsystem (src/app/Root.tsx). The full-height vertical left rail is Tower’s — brand block at its top, then the primary nav, the Modules section, the org-profile block, and the collapse toggle. The main column carries a 66px header (page title + global actions), the current module’s 40px horizontal tab strip, and the content zone, which hosts each page’s Page shell: flush Left/Right panels plus the padded middle column, the only region that scrolls independently. The shell lays this out from the page’s PageContract (epic #2316); pages no longer assemble it themselves. Every page inside a module is wrapped in AppShell.
Live demo
Usage
import { AppShell } from "@beacon/design-system";import type { NavItem } from "@beacon/design-system";import { useNavigate, useMatch } from "react-router-dom";
function Shell({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const onLien = useMatch("/lien/*");
const primaryNav: NavItem[] = [ { id: "home", label: "Home", active: !onLien, onClick: () => navigate("/") }, ];
const modulesNav: NavItem[] = [ { id: "lien", label: "Lien", active: !!onLien, onClick: () => navigate("/lien/dashboard") }, ];
const lienSecondaryNav: NavItem[] = [ { id: "dashboard", label: "Dashboard", active: true, onClick: () => navigate("/lien/dashboard") }, ];
return ( <AppShell primaryNav={primaryNav} modulesNav={modulesNav} secondaryNav={onLien ? lienSecondaryNav : undefined} secondaryNavLabel="Lien & Collections" pageTitle="Lien & Collections" brandTitle="Helm" onBrandClick={() => navigate("/")} > {children} </AppShell> );}Props
| Prop | Type | Default | Description |
|---|---|---|---|
primaryNav | NavItem[] | — | Tower-owned vertical nav items (left rail). Required. |
modulesNav | NavItem[] | undefined | The rail’s “Modules” section: one entry per enabled module. |
utilityNav | NavItem[] | undefined | Rail items pinned below a separator (Settings, Admin). |
secondaryNav | NavItem[] | undefined | Module-owned horizontal tabs (40px strip). Optional. |
secondaryNavLabel | string | undefined | Uppercase module label in the strip’s leading cell. |
brandMark | ReactNode | brand.mark icon role | Mark rendered on the 34px primary-color tile at the top of the rail. |
brandTitle | ReactNode | undefined | Product wordmark next to the tile (hidden when collapsed). |
brandSubtitle | ReactNode | undefined | Small uppercase line under the wordmark. |
onBrandClick | () => void | undefined | Click handler for the brand block (conventionally: Home). |
brand | ReactNode | undefined | Fully custom brand block (product skins); replaces mark/title. |
pageTitle | ReactNode | undefined | Page/module context at the left of the 66px header. |
topBarActions | ReactNode | undefined | Right-aligned header content (search, notifications, org). |
sidebarFooter | ReactNode | undefined | Org-profile block above the collapse toggle. |
footer | ReactNode | undefined | Full-width app footer. |
children | ReactNode | — | Page content rendered in the scrolling main area. |
NavItem shape
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable key. |
label | string | — | Display text. |
icon | ReactNode | undefined | Explicit icon node; wins over iconRole. |
iconRole | IconRole | undefined | Themeable icon role from the token store, rendered via RoleIcon at 16px. |
active | boolean | undefined | Marks the currently selected item. |
onClick | () => void | undefined | Called when the item is clicked. Use with your router’s navigate() — AppShell is router-agnostic. |
Design tokens used
AppShell.module.css reads exclusively from store tokens. No hardcoded colors.
| Token | Role |
|---|---|
--sidebar / --sidebar-border | Rail, header, and footer chrome surfaces (mirror card/border so the shell follows the theme). |
--helm-amber-dim / --helm-amber | Active nav item background / text, module tab underline. |
--helm-s2 / --helm-s3 | Module-nav strip background / hover background. |
--helm-text-dim / --muted-foreground | Inactive nav text / section labels and captions. |
--sidebar-primary | Brand tile background (mirrors primary). |
--top-bar-height / --secondary-nav-height / --sidebar-width | 66px header, 40px strip, 228px/52px rail. |
--page-padding-y / --page-padding-x | 28px/32px page padding, applied by the Page shell’s middle column (layout canon). |
--duration-fast / --duration-base / --easing-ease | 120ms color feedback, 200ms rail collapse on the standard curve. |
Architecture note
Tower builds primaryNav/modulesNav from the tenant’s enabled modules. Individual modules supply secondaryNav for their own views. Neither layer should know about the other’s items. This enforces the module boundary: a module cannot add itself to Tower’s primary nav, and Tower cannot dictate a module’s secondary nav.