Adminlayout
ImplementedAdmin application frame with a skip link, sidebar and header slots, and a focusable, scrollable main content region.
components/organisms/AdminLayout/AdminLayout.tsxAdmin application frame — a skip link, sidebar and header slots, and a focusable, scrollable main content region; compose the Sidebar molecule in the sidebar slot. The skip-link and main-landmark labels default English-literal and are overridable via skipLabel / mainAriaLabel (zero owned i18n keys).
AdminLayout
Organism — Admin family
The outermost layout organism for admin surfaces: sidebar + header slots (compose the Sidebar molecule / AdminHeader organism), a full-height flex row, a skip link to the main region, and a focusable (tabIndex -1) scrolling <main> landmark. The header column is min-w-0 so long tables truncate instead of blowing out the layout. AdminSidebar is desktop-only legacy kept for existing frames — not for new work (prefer the Sidebar molecule, whose portal drawer + scrim + focus trap the storefront dashboard adopted in DS-CORR-08-05; formal deprecation decision pending).
skipLabel: “Skip to main content” (default) — mainAriaLabel: “Main content” (default). Tab into the frame to surface the skip link.
Sidebar slot
Overview
Orders
Events
Team
AdminHeader slot
⌘K — command palette trigger lives here
Main content region
Page content lands in a focusable <main> landmark with its own horizontal scroll — broad admin tables scroll inside the frame instead of the page. Focus it programmatically on route change.
This component supports light and dark mode.
Usage
AdminLayout is the frame, not the page: fill the sidebar slot with the Sidebar molecule (pass collapsible so the frame works on mobile) and the header slot with AdminHeader, then render the route inside children. The frame owns zero i18n keys — override skipLabel / mainAriaLabel with translated strings when the app language isn't English, and pass AdminHeader its own required locale for its command-palette label.
import { AdminLayout, AdminHeader, Sidebar, type SidebarGroup } from '@epasslive/design-system'
const navGroups: SidebarGroup[] = [
{ id: 'manage', label: 'Manage', items: [
{ id: 'overview', label: 'Overview', href: '/admin' },
{ id: 'orders', label: 'Orders', href: '/admin/orders' },
{ id: 'events', label: 'Events', href: '/admin/events' },
] },
{ id: 'account', label: 'Account', items: [
{ id: 'team', label: 'Team', href: '/admin/team' },
{ id: 'settings', label: 'Settings', href: '/admin/settings' },
] },
]
<AdminLayout
skipLabel={locale === 'fr' ? 'Aller au contenu principal' : undefined}
sidebar={<Sidebar logo={<Logo />} groups={navGroups} activeKey="orders" collapsible />}
header={<AdminHeader locale={locale} brand={<Logo />} onSearch={openPalette} />}
>
{/* page content — renders inside <main id="admin-layout-main"> */}
<OrdersPanel />
</AdminLayout>Composition notes
The slots are plain ReactNode — the frame imposes no markup on them, so sidebars and headers stay independently swappable. The frame owns zero i18n keys: its only strings (skip-link label, main landmark aria-label) default English-literal and are overridable via skipLabel / mainAriaLabel — while AdminHeader takes its own required locale. The main region owns scrolling (overflow-x-auto with focus:outline-none): focus it on route change and broad tables scroll horizontally inside the frame. Header-slot dropdowns must portal to document.body — the content column clips (overflow-y-hidden). Pass a distinct id if a second frame ever appears, so the skip link still targets the right <main>. AdminLayout owns the "which admin frame" story — AdminShell is the legacy template retained per ADR-035; don't start new work on it.
Responsive behavior
The content column's <main> carries a responsive gutter (p-4 md:p-6) and sits in a min-w-0 flex column, so wide tables truncate or scroll inside the frame instead of blowing it out. Mobile is owned by the sidebar slot: compose the Sidebar molecule with collapsible and a breakpoint ( "lg" default) and it swaps to a portal drawer (scrim + focus trap + Escape + scroll lock) below the breakpoint while staying a persistent sticky rail at and above it.
Migrating from AdminSidebar
AdminSidebar (organism) is desktop-only legacy, kept for existing frames — not for new work (the storefront dashboard migrated off it in DS-CORR-08-05). Nothing changes on AdminLayout itself — the slot accepts either; new code should compose Sidebar. The API maps directly: groups becomes SidebarGroup[] (items gain icon, badge, nested children and an ADR-031 accent), activeId becomes activeKey, logo / ariaLabel / className / id are unchanged, and the mobile-drawer props (collapsible, breakpoint, defaultOpen, storageKey, toggleLabel, closeLabel) are new.
Accessibility
Skip-to-content link (WCAG 2.4.1 Bypass Blocks): translated off-canvas until focused (-translate-y-40 → focus:translate-y-0), named by its rendered skipLabel text, targeting the main region's id. The visible state uses the interactive token bg-ep-btn-primary (5.70:1 — AA in both themes) with a focus-visible ring. The main region is a real <main> landmark, focusable via tabIndex={-1} so programmatic focus lands without a visible ring trap (WCAG 2.4.3). Frame colors come from tokens, which meet AA contrast in both themes.
Props
sidebar*React.ReactNodeLeft rail slot — compose the Sidebar molecule here (ADR-035).
header*React.ReactNodeTop bar slot — AdminHeader or a custom bar.
children*React.ReactNodeMain content rendered inside the focusable <main> landmark.
skipLabelReact.ReactNode"Skip to main content"Skip-link label — English-literal default; override with a translated node (zero-owned-keys precedent).
mainAriaLabelstring"Main content"aria-label on the <main> landmark — English-literal default, overridable.
locale"en" | "fr"Optional locale forwarded from the consumer for symmetry with AdminHeader / AdminFooter. The frame itself owns zero i18n keys — accepted so callers can spread a single locale to every admin chrome component without TS excess-property errors. Internally unused.
classNamestringExtra classes on the root.
idstring"admin-layout"Base id — the main landmark gets <id>-main; the skip link targets it.
Used By
DashboardShell — storefront (packages/storefront/src/components/dashboard/DashboardShell.tsx) — imports AdminLayout from the barrel and fills the slots with its DashboardSidebar (which composes the Sidebar molecule) and DashboardTopbar, with id="admin-layout".
Props
Adminlayout
sidebarReact.ReactNode—Left rail slot — compose the Sidebar molecule here (ADR-035).
headerReact.ReactNode—Top bar slot — AdminHeader or custom bar.
childrenReact.ReactNode—Main content rendered inside the focusable main landmark.
skipLabelReact.ReactNode"Skip to main content"Skip-link label — defaults to "Skip to main content" (English-literal, overridable per DetailPageLayout precedent).
mainAriaLabelstring"Main content"aria-label on the <main> landmark — defaults to "Main content" (English-literal, overridable).
locale"en" | "fr"—Optional locale forwarded from the consumer for symmetry with sibling admin organisms (AdminHeader, AdminFooter). The frame itself owns zero i18n keys — accepted so callers can spread a single `locale` to every admin chrome component without TS excess-property errors. Internally unused.
classNamestring—Extra classes on the root.
idstring"admin-layout"Base id — main landmark gets `${id}-main`; default "admin-layout".
<Adminlayout />Accessibility
ARIA
1 ruleUsage
Import
import { Adminlayout } from "@epasslive/design-system"Example 1
<AdminLayout sidebar={<Sidebar logo={logo} groups={navGroups} activeKey="orders" collapsible />} header={<AdminHeader locale={locale} brand={brand} onSearch={openPalette} />}>{page}</AdminLayout>