layouts
AppShell
Universal in-app layout — TopBar with brand + breadcrumbs, collapsible Sidebar with three modes (expanded / collapsed / hover), scrollable content area. The chrome around every product page.
The sidebar mode is persisted across reloads via a small client-side store. Nav structure is supplied via `navGroups` so each product wires its own sections. AppShell already wraps everything in `TooltipProvider` — don't add another one inside. Two opt-out props for special cases: `hideBrand` suppresses the wordmark in the topbar (use when the product's identity is carried entirely by another piece of chrome, e.g. an OrgSwitcher with workspace avatar); `hideSidebar` removes the left rail entirely (`navGroups` becomes optional in that mode) — useful for routes that want the global topbar but no left nav, e.g. a `/settings` family when the team prefers familiar chrome over strict focused-mode.
Install
Pull this component (and its dependencies) straight into your app via the shadcn CLI:
npx shadcn@latest add https://design.oapps.io/r/app-shell.jsonOr import from the workspace package:
import { AppShell, NavItem, NavGroup } from "@8maverik8/design";Examples
Schematic preview
AppShell occupies the full viewport, so the live one can't honestly fit inside a doc page. The mock below mirrors the exact structure (TopBar + Sidebar + main + bottom mode-picker). Mount the real one as the root layout of your Next.js app.
import { AppShell, type NavGroup } from "@8maverik8/design";
import { Home, Users, Megaphone } from "lucide-react";
const navGroups: NavGroup[] = [
{
section: "Workspace",
items: [
{ label: "Dashboard", icon: Home, href: "/" },
{ label: "Audiences", icon: Users, href: "/audiences" },
{ label: "Campaigns", icon: Megaphone, href: "/campaigns" },
],
},
];
export default function RootLayout({ children }) {
return (
<AppShell wordmark="cip" navGroups={navGroups}>
{children}
</AppShell>
);
}Variants
Chrome mode
full(default)Default. Brand + topbar slots + collapsible left sidebar + scrollable main. The shape of every standard product page.hideBrandDrop the wordmark + leading separator. Use when another topbar slot (typically `<OrgSwitcher>`) carries the product identity.hideSidebarDrop the entire left rail; topbar persists. Use when a route family doesn't fit a left-nav model but you still want global topbar consistency (sign-out, brand, search). Alternative to `<SettingsShell>` for teams that prefer not to drop global chrome entirely.
Anatomy
wordmarkLowercase product name shown in the topbar (e.g. `"cip"`). Hidden when `hideBrand={true}`.hideBrandSuppress the wordmark + the leading separator in the topbar. The first left slot collapses against the left edge. Use when another piece of chrome (e.g. `<OrgSwitcher>` with workspace avatar) already carries the product's identity.hideSidebarRemove the left rail entirely. The topbar still renders (brand + slots + UserMenu). `navGroups` becomes optional in this mode. Use for routes that want consistent global chrome but no left nav (e.g. `/settings` family when the team prefers familiar chrome over `<SettingsShell>`'s strict focused-mode).navGroupsArray of `{ section, items[] }`. Sections render as mono-caps headings; items are `{ label, icon, href }`. Required unless `hideSidebar={true}`.topbarLeftSlot to the right of the brand — typically a `<BreadcrumbTrail>`.topbarCenterCenter slot — typically the `<SearchPalette>` trigger button.topbarRightRight slot — typically `<FeedbackButton>` + `<UserMenu>`.
Guidelines
- Wire the `navGroups` once at the app's root layout — don't switch them per route.The sidebar should feel constant across the product. If a section needs to disappear contextually, hide it inside the page, not by mutating navGroups.
- Pick exactly ONE pattern for `/settings`: either `<AppShell hideSidebar>` (keep familiar chrome, drop left nav) OR `<SettingsShell>` (strict focused-mode, no global chrome at all). Don't mix — the user must feel a consistent transition into settings across products.
- Render your own page padding around children.AppShell's `<main>` already has `p-6`. DSPageShell's footer relies on this exact gutter to bleed correctly.
- Pass `hideSidebar={true}` AND `navGroups`.When `hideSidebar` is true, `navGroups` is ignored — passing it is dead code that drifts away from reality. Drop the prop.