Ansora app shell
Application skeleton on top of the sidebar — collapsible navigation with a brand and a user block, a top bar with the page title, breadcrumbs and an actions slot, a skip link, and a slot for the content. A server component; the only client parts are the sidebar itself, deriving the active nav item from the path, and the user menu.
blocknavigation
Install
pnpm dlx shadcn@4.23.0 add @ansora/app-shellExamples
Usage
Call readSidebarState() from @/lib/ansora/sidebar-state in a server layout and pass the result to AppShell as defaultOpen — without it the sidebar re-expands on every navigation.
Props: defaultOpen and nav are required; brand, navSecondary, user, userMenu, title, breadcrumbs and headerActions are optional and each one is the only way to fill in its part of the skeleton.
brand({ label, icon?, href? }) is the row at the top of the sidebar column. Without it the column starts with the navigation.user({ name, email, initials }) draws the footer block; without it no footer is rendered at all.userMenu(NavItem[]) is what its dropdown contains — without it the block is inert. The initials are yours to supply: deriving them from a Czech name with a title ("MUDr. Jana Krejčí") would produce "MJ".titlerenders as the page's<h1>in the top bar. Breadcrumbs do not replace it; with both filled in the bar shows the title, then a separator, then the breadcrumbs. The last breadcrumb always renders as plain text, even when it has anhref— it is the current page, not a link to it.headerActionsis the right-hand side of the top bar. This is where<DashboardActions />from @ansora/dashboard goes.
nav and breadcrumbs are pure data and the shell knows no icon library: icon on a nav item is a ReactNode, not an icon name (Phosphor, regular weight; switching the active item to fill is up to you, the shell only ever sees a finished node).
Two things the shell already renders, so that you do not add a second one:
- A skip link as the first focusable element on the page. Its target is the container around
children, which therefore carries the idansora-app-shell-content— that id is taken, do not reuse it. - The sidebar trigger, in the top bar rather than in the sidebar column. It has to be there: below 768px the sidebar renders as a Sheet that starts closed, and a closed dialog mounts no children, so a trigger inside
<Sidebar>would not exist in the DOM on a phone and nothing would open the navigation.
import { AppShell, type NavGroup } from "@/components/ansora/app-shell"
import { readSidebarState } from "@/lib/ansora/sidebar-state"
const nav: NavGroup[] = [
{ label: "Provoz", items: [{ href: "/", label: "Přehled", icon: <HouseIcon /> }] },
]
export default async function Layout({ children }: { children: React.ReactNode }) {
return (
<AppShell
defaultOpen={await readSidebarState()}
brand={{ label: "Ambulance Vinohrady", icon: <HeartbeatIcon />, href: "/" }}
nav={nav}
user={{ name: "MUDr. Jana Krejčí", email: "j.krejci@ambulance.cz", initials: "JK" }}
userMenu={[{ href: "/ucet", label: "Účet", icon: <UserCircleIcon /> }]}
title="Přehled"
headerActions={<DashboardActions />}
>
{children}
</AppShell>
)
}
Files
registry/ansora/components/app-shell.tsx→@components/ansora/app-shell.tsxregistry/ansora/components/app-shell-nav.tsx→@components/ansora/app-shell-nav.tsxregistry/ansora/components/app-shell-user.tsx→@components/ansora/app-shell-user.tsxregistry/ansora/lib/sidebar-state.ts→@lib/ansora/sidebar-state.ts
Dependencies
@ansora/sidebarhttps://ui.shadcn.com/r/styles/base-nova/avatar.jsonhttps://ui.shadcn.com/r/styles/base-nova/dropdown-menu.jsonhttps://ui.shadcn.com/r/styles/base-nova/separator.json
Ansora value change
One 'from → to' pair: what a value was and what it is now, legible without relying on colour. The caller names both sides, which is also what a screen reader announces.
Ansora overview dashboard
Overview screen for an outpatient clinic / front desk — a KPI strip as one card with internal dividers, a visits chart and a filterable patients table with statuses. The header actions (date range, export, light/dark toggle) ship separately as DashboardActions and belong in the shell's headerActions; the screen renders no top bar, no search field and no user menu of its own. Composes shadcn primitives (Card, Table, Button, Avatar, Select, Tabs, Chart, Badge) on top of @ansora/tokens — only the status logic is vendored (@ansora/status-pill); the primitives are installed from the public shadcn registry and pick up our look automatically.