{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dashboard-shell","title":"Dashboard Shell","description":"Responsive application frame with a sidebar, a header and scrolling content. Search terms: sticky header.","dependencies":[],"registryDependencies":["@siteplane/base","@siteplane/sidebar"],"files":[{"path":"packages/ui/src/components/dashboard-shell/dashboard-shell.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport {\n  Sidebar,\n  SidebarHeader,\n  SidebarInset,\n  SidebarProvider,\n  SidebarTrigger,\n} from \"../sidebar\";\n\nexport type DashboardShellLayout = \"inset\" | \"push\";\n\nexport interface DashboardShellProps extends React.ComponentProps<\n  typeof SidebarProvider\n> {\n  layout?: DashboardShellLayout;\n  /**\n   * Keeps DashboardShellHeader in view while the content scrolls. Default\n   * true. From the Sidebar's mobile breakpoint up, the shell is the\n   * viewport's height and only DashboardShellMain scrolls. Below it the page\n   * scrolls and the header sticks to the top of the viewport. Set false for a\n   * page that scrolls as a whole: the shell is then at least the viewport's\n   * height, and the header scrolls away with the page.\n   */\n  stickyHeader?: boolean;\n}\n\nexport interface DashboardShellHeaderProps extends React.ComponentProps<\"header\"> {\n  divider?: boolean;\n}\n\nexport interface DashboardShellMainProps extends React.ComponentProps<\"div\"> {\n  /**\n   * Runs the content to the edges of the main area, without its padding, for\n   * a view that lays out its own panes, such as a list beside a detail pane.\n   */\n  flush?: boolean;\n}\n\n// The parts that follow the shell's settings read them here. Outside a\n// DashboardShell no sticky rule applies, so a part there keeps the Sidebar's\n// own behaviour.\nconst DashboardShellContext: React.Context<{ stickyHeader: boolean }> =\n  React.createContext<{ stickyHeader: boolean }>({ stickyHeader: false });\n\nexport function DashboardShell({\n  children,\n  className,\n  layout = \"push\",\n  mobileBreakpoint = \"md\",\n  stickyHeader = true,\n  ...props\n}: DashboardShellProps): React.ReactElement {\n  const context = React.useMemo(() => ({ stickyHeader }), [stickyHeader]);\n  return (\n    <DashboardShellContext.Provider value={context}>\n      <SidebarProvider\n        className={cn(\"min-h-svh\", className)}\n        // The bar height and the sidebar width are the stylesheet's\n        // (design-rework.css, \"Dashboard Shell\"); an app's style overrides\n        // them. So are the sticky header's rules, which switch at the\n        // Sidebar's mobile breakpoint.\n        data-dashboard-shell=\"root\"\n        data-mobile-breakpoint={stickyHeader ? mobileBreakpoint : undefined}\n        data-sticky-header={stickyHeader || undefined}\n        layout={layout}\n        mobileBreakpoint={mobileBreakpoint}\n        {...props}\n      >\n        {children}\n      </SidebarProvider>\n    </DashboardShellContext.Provider>\n  );\n}\n\nexport function DashboardShellSidebar({\n  className,\n  collapsible = \"icon\",\n  ...props\n}: React.ComponentProps<typeof Sidebar>): React.ReactElement {\n  return (\n    <Sidebar\n      className={className}\n      collapsible={collapsible}\n      data-dashboard-shell=\"sidebar\"\n      {...props}\n    />\n  );\n}\n\nexport function DashboardShellSidebarHeader({\n  className,\n  ...props\n}: React.ComponentProps<typeof SidebarHeader>): React.ReactElement {\n  return (\n    <SidebarHeader\n      // Content shorter than the band centres in it, level with the top bar\n      // beside it (rulebook S2).\n      className={cn(\n        \"min-h-(--dashboard-shell-header-height) shrink-0 justify-center\",\n        className,\n      )}\n      data-dashboard-shell=\"sidebar-header\"\n      data-slot=\"sidebar-header\"\n      {...props}\n    />\n  );\n}\n\nexport function DashboardShellInset({\n  className,\n  ...props\n}: React.ComponentProps<typeof SidebarInset>): React.ReactElement {\n  const { stickyHeader } = React.useContext(DashboardShellContext);\n  return (\n    <SidebarInset\n      // A sticky header sticks to its nearest scroll container. The inset\n      // clips its content without becoming one (overflow: clip, where it is\n      // otherwise hidden), so below the breakpoint, where the page scrolls,\n      // the header sticks to the viewport.\n      className={cn(stickyHeader && \"overflow-clip\", className)}\n      data-dashboard-shell=\"inset\"\n      {...props}\n    />\n  );\n}\n\nexport function DashboardShellHeader({\n  className,\n  divider = true,\n  ...props\n}: DashboardShellHeaderProps): React.ReactElement {\n  // The header sits on the main surface (B1), never on the page canvas: it has\n  // no fill of its own, so the sidebar and the content never share one grey.\n  // Its inline inset is the main area's 16 px in both layouts and at every\n  // width, so a title in it starts on the content's line; a leading sidebar\n  // trigger puts its glyph on that line (design-rework.css, \"Dashboard Shell\n  // header\"). In the push layout SidebarInset moves the header clear of the\n  // expanded sidebar with the rest of the content. With the shell's\n  // stickyHeader, the default, it sticks to the top below the breakpoint,\n  // filled with the surface it sits on there, so the content scrolls under it\n  // unseen.\n  return (\n    <header\n      className={cn(\n        slotClass(\"dashboard-shell-header\", props),\n        \"flex min-h-(--dashboard-shell-header-height) shrink-0 items-center gap-2 px-4\",\n        className,\n      )}\n      data-dashboard-shell=\"header\"\n      data-divider={divider ? \"true\" : \"false\"}\n      data-slot=\"dashboard-shell-header\"\n      {...props}\n    />\n  );\n}\n\nexport function DashboardShellMain({\n  className,\n  flush = false,\n  ...props\n}: DashboardShellMainProps): React.ReactElement {\n  return (\n    <div\n      className={cn(\"min-h-0 flex-1 overflow-auto\", !flush && \"p-4\", className)}\n      data-dashboard-shell=\"main\"\n      data-flush={flush || undefined}\n      {...props}\n    />\n  );\n}\n\nexport function DashboardShellPrimary({\n  className,\n  ...props\n}: React.ComponentProps<\"section\">): React.ReactElement {\n  return (\n    <section\n      className={cn(\n        \"relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden\",\n        className,\n      )}\n      data-dashboard-shell=\"primary\"\n      {...props}\n    />\n  );\n}\n\nexport { SidebarTrigger as DashboardShellSidebarTrigger };\n","type":"registry:ui","target":"~/src/siteplane/components/dashboard-shell/dashboard-shell.tsx"},{"path":"packages/ui/src/components/dashboard-shell/index.ts","content":"export * from \"./dashboard-shell\";\n","type":"registry:ui","target":"~/src/siteplane/components/dashboard-shell/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/dashboard-shell.md","categories":["Navigation"],"meta":{"avoidWhen":["Avoid for pages without persistent navigation, such as a marketing page.","Avoid more than one shell per page."],"description":"Responsive application frame with a sidebar, a header and scrolling content.","knownLimitations":["The shell adds layout only. Routing, authentication and page data stay with the application.","By default (stickyHeader), DashboardShellMain is the only part that scrolls from the Sidebar's mobile breakpoint up, so the page's content belongs in it, and the header sticks only as a direct child of DashboardShellInset."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["dashboard-shell","dashboard","shell","sticky header"],"useWhen":["Use once at the boundary of a signed-in application to lay out its sidebar, header and scrolling content.","Keep the default stickyHeader for a viewport-sized application whose header stays in view while the content scrolls; set stickyHeader={false} for a page that scrolls as a whole."]}}