Skip to content

@plexusone/shell

Application shell components for consistent layouts.

Installation

pnpm add @plexusone/shell react-router-dom

Features

  • Responsive application layout
  • Configurable sidebar navigation
  • Top navbar with user menu
  • Organization switcher
  • Breadcrumbs
  • Mobile-friendly

Usage

AppShell

The main layout component:

import { AppShell } from '@plexusone/shell';

function App() {
  return (
    <AppShell
      brandName="My App"
      brandLogo="/logo.svg"
      navigation={[
        { label: 'Dashboard', href: '/', icon: HomeIcon },
        { label: 'Projects', href: '/projects', icon: FolderIcon },
        { label: 'Settings', href: '/settings', icon: SettingsIcon },
      ]}
    >
      <YourContent />
    </AppShell>
  );
}

Nested Navigation

const navigation = [
  { label: 'Dashboard', href: '/' },
  {
    label: 'Projects',
    href: '/projects',
    children: [
      { label: 'All Projects', href: '/projects' },
      { label: 'Archived', href: '/projects/archived' },
    ],
  },
  { label: 'Settings', href: '/settings' },
];

Custom User Menu

import { AppShell, UserMenu } from '@plexusone/shell';

function App() {
  return (
    <AppShell
      userMenu={
        <UserMenu
          items={[
            { label: 'Profile', href: '/profile' },
            { label: 'Settings', href: '/settings' },
            { type: 'divider' },
            { label: 'Logout', onClick: handleLogout },
          ]}
        />
      }
    >
      <YourContent />
    </AppShell>
  );
}
import { Breadcrumbs } from '@plexusone/shell';

function ProjectPage() {
  return (
    <div>
      <Breadcrumbs items={[{ label: 'Projects', href: '/projects' }, { label: 'My Project' }]} />
      <h1>My Project</h1>
    </div>
  );
}

Product and organization switchers

ProductSwitcher lists the deployment's applications the person holds a role in for the current organization, from the session's app catalog, in the catalog's order. The current application is highlighted in its place, so the switcher looks the same in every application of a suite; an application the catalog does not list comes first. OrganizationSwitcher is controlled: it reports the chosen organization's ID and the application navigates. Both read the session from SessionProvider in @plexusone/auth. See Composing applications.

<ProductSwitcher self={{ id: 'reports', name: 'Reports', url: '/' }} organizationId={org} />
<OrganizationSwitcher value={org} onChange={(id) => navigate(`/orgs/${id}`)} />

OrgSwitcher remains for applications that keep the current organization in TenantProvider.

Carrying the sidebar and theme between applications

Each application of a suite runs on its own host, so its stored sidebar state and theme are its own. A product switcher link to another application carries both as a URL fragment (#sf-sidebar=collapsed&sf-theme=dark), set when the link is followed so it reflects the sidebar as it is then. The application it opens adopts them before its first render, stores them, and removes the fragment from the address bar. A fragment never reaches a server. An application that navigates with onNavigate stays in one build and shares its storage, so nothing is carried.

An application that builds its own links to another application, such as a settings entry, wraps the URL with carryPreferences:

import { carryPreferences } from '@plexusone/shell';

window.location.assign(carryPreferences('https://agents.example.com/settings'));

carryPreferences(url, prefs?) leaves a URL that already has a fragment alone, and reads any preference not passed from storage. adoptCarriedPreferences() is called for you by ShellProvider, so by AppShell, and by applyStoredTheme(); call it yourself only in a shell that does neither.

Profile menu

Under SessionProvider, the bar shows ProfileMenu: who is signed in, the current organization, the role held in this application (falling back to the organization role), the application's profileItems, and sign out. It has no default links. Applications on AuthProvider keep UserMenu, whose default links were removed for the same reason.

<AppShell
  appName="Reports"
  app={{ id: 'reports', name: 'Reports', url: '/' }}
  organizationId={org}
  onOrganizationChange={(id) => navigate(`/orgs/${id}`)}
  profileItems={[{ id: 'keys', label: 'API keys', href: `/orgs/${org}/keys` }]}
>
  {children}
</AppShell>

Theme

showThemeToggle adds a toggle that cycles light, dark and the system's preference. The choice is stored in local storage and applied as the dark class on the root element, which the design system's styles key on; while on system it follows the operating system's preference as it changes. If storage is unavailable the choice lasts for the page.

Call applyStoredTheme() before the first render so the page does not flash the other theme:

import { applyStoredTheme } from '@plexusone/shell';

applyStoredTheme();
createRoot(document.getElementById('root')!).render(<App />);

useTheme() returns the choice and a setter for an application that renders its own control.

API Reference

AppShell Props

Prop Type Description
appName string Application name, used for the default sidebar logo
logo ReactNode Sidebar logo
logoCollapsed ReactNode Sidebar logo when collapsed
navigation NavSection[] Sidebar navigation
breadcrumbs BreadcrumbItem[] Breadcrumbs for the current page
brand ReactNode Slot at the start of the bar: logo and wordmark
app CatalogApp This application; shows the product switcher from the session's catalog
organizationId string The current organization's ID
onOrganizationChange (id: string) => void Shows the controlled OrganizationSwitcher and reports the chosen ID
onNavigateApp (url: string) => void Client-side navigation between applications in one build
command ReactNode Slot after the switchers: search or a command bar
actions ReactNode Slot before the profile menu: the application's own actions
profileItems UserAction[] The application's entries in the profile menu, above sign-out
showThemeToggle boolean Show the light, dark and system theme toggle. Default false
userActions UserAction[] Profile menu entries
showOrgSwitcher boolean Show the TenantProvider-based OrgSwitcher when onOrganizationChange is not set
sidebarCollapsible boolean Whether the sidebar can collapse
defaultCollapsed boolean Initial collapsed state
onNavigate (href: string) => void Navigation within the application, for SPA routing
onCollapseChange (collapsed: boolean) => void Called when the sidebar collapses or expands

The bar, from start to end: brand, product switcher, organization switcher, command, breadcrumbs, actions, profile menu.

interface NavItem {
  label: string;
  href: string;
  icon?: ComponentType;
  children?: NavItem[];
  badge?: string | number;
}
Prop Type Description
items NavItem[] Navigation items
collapsed boolean Collapsed state
onCollapse (collapsed: boolean) => void Collapse callback
Prop Type Description
brandName string Application name
brandLogo string Logo URL
userMenu ReactNode User menu component
orgSwitcher ReactNode Organization switcher