Documentation/frontend/Layouts
In this section

Layouts

Layouts are page-shell components — they own the chrome (sidebar, header, footer) and slot in your route content (typically a react-router <Outlet />).

Everything lives under a single subpath, @omnitron-dev/prism/layouts. There are no /layouts/dashboard, /layouts/auth, or /layouts/core deep imports — the package exports map only declares ./layouts, so deep imports will not resolve.

import {
  // Application shell
  DashboardLayout,
  // Auth shells
  AuthCenteredLayout,
  AuthSplitLayout,
  AuthSimpleLayout,
  // Context / config
  LayoutProvider,
  useLayoutContext,
  useLayoutConfig,
  useNavActive,
  // Core primitives (build-your-own)
  LayoutSection,
  MainSection,
  HeaderSection,
  UserMenu,
} from '@omnitron-dev/prism/layouts';
ExportWhat it is
DashboardLayoutFull app shell — sidenav / topnav / combo + header + content
DashboardContentPadded content wrapper for use inside the dashboard <main>
PanelLayout / SidebarToggleNested navigation with independent collapse state and a shared edge control
Sidenav / MobileSidenavThe desktop / mobile sidebar (rendered by DashboardLayout)
AuthCenteredLayoutCard-centered auth shell (sign-in / sign-up / reset)
AuthSplitLayoutSplit-screen auth shell (illustration + form)
AuthSimpleLayoutMinimal auth/error/maintenance shell
LayoutProviderContext provider holding layout config + nav state
LayoutSection / MainSectionStructural primitives for custom shells
HeaderSection / HeaderToolbarSlot-based sticky header
UserMenuAvatar-trigger account dropdown for the header rightArea

There is no single <AuthLayout> component (only an AuthLayoutProps / AuthLayoutVariant type), and there is no <CoreLayout> — the minimal shell is composed from LayoutSection + HeaderSection, or you reach for AuthSimpleLayout.

Secondary navigation with <PanelLayout>

Use one application DashboardLayout and place PanelLayout inside it for corpus contents, document navigation, graph indexes or administration tools. Each panel can collapse independently. The main editor remains mounted when a panel collapses or its mobile drawer opens. Render the panel as a function to provide compact navigation while retaining the same component and its state. A plain React node remains mounted but hidden when collapsed. Mobile drawers retain their body between openings.

import { PanelLayout } from '@omnitron-dev/prism/layouts';

<PanelLayout
  title="Document contents"
  labels={{ expand: 'Expand contents', collapse: 'Collapse contents', close: 'Close contents' }}
  persistKey="research.document-contents"
  navigationKey={selectedDocumentId}
  panel={({ collapsed }) => <DocumentContents compact={collapsed} />}
>
  <DocumentEditor />
</PanelLayout>

PanelLayout does not require a router. navigationKey closes the mobile drawer after the host changes selection; route-based applications can pass useLocation().key. It does not reset the desktop preference. Use distinct persistKey values for the application sidebar and each kind of nested panel. The default desktop breakpoint is md, expanded width is 264 px, and sticky offset is 88 px. The compact rail is 64 px wide. Its height is identical in both modes: by default calc(100dvh - (offset + 24px)). Override height for embedded surfaces such as a graph, and collapsedWidth, layoutQuery, width, offset and sx as needed. DocSidebar supports compact, full tooltip/accessibility labels, active destination identity and optional item shortLabel values. Hide auxiliary search controls instead of unmounting them when retaining their input state matters. The dashboard and nested panels share SidebarToggle and the themed Scrollbar; scrollbar styling also applies to nested overflow regions through Prism's CSS baseline. On a fine pointer, thumbs reveal on hover or keyboard focus inside the region without changing layout. Touch input keeps the native controls visible; forced-colors mode keeps native colors and dimensions.

It uses native MUI controls and the shared Prism settings store, including its storage-failure fallback. The panel body may remount when crossing the mobile breakpoint; lift durable drafts and search state into the owning feature.

DashboardLayout.labels localizes opening/closing navigation, collapse/expand, main landmark and skip link. slotProps.main.id and className integrate with the application's focus and fragment navigation. Its mobile drawer closes on actual router navigation; collapsed destinations retain accessible names and aria-current values. A public website and a workspace should render separate shells, rather than placing the full application inside a bounded marketing container.

<DashboardLayout>

Admin / operator console shell. It wraps its children in a LayoutProvider and renders a header plus a sidenav, a topnav, or both — driven by initialConfig.navigationMenuType ('sidenav' by default).

import { DashboardLayout, UserMenu } from '@omnitron-dev/prism/layouts';
import type { LayoutNavData } from '@omnitron-dev/prism/layouts';
import { Outlet } from 'react-router-dom';

const navData: LayoutNavData = [
  {
    id: 'main',
    subheader: 'Overview',
    items: [
      { id: 'dashboard', title: 'Dashboard', path: '/', icon: <DashboardIcon /> },
      { id: 'apps', title: 'Apps', path: '/apps', icon: <AppsIcon /> },
    ],
  },
  {
    id: 'infra',
    subheader: 'Infrastructure',
    items: [
      {
        id: 'infrastructure',
        title: 'Infrastructure',
        icon: <GridIcon />,
        children: [
          { id: 'containers', title: 'Containers', path: '/containers' },
          { id: 'nodes', title: 'Nodes', path: '/nodes' },
        ],
      },
      {
        id: 'settings',
        title: 'Settings',
        path: '/settings',
        icon: <CogIcon />,
        allowedRoles: ['admin'],
      },
    ],
  },
];

function ConsoleShell() {
  return (
    <DashboardLayout
      logo={<Logo size={32} />}
      navData={navData}
      headerSlots={{
        centerArea: <CommandPaletteTrigger />,
        rightArea: (
          <>
            <NotificationsBell />
            <UserMenu user={{ name: 'Ada Lovelace', email: 'ada@example.com' }} />
          </>
        ),
      }}
      initialConfig={{ navigationMenuType: 'sidenav', sidenavVariant: 'default' }}
      persistKey="console"
    >
      <Outlet />
    </DashboardLayout>
  );
}

Bounded workspaces

For terminals, editors and infrastructure consoles, set contentMode="workspace". The shell uses 100dvh, keeps its header and footer in the available height, and gives the main region minHeight: 0 and hidden overflow. Children should use flex sizing with minHeight: 0 and minWidth: 0; put scrolling on the panel that owns it. To scroll ordinary pages inside the same shell, use slotProps={{ main: { sx: { overflowY: 'auto' } } }}. Default document mode retains normal page scrolling for other applications.

Props

PropTypeDefaultNotes
childrenReactNode—Page content (your <Outlet />)
contentMode'document' | 'workspace''document'Document flow, or a viewport-bounded shell whose panels own scrolling
navDataLayoutNavData—Navigation tree — LayoutNavSection[] (see below)
headerSlotsHeaderSlots—Header content per slot (leftArea / centerArea / rightArea / topArea / bottomArea)
logoReactNode—Logo shown in the sidenav (and as the mobile header left slot)
footerReactNode—Rendered at the bottom of the main column
sidenavFooterReactNode—Footer content pinned inside the sidenav
layoutQueryBreakpoint'lg'Breakpoint at/above which the desktop sidenav shows and the mobile drawer/menu-button hides
initialConfigPartial<LayoutConfig>—Initial layout config (nav type, sidenav variant, nav color, …)
persistKeystring—When set, sidenav collapsed state persists to localStorage under this key. Use a unique key per layout (e.g. 'main', 'admin')
slotProps{ header?, main? }—header forwards to HeaderSection; main accepts { sx, disablePadding, maxWidth }

There is no brand / navItems / topRight / topLeft / sidebarWidth / defaultCollapsed / persistCollapse / showBreadcrumbs / userRoles prop. The header is composed via headerSlots; the logo goes through logo; nav uses navData; and nav-item visibility is expressed per-item with allowedRoles / allowedPermissions (see LayoutNavItem).

LayoutNavData

Navigation is a list of sections, each with an optional subheader and a list of items:

type LayoutNavData = LayoutNavSection[];

interface LayoutNavSection {
  id: string;
  subheader?: string;       // group heading
  items: LayoutNavItem[];
}

LayoutNavItem

interface LayoutNavItem {
  id: string;                       // unique item ID (required)
  title: string;                    // display label
  path?: string;                    // route (leaf items)
  icon?: ReactNode;                 // icon element
  info?: ReactNode;                 // trailing info/label
  caption?: string;                 // secondary description text
  disabled?: boolean;
  external?: boolean;               // external link
  children?: LayoutNavItem[];       // nested menu — expands inline
  allowedRoles?: string[];          // visibility filter (roles)
  allowedPermissions?: string[];    // visibility filter (permissions)
  deepMatch?: boolean;              // active on child routes (default: true)
  badge?: string | number;          // count / "new" indicator
  selectionPrefix?: string | string[]; // keep active across disjoint URL spaces
}

Active-state rules (implemented in isNavItemActive):

  • Exact match — path equals the current pathname (query/hash and trailing slashes are stripped first).

  • selectionPrefix — active when the path starts with any listed prefix. Accepts a string or an array (any prefix wins) so one entry can span two URL spaces, e.g. selectionPrefix: ['/communities', '/c/'].

  • deepMatch (default true) — active when path is a segment-aligned prefix of the pathname (/admin/orgs stays highlighted on /admin/orgs/list, but /product does not match /products). Set deepMatch: false to require an exact match.

Items with children render as inline-expandable groups; a parent stays highlighted when one of its descendants is active.

Behaviour

  • Navigation type — initialConfig.navigationMenuType selects 'sidenav' (default), 'topnav', or 'combo' (both). Sidenav and combo render Sidenav + MobileSidenav; topnav and combo render the Topnav bar.

  • Sidenav variants — initialConfig.sidenavVariant is 'default', 'mini', or 'stacked'; widths come from DRAWER_WIDTHS (full: 280, mini: 73, stackedExpanded: 300, stackedCollapsed: 72).

  • Responsive — at/above layoutQuery the desktop Sidenav shows and the main content is offset by the drawer width; below it, the sidebar becomes a MobileSidenav drawer toggled by the injected header menu button.

  • Collapse persistence — toggling collapse persists to the settings store under persistKey (and is restored on mount). Without persistKey, collapse is in-memory only.

  • Accessibility — renders a SkipLink to #main-content, a role="main" landmark, and ARIA labels for the header / nav.

DashboardContent

Optional padded wrapper for content rendered inside the dashboard <main> (handy when you don't pass slotProps.main):

import { DashboardContent } from '@omnitron-dev/prism/layouts';

<DashboardContent maxWidth="xl">
  <PageHeader title="Apps" />
  <AppsTable />
</DashboardContent>
PropTypeDefault
childrenReactNode—
disablePaddingbooleanfalse
maxWidthBreakpoint | false—
sxSxProps<Theme>—

Layout density

Density is a setting on Prism's settings store, not a layout prop — read/write it with useSettingsStore:

import { useSettingsStore } from '@omnitron-dev/prism';

function DensityToggle() {
  const density = useSettingsStore((s) => s.density);
  const setDensity = useSettingsStore((s) => s.setDensity);
  return (
    <select value={density} onChange={(e) => setDensity(e.target.value as any)}>
      <option value="compact">Compact</option>
      <option value="standard">Standard</option>
      <option value="comfortable">Comfortable</option>
    </select>
  );
}

ComponentDensity is 'compact' | 'standard' | 'comfortable'. Tables, lists, and forms respond to it via theme spacing tokens.

Auth layouts

There are three auth shells. They share a help-link header pattern but differ in how the form is framed. None of them require a LayoutProvider.

<AuthCenteredLayout>

Card-centered form on a subtle gradient — the default for sign-in / sign-up / reset / verify.

import { AuthCenteredLayout } from '@omnitron-dev/prism/layouts';

function SignInPage() {
  return (
    <AuthCenteredLayout logo={<Logo />}>
      <SignInForm />
    </AuthCenteredLayout>
  );
}
PropTypeDefault
childrenReactNode—
logoReactNode—
maxWidthnumber | string420
sxSxProps<Theme>—

Companion: AuthCenteredContent — a standalone card wrapper (children, maxWidth = 420, sx) for composing your own centered shell.

<AuthSplitLayout>

Split-screen: branded illustration pane on the left (hidden below md), form on the right. Includes a built-in help link in the header.

import { AuthSplitLayout } from '@omnitron-dev/prism/layouts';

function SignInPage() {
  return (
    <AuthSplitLayout
      logo={<Logo />}
      title="Welcome to Platform"
      subtitle="Sign in to continue to your dashboard"
      illustration={<img src="/auth-illustration.svg" alt="" />}
    >
      <SignInForm />
    </AuthSplitLayout>
  );
}
PropTypeDefault
childrenReactNode—
logoReactNode—
titlestring'Welcome'
subtitlestring—
illustrationReactNode—
showHelpbooleantrue
helpUrlstring'/help'
helpTextstring'Need help?'
settingsButtonReactNode—
maxWidthnumber | string480
illustrationSxSxProps<Theme>—
sxSxProps<Theme>—

The illustration pane is hidden below the md breakpoint; the form then takes the full width. Companions AuthSplitIllustration (title / subtitle / illustration / sx) and AuthSplitContent (children / maxWidth / sx) let you assemble a custom split.

<AuthSimpleLayout>

Minimal shell with just a help-link header — for error pages (404 / 500), maintenance pages, or compact auth forms.

import { AuthSimpleLayout } from '@omnitron-dev/prism/layouts';

function NotFoundPage() {
  return (
    <AuthSimpleLayout logo={<Logo />} compact>
      <NotFoundContent />
    </AuthSimpleLayout>
  );
}
PropTypeDefault
childrenReactNode—
logoReactNode—
showHelpbooleantrue
helpUrlstring'/help'
helpTextstring'Need help?'
settingsButtonReactNode—
compactbooleanfalse
maxWidthBreakpoint | false'lg'
sxSxProps<Theme>—

With compact, content is centered in a 448px column; otherwise it fills a <Container maxWidth={maxWidth}>. Companion: SimpleCompactContent (children / sx) for the compact wrapper alone.

<LayoutProvider>

DashboardLayout mounts a LayoutProvider for you. Use it directly only when you build a custom dashboard-style shell from the primitives and need access to layout config + nav state.

import { LayoutProvider, useLayoutContext } from '@omnitron-dev/prism/layouts';

<LayoutProvider initialConfig={{ navigationMenuType: 'combo' }} persistKey="admin">
  <CustomShell />
</LayoutProvider>
PropTypeDefault
childrenReactNode—
initialConfigPartial<LayoutConfig>—
persistKeystring—

LayoutConfig holds: navigationMenuType ('sidenav' \| 'topnav' \| 'combo'), sidenavVariant ('default' \| 'mini' \| 'stacked'), sidenavCollapsed, topnavVariant, navColor ('default' \| 'vibrant' \| 'integrate'), drawerOpen, drawerWidth, and compactLayout.

Hooks

HookReturns
useLayoutContext()Full context: config, isMobile, setConfig, toggleDrawer, toggleSidenavCollapse, setNavigationMenuType, setSidenavVariant, setNavColor, openItems / toggleItem / isItemOpen, isNavItemActive, isNestedItemActive. Throws outside a LayoutProvider.
useLayoutConfig()Just the current LayoutConfig.
useSidenavVisible()true for 'sidenav' / 'combo'.
useTopnavVisible()true for 'topnav' / 'combo'.
useNavActive(pathname){ isItemActive, isNestedActive } for a given pathname (pass useLocation().pathname from your router).
import { useLayoutContext } from '@omnitron-dev/prism/layouts';

function CollapseToggle() {
  const { config, toggleSidenavCollapse } = useLayoutContext();
  return (
    <IconButton onClick={toggleSidenavCollapse}>
      {config.sidenavCollapsed ? <MenuOpenIcon /> : <MenuIcon />}
    </IconButton>
  );
}

Core primitives (custom shells)

When none of the pre-built shells fit, compose your own from the core primitives. There is no <CoreLayout> — LayoutSection is the root container, HeaderSection is the slotted header, and MainSection is a padded <main>.

import {
  LayoutSection,
  MainSection,
  HeaderSection,
  Sidenav,
} from '@omnitron-dev/prism/layouts';

function CustomShell() {
  return (
    <LayoutSection
      headerSection={
        <HeaderSection
          slots={{ leftArea: <Logo />, rightArea: <UserMenu user={user} /> }}
        />
      }
      sidebarSection={<Sidenav navData={navData} />}
      footerSection={<Footer />}
    >
      <MainSection>
        <Outlet />
      </MainSection>
    </LayoutSection>
  );
}

LayoutSection

PropTypeNotes
headerSectionReactNodeRendered at the top
sidebarSectionReactNodeRenders a flex sidebar+main row when present
footerSectionReactNodeRendered at the bottom of the main column
childrenReactNodeMain content
cssVarsRecord<string, string | number>Override layout CSS variables
sxSxProps<Theme>—

MainSection takes just children + sx (padded <main> flex column).

HeaderSection

A sticky, slot-based AppBar with a scroll blur/elevation effect. Used internally by the dashboard and split/simple auth layouts; usable standalone.

PropTypeDefaultNotes
slotsHeaderSlots—topArea / leftArea / centerArea / rightArea / bottomArea
slotPropsper-slot SxProps + container—Style each slot
disableStickybooleanfalseUse position="static"
disableOffsetbooleanfalseDisable the scroll-blur background layer
disableElevationbooleanfalseDisable the scroll shadow
containerizedbooleanfalseWrap the toolbar in a max-width Container
maxWidthBreakpoint | false'lg'Container width when containerized
sxSxProps<Theme>——

Companion HeaderToolbar (children / sx) is the bare toolbar without the AppBar wrapper.

UserMenu

Avatar-trigger account dropdown, designed for the header rightArea.

import { UserMenu } from '@omnitron-dev/prism/layouts';

<UserMenu
  user={{ name: 'Ada Lovelace', email: 'ada@example.com', role: 'Admin' }}
  menuItems={[
    { key: 'profile', label: 'Profile', onClick: goToProfile },
    { key: 'logout', label: 'Sign out', destructive: true, onClick: signOut },
  ]}
  showEmail
/>
PropTypeDefault
userUserMenuUser ({ name; email?; avatarUrl?; role? })— (required)
menuItemsUserMenuItem[]—
header / footerReactNode—
avatarSize'small' | 'medium' | 'large''medium'
showEmailboolean—
showRoleboolean—
sx / popoverSxSxProps<Theme>—
onOpen / onClose() => void—

Each UserMenuItem is { key, label, icon?, onClick?, destructive?, disabled?, dividerAfter? }.

Mixing layouts in one app

Layouts are just route-element wrappers — react-router 7 handles the rest:

<Routes>
  {/* Auth flow */}
  <Route element={<GuestGuard><AuthCenteredLayout logo={<Logo />}><Outlet /></AuthCenteredLayout></GuestGuard>}>
    <Route path="/sign-in" element={<SignInForm />} />
    <Route path="/sign-up" element={<SignUpForm />} />
  </Route>

  {/* Authenticated app */}
  <Route element={<AuthGuard><ConsoleShell /></AuthGuard>}>
    <Route path="/dashboard" element={<DashboardPage />} />
    <Route path="/apps" element={<AppsPage />} />
  </Route>

  {/* Error pages */}
  <Route path="*" element={<AuthSimpleLayout logo={<Logo />} compact><NotFound /></AuthSimpleLayout>} />
</Routes>

(ConsoleShell is the DashboardLayout wrapper shown at the top of this page; it renders <Outlet /> for the matched child routes.)

See also