@quanticjs/react-layouts
v8.2.0
Published
Application shell & layout components for QuanticJS — AppShell, Sidebar, TopBar, UserMenu
Downloads
1,093
Readme
@quanticjs/react-layouts
Application shell for QuanticJS apps — sidebar, top bar, mobile drawer, and user menu, driven by a nav config instead of hand-rolled chrome.
AppShell— composition root: persistent sidebar onlg+with collapse-to-icons (persisted tolocalStorage), overlay drawer belowlg(focus-trapped, Escape/backdrop close), skip-to-content link,<main>landmark.Sidebar— renders aNavItem[]config with section headers, icons, one nesting level, and permission/role gating viaCanfrom@quanticjs/react-core.TopBar—start/actionsslots plus a built-inUserMenu(session name fromuseAuth, logout viauseLogout, custom entries, full keyboard support).useSidebarState— read/control collapse and drawer state from app code.- Router-agnostic via
renderLink/isActive. No state-management dependency (useSyncExternalStoreinternally). Styled exclusively with@quanticjs/tailwind-presetsemantic tokens, so dark mode works automatically.
Install
pnpm add @quanticjs/react-layouts @quanticjs/react-ui @quanticjs/react-coreRequires @quanticjs/tailwind-preset >= 8 in the consuming app's CSS build — components render with v8 token utilities (shadow-* tiers, z-(--z-*), animate-*) that compile to nothing on older presets. See docs/MIGRATION-8.md.
@quanticjs/react-core powers permission gating and the user menu. Without a <QuanticProvider> ancestor the shell still renders — gated nav items and the session menu are simply hidden (a one-time warning logs in dev builds).
Usage with react-router
import { AppShell, type NavItem } from '@quanticjs/react-layouts';
import { Link, useLocation } from 'react-router-dom';
import { Home, Settings, Shield } from 'lucide-react';
const nav: NavItem[] = [
{ label: 'Dashboard', href: '/', icon: <Home /> },
{
section: 'Administration',
label: 'Users',
href: '/admin/users',
icon: <Shield />,
permission: 'users:manage',
},
{
section: 'Administration',
label: 'Settings',
href: '/admin/settings',
icon: <Settings />,
role: 'admin',
items: [
{ label: 'General', href: '/admin/settings/general' },
{ label: 'Branding', href: '/admin/settings/branding' },
],
},
];
export function Layout({ children }: { children: React.ReactNode }) {
const { pathname } = useLocation();
return (
<AppShell
nav={nav}
renderLink={(item, props) => (
<Link to={item.href} {...props}>
{props.children}
</Link>
)}
isActive={(href) => pathname === href}
topBar={{
start: <h1 className="text-sm font-semibold">Delivery Hub</h1>,
userMenu: {
menuItems: [{ label: 'Profile', onSelect: () => navigate('/profile') }],
},
}}
>
{children}
</AppShell>
);
}Usage with Next.js (App Router)
'use client';
import { AppShell, type NavItem } from '@quanticjs/react-layouts';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const nav: NavItem[] = [
{ label: 'Dashboard', href: '/' },
{ label: 'Reports', href: '/reports', permission: 'reports:read' },
];
export function Shell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AppShell
nav={nav}
renderLink={(item, props) => (
<Link href={item.href} className={props.className}>
{props.children}
</Link>
)}
isActive={(href) => pathname === href}
>
{children}
</AppShell>
);
}AppShell is SSR-safe: the first render always uses the default expanded sidebar (no localStorage read), then syncs the persisted state after mount — no hydration mismatch.
Brand, nav badges, search & user subtitle
The shell renders a full dashboard chrome out of the box — a sidebar brand header, count badges on nav items, a top-bar search slot with an optional divider, and a subtitle under the user's name:
<AppShell
nav={[
{ label: 'Dashboard', href: '/' },
{ label: 'Applications', href: '/apps', badge: 4 }, // count pill (dot when collapsed)
{ label: 'Support', href: '/support', badge: 2 },
]}
renderLink={renderLink}
brand={<Brand />} // logo + wordmark (sidebar header)
brandCollapsed={<LogoOnly />} // shown when the sidebar collapses to icons
topBar={{
start: <Breadcrumb items={[{ label: 'Portal', href: '/' }, { label: 'Dashboard' }]} />,
search: <SearchField />, // right cluster, before actions
actions: <NotificationCenter />,
showUserDivider: true, // vertical rule before the user menu
userMenu: { subtitle: 'Enterprise Client' }, // secondary line under the name
}}
>
{children}
</AppShell>NavItem.badgerenders a right-aligned count pill (a small dot when the sidebar is collapsed; the count is kept in the item's accessible name).brand/brandCollapsedrender in a header band above the nav.topBar.searchsits in the right cluster;showUserDivideradds the divider.UserMenuusesAvatar(initials + image fallback) from@quanticjs/react-uiand showssubtitleunder the name.- Build the page body with
StatCard,Progress,Breadcrumb, andStatusBadgefrom@quanticjs/react-ui. The brand color is your app'sprimary(set via@quanticjs/tailwind-preset) — the shell ships brand-neutral.
Adding the notification bell
AppShell is notification-agnostic by design — it has no dependency on the
notification engine. Mount the bell from @quanticjs/notification-ui in the
topBar.actions slot, and wrap the shell in a NotificationProvider so the bell
reads the app's appId and opens the realtime socket. The provider must sit
above AppShell (or at least above the bell):
import { AppShell } from '@quanticjs/react-layouts';
import { NotificationProvider, NotificationCenter } from '@quanticjs/notification-ui';
export function Layout({ children }: { children: React.ReactNode }) {
return (
// appId = this app's slug in the notification engine's application registry.
// Omit appId on a shell/portal to get the unified cross-app inbox.
<NotificationProvider appId="delivery-hub">
<AppShell
nav={nav}
renderLink={renderLink}
topBar={{
start: <h1 className="text-sm font-semibold">Delivery Hub</h1>,
actions: <NotificationCenter />,
userMenu: { menuItems: [{ label: 'Profile', onSelect: () => navigate('/profile') }] },
}}
>
{children}
</AppShell>
</NotificationProvider>
);
}The actions slot takes any ReactNode, so you can place other controls
(theme toggle, etc.) alongside the bell. See @quanticjs/notification-ui for the
provider options, the engine endpoint contract, and the BFF proxy requirement.
API
AppShellProps
| Prop | Type | Notes |
|---|---|---|
| nav | NavItem[] | Required. |
| renderLink | (item, { className, children }) => ReactNode | Required router adapter. |
| isActive | (href: string) => boolean | Active item styling. |
| topBar | { start?, actions?, userMenu? } | userMenu: false hides the built-in menu. |
| labels | Partial<ShellLabels> | Override any built-in string (i18n). |
| storageKey | string | Collapse persistence key. Default quantic.sidebar. |
| contentClassName | string | Override <main> classes. Default gutter is p-4 sm:p-6; pass p-0 for full-bleed pages. |
NavItem
label, href, icon?, permission?, role?, section? (group header for consecutive items), items? (one nesting level — deeper levels are flattened with a dev warning).
Items with permission/role render inside Can; a section header is hidden automatically when every item in the section is hidden.
Labels / i18n
All built-in strings are overridable, either per component:
<AppShell labels={{ logout: 'Abmelden', skipToContent: 'Zum Inhalt springen' }} … />…or app-wide via the TranslationProvider from @quanticjs/react-ui — this package registers the layouts namespace (LayoutsTranslations), and every shell component resolves each label with the precedence explicit labels prop > provider catalog > English default:
import { TranslationProvider } from '@quanticjs/react-ui';
<TranslationProvider
translations={{ layouts: { shell: { logout: 'Abmelden', navigation: 'Hauptnavigation' } } }}
>
<AppShell … />
</TranslationProvider>useShellLabels(overrides?) is exported for consumers building custom shell parts that should follow the same resolution.
Exports
AppShell, type AppShellProps
Sidebar, type SidebarProps, type NavItem, type RenderLink
TopBar, type TopBarProps
UserMenu, type UserMenuProps, type UserMenuItem
useSidebarState
defaultLabels, useShellLabels, type ShellLabels, type LayoutsTranslationsRTL & reduced motion
All positioned chrome — the skip link, sidebar border, nesting indent, mobile drawer, and user-menu dropdown — uses Tailwind logical properties (start-*, ps-*, border-e, text-start). Set dir="rtl" on <html> (or any ancestor) and the shell mirrors automatically; no props or providers involved. Hover/expand transitions respect prefers-reduced-motion via the global block in @quanticjs/tailwind-preset's theme.css.
