Patterns · Shell presets
WorkspaceAppBar
The Workspace shell's app bar, ready to go — the bar your daily-driver app wears (the client portal and its siblings). The chrome that never changes ( LanguageSelector, NavHistory ) is baked in, brand defaults to the compact mark (a daily-use app doesn't need to re-introduce itself), and the parts that depend on your app ( middle, account ) come in as composition. The Workspace twin of <ReferenceAppBar> — one doctrine, two shells.
BetaWorkspaceAppBar is Beta — the composition is settled (BrandSlot · NavHistory · WorkspaceMiddle · LanguageSelector · AccountSlot); the prop surface may still move.
This pattern pairs with WorkspaceMiddle: one wraps the Workspace shell's middle — the ⌘K search door with its optional session-context buttons — the other wraps the whole app bar. Want full control? Drop down to the <AppBar> primitive and put the canonical pieces together yourself.
Live demo
Running in bounded mode so the bar stays inside the demo box. The middle is the real WorkspaceMiddle wired to this site's search index: the org button shows an active session override (the gold active wash), and the search door opens the CommandPalette right over the page. The side buttons pop a toast explaining what your app would do there, and the avatar is a real signed-in AccountSlot — its menu rows are yours to fill, so here they toast too.
API shape — the three-category rule
Every slot in the Workspace app bar falls into one of three buckets — the same rule ReferenceAppBar follows — and the preset's API comes straight from that.
- Invariant content
Baked. Not a prop. LanguageSelector is invariant chrome on every shell — the preset just mounts it; its one per-deployment config (
cookieDomain) is forwarded. BrandSlot leads the Brand slot, and NavHistory (back / forward / recents — the client portal’s left cluster) sits beside it, gated on onNavigate.- Enumerated content
Variant prop. Brand resolves between
'mark'and'lockup'; mode resolves between'edge'and'bounded'. Finite, known sets — you just pick from the list.- Contextual content
Composition. Middle takes the search index plus the optional session-context triggers; account takes the authenticated user. Both vary by consumer and reactively respond to runtime state — they’re
ReactNodeprops, not parametric options.
WorkspaceAppBarProps
- brand
'mark' | 'lockup'Default
'mark'. Daily-use shells get the dial, not the lockup — reach for lockup only where the surface must name itself.- brandHref
stringDefault
'/'. Brand anchor target.- brandLabel
stringDefault
'Halo — Home'. Accessible label on the brand anchor.- mode
'edge' | 'bounded'Default
'edge'. Forwarded to<AppBar>.- onNavigate
(pathname: string) => voidRouter navigate for the baked
NavHistory(back / forward / recents in the Brand slot). Pass it to wire history; omit it for a Workspace shell without. The app also records visits via recordNav.- navHistoryIcons
Record<string, ReactNode>Optional icon-key → glyph map forwarded to NavHistory, for recents rows whose recorded visit carried an icon key.
- middle
ReactNodeRequired. Typically
<WorkspaceMiddle items={…} orgContext={…} impersonation={…} />. There is no canonical “no middle” Workspace shell — the search door is the workspace’s spine.- account
ReactNodeRequired. Typically
<AccountSlot state="signed-in" …/>— a Workspace shell is an authenticated surface.- cookieDomain
stringForwarded to the baked LanguageSelector. Pass
'.halocompliance.com'in production for cross-subdomain.- onSlotError
(error: Error, info: ErrorInfo) => voidFires when any of the AppBar’s per-slot boundaries catch (Brand / Middle / End). Same shape as the AppBar’s own prop.
- onChromeError
(error: Error, info: ErrorInfo) => voidFires when one of the baked chrome patterns (BrandSlot, LanguageSelector) catches inside its own ErrorBoundary. Chrome degrades silently to the user; this is the only developer signal.
WorkspaceMiddleProps
The middle pattern’s own knobs — the ⌘K search door plus the session-context cluster: [ org context ] [ Go to… ⌘K ] [ impersonation ]. If the pattern’s boundary catches, the fallback preserves the middle silhouette: an inert search trigger; the context triggers drop (their functions remain reachable through consumer surfaces).
- items
PaletteItem[]The searchable index (pages, records, actions).
- placeholder
stringDefault
'Go to…'— ships translated with the pattern’s chrome strings. Used by both the trigger and the palette input.- shortcutHint
stringDefault
'⌘K'. Visual + a11y shortcut hint.- shortcutMatcher
((event: KeyboardEvent) => boolean) | nullOverride the keyboard shortcut. Defaults to ⌘/Ctrl+K. Pass null to disable the global binding (the consumer owns it).
- onNavigate
(href: string) => voidSPA navigation hook; the default uses window.location.assign.
- orgContext
WorkspaceContextTriggerOrg-context switcher trigger (cross-org operators). Renders only when passed — a single-org user sees just the search field.
- impersonation
WorkspaceContextTriggerImpersonation trigger (support roles). Renders only when passed.
- onError
(error: Error, info: ErrorInfo) => voidTelemetry reporter for the pattern’s internal boundary.
WorkspaceContextTrigger
The flanking triggers share one shape. Each is optional, and on narrow viewports both drop (CSS) — their functions remain reachable through the palette, mirroring the client’s mobile behavior.
- label
stringAccessible name AND title. Consumer-translated — it carries session specifics (“Viewing as Dana…”, “Acting in Acme Corp”).
- active
booleanWhether a session override is active. An active trigger carries the gold active wash (gold = active state, per the gold discipline) plus
aria-pressed.- onOpen
() => voidOpen the consumer’s switcher UI (palette mode, modal — its call).
Usage
import {
WorkspaceAppBar,
WorkspaceMiddle,
AccountSlot,
} from '@halo-compliance/ui'
<WorkspaceAppBar
onNavigate={navigate}
middle={<WorkspaceMiddle items={SEARCH_INDEX} onNavigate={navigate} />}
account={
<AccountSlot state="signed-in" userName={user.name} menu={accountMenu} />
}
/><WorkspaceAppBar
brand="mark"
brandHref="/"
brandLabel="Halo — Home"
mode="edge"
onNavigate={navigate}
cookieDomain=".halocompliance.com"
middle={
<WorkspaceMiddle
items={SEARCH_INDEX}
onNavigate={navigate}
orgContext={{
label: orgContextLabel, // "Acting in Acme Corp — switch organization"
active: orgOverridden, // gold active wash + aria-pressed
onOpen: openOrgSwitcher,
}}
impersonation={{
label: impersonationLabel, // "Viewing as Dana — stop impersonating"
active: impersonating,
onOpen: openImpersonationPicker,
}}
onError={reportChromeError}
/>
}
account={
<AccountSlot
state="signed-in"
userName={user.name}
avatarSrc={user.avatarUrl}
menu={accountMenu}
onError={reportChromeError}
/>
}
onSlotError={reportChromeError}
onChromeError={reportChromeError}
/>Deliberately absent — theme and help
The live client portal still has a theme toggle and a help button in its bar. This preset skips both on purpose: the universal surface gives each a better home (ADR-0027) — appearance settings live in the Preferences corner (bottom-left), and the human door lives in the Comms corner (bottom-right). The portal's bar-mounted versions just predate that decision; adopting the preset is how an app catches up. What's left is exactly what earns a spot in the bar: brand, history, the search door, session context, language, account.
When to reach for it
- Use the preset when you're building a Workspace shell — a daily-use app like the client portal. Match it and the bar stays consistent across every surface.
- Drop down to
<AppBar>when you need full control: a different middle (a Workspace shell variant), a custom End-slot composition (a third invariant element), or a custom mode treatment.
Anti-patterns
- Don’t re-mount theme or help in the bar. They’re rehomed corners (ADR-0027), not bar chrome. A bar-mounted theme toggle is the legacy portal shape, not the target — see the section above.
- Don’t turn composition slots into prop bags. The temptation is to flatten
middle=<WorkspaceMiddle items={…} />intomiddleItems={…}. Don’t — the middle has half a dozen optional knobs (orgContext, impersonation, shortcutMatcher, placeholder, …) that would all have to flatten too. Composition keeps the API stable as the inner pattern grows. - Don’t ship a Workspace shell without the middle. The search door is the workspace’s spine — middle is required. A single-org, non-support user doesn’t get an emptier bar; they simply see just the search field.
Related
- /app-bar — the AppBar Component this preset composes. Drop down to it for hand-composed shells.
- /reference-app-bar — the Reference sibling preset; same three-category doctrine, different shell.
- /nav-history — the baked back / forward / recents cluster in the Brand slot.
- /preferences — where the theme toggle rehomed (ADR-0027).
- /comms — where help rehomed (ADR-0027).