App-shell pattern
The chrome every app in the Nexus family (Materials Exchange, Assurance, Procurement…) adopts. It matches the production nx app shell and ships from nx-ui as a set of composable blocks.
The scope model
Every screen in a Nexus app renders inside a scope:
scope.ts
type Scope = { org: OrgRef; project: ProjectRef | null }- Organisation — the auth boundary. Switching org changes what the user may see and do; every org-scoped query re-resolves. Always present.
- Construction project — an optional narrowing within the org. When set, the nav, breadcrumb tail and page data are project-scoped.
OrgRef / ProjectRef (in components/blocks/scope.tsx) are display snapshots — id, name, and the couple of fields the chrome renders. They are not domain records; consumers map their own models down to them.
The blocks hold no state, persistence or auth
Scope arrives via props and changes leave via three callbacks —
onOrgChange(treat as an auth-scope change: persist, clear the old org's project, re-resolve), onProjectChange (land on the project's overview — never carry the previous project's sub-path across), and onProjectClear (return to the projects picker).Composition
app-shell composition
AppShell ─ topBar ── left: ScopeBreadcrumbs (org ▸ project ▸ …trail)
│ └──── right: actions (help icon-button default)
├── sidebar: AppSidebar
│ ├── header: AppSwitcher (Nexus app family launcher)
│ ├── content: ScopedNav (two-level sliding nav)
│ └── footer: UserMenu
└── page card (rounded, ringed, the scroll container)- AppShell (
app-shell) — the chrome frame. The optionaltopBararrangement renders the nx header strip on the gutter, above the page card (breadcrumbs left, actions right, a help button by default); it never scrolls with the card. - ScopeBreadcrumbs (
scope-breadcrumbs) — the top bar's left slot. The first crumb is the org switcher, the second (only when scoped) is the project switcher with an "All projects" action that clears the project; ordinary trail crumbs follow. - ScopedNav (
scoped-nav) — the sidebar nav with two levels. The workspace level (Projects picker first, then cross-project zoom-outs like Dashboard) slides left when a project is selected; the project level enters from the right, headed by a "Back to all projects" row. ~200 ms translate+fade, instant underprefers-reduced-motion. - AppSwitcher (
app-switcher) — the sidebar-header launcher: the active app's tile as the trigger, a grid popover of the Nexus app family, "coming soon" tiles disabled. - scope — the shared contract module; every scoped block depends on it and nothing else in the family.
Wiring a consumer app
Routing and auth stay with the consumer. The reference wiring (as in nx):
- URL is the source of truth for the project.
/projects/[slug]/…is project scope;onProjectChangenavigates to/projects/{id}/overviewandonProjectClearto/projects. Deep links and back/forward then work for free. - Org is account state, not URL.
onOrgChangepersists the selection and re-renders the shell with the new org's projects; org-scoped queries re-run reactively. - Active nav items are computed by the consumer (prefix-match the pathname) and passed via
isActive— blocks never read the router.
Scope carries across the app family
The Nexus apps live on sibling subdomains and share this shell. The AppSwitcher links to each app's entry URL, and scope is expected to carry across — but the carry mechanism (a scope cookie on the shared parent domain, or claims in the shared session JWT) is platform infrastructure and deliberately out of scope for nx-ui. Don't add persistence to the blocks to "help".
The full working reference is the live Scoped app example — state-based for self-containment; swap the state transitions for router navigations in a real app. The canonical design note is
docs/scoped-shell.md.