{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "app-shell", "type": "registry:ui", "title": "AppShell", "description": "The app layout skeleton: a fixed-height shell or a document scroller, with a frosted header, nav pills, and the main scroll region.", "categories": [ "navigation", "layout" ], "registryDependencies": [ "https://whiskeyjack.net/r/scroll-indicator.json", "https://whiskeyjack.net/r/utils.json" ], "files": [ { "path": "components/ui/app-shell.tsx", "type": "registry:ui", "target": "components/ui/app-shell.tsx", "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ScrollIndicator } from \"@/components/ui/scroll-indicator\";\n\n/**\n * The app shell skeleton shared by every Whiskeyjack app: a root container,\n * the frosted desktop header (floating or sticky), the standard header nav\n * pills, and the main scroll region. Composable slots rather than a\n * monolith -- app-specific chrome (Tauri window controls, sidebar rails,\n * drawers) stays in the app and nests between these pieces.\n *\n * Two scroll models (see `.claude/rules/web-apps.md`, \"Layout shell\"):\n * - `shell`: fixed-height column (`h-dvh overflow-hidden`); ONLY the AppMain\n * scroller moves. Pair with AppHeader `floating` + AppMain `self`.\n * - `document`: the page itself scrolls (content sites, simple apps). Pair\n * with AppHeader `sticky` + AppMain `document`.\n */\n\n// -- AppShell ----------------------------------------------------------------\n\nexport interface AppShellProps extends React.HTMLAttributes {\n /** 'shell': fixed h-dvh column, main scrolls. 'document': page scrolls. */\n scroll?: \"shell\" | \"document\";\n}\n\nexport const AppShell = React.forwardRef(\n ({ scroll = \"shell\", className, children, ...props }, ref) => (\n \n {children}\n \n ),\n);\nAppShell.displayName = \"AppShell\";\n\n// -- AppHeader ---------------------------------------------------------------\n\nexport interface AppHeaderProps extends React.HTMLAttributes {\n /**\n * 'floating': absolute over the scroller(s) -- content scrolls behind the\n * frost; scrollers reserve the header height (md:pt-14 / md:pt-16).\n * 'sticky': in-flow sticky bar for document-scroll apps.\n */\n variant?: \"floating\" | \"sticky\";\n /**\n * 'contained': centered title-bar (max-w 800). 'full': edge-to-edge.\n * 'none': no width/padding classes on the row -- supply them via\n * `rowClassName` (Tauri headers with window-control clearance vars).\n */\n width?: \"contained\" | \"full\" | \"none\";\n /** Header row height in Tailwind units. 14 = 56px, 16 = 64px. */\n height?: 14 | 16;\n /** Right side of the bar (action pill, toggle, ...). */\n toolbar?: React.ReactNode;\n /**\n * Absolutely-positioned chrome inside the header but outside the content\n * row -- the Tauri extension point (window-control containers pinned to the\n * window edges while the row clears them via the --wc-* vars).\n */\n chrome?: React.ReactNode;\n /** Extra classes for the inner content row (merged after the width preset). */\n rowClassName?: string;\n}\n\nexport const AppHeader = React.forwardRef(\n (\n {\n variant = \"floating\",\n width = \"contained\",\n height = 16,\n toolbar,\n chrome,\n rowClassName,\n className,\n children,\n ...props\n },\n ref,\n ) => (\n \n {chrome}\n \n {children}\n {toolbar}\n \n \n ),\n);\nAppHeader.displayName = \"AppHeader\";\n\n// -- HeaderNav ---------------------------------------------------------------\n\nexport interface HeaderNavItem {\n key: string;\n /** Passed to the link component's `to` prop. */\n to: string;\n label: React.ReactNode;\n active: boolean;\n renderIcon: (args: { active: boolean; size: number }) => React.ReactNode;\n}\n\nexport interface HeaderNavProps extends React.HTMLAttributes {\n items: HeaderNavItem[];\n /** Router link component (e.g. react-router's Link) -- same injection as MobileBottomNav. */\n linkComponent: React.ElementType;\n}\n\nexport const HeaderNav = React.forwardRef(function HeaderNav(\n { items, linkComponent: LinkComp, className, ...props },\n ref,\n) {\n return (\n \n );\n});\nHeaderNav.displayName = \"HeaderNav\";\n\n// -- AppMain -----------------------------------------------------------------\n\nexport interface AppMainProps extends React.HTMLAttributes {\n /**\n * 'self': the main element is the scroller (shell model) -- hides its native\n * scrollbar and ships a bounded ScrollIndicator. 'document': plain flex-1\n * region, the page scrolls.\n */\n scroll?: \"self\" | \"document\";\n /**\n * ScrollIndicator top offset (header height + ~4px: 60 for h-14, 68 for\n * h-16). Only used with scroll='self'.\n */\n indicatorTopOffset?: number;\n /**\n * Hide the native scrollbar at every width (default). Pass false when the\n * app gates the hiding itself (Chip Away hides it md+ only via CSS, keeping\n * the native overlay indicator on phones).\n */\n hideScrollbar?: boolean;\n /**\n * Hide the bounded ScrollIndicator below `md` (default `true`), where a\n * touch viewport's own overlay scrollbar is the better affordance.\n *\n * Pass `false` for a real desktop WINDOW, which can be resized narrower than\n * `md` while still being a desktop: the default would hand it back the native\n * scrollbar, which then runs the full height behind a floating header --\n * exactly what the indicator exists to avoid. Width is not a proxy for\n * platform once the app ships as a resizable window.\n */\n indicatorHideBelowMd?: boolean;\n}\n\nexport const AppMain = React.forwardRef(\n (\n {\n scroll = \"self\",\n indicatorTopOffset = 68,\n hideScrollbar = true,\n indicatorHideBelowMd = true,\n className,\n children,\n ...props\n },\n ref,\n ) => {\n const innerRef = React.useRef(null);\n const setRefs = (el: HTMLElement | null) => {\n innerRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) ref.current = el;\n };\n\n if (scroll === \"document\") {\n return (\n
\n {children}\n
\n );\n }\n\n return (\n <>\n \n {children}\n \n \n \n );\n },\n);\nAppMain.displayName = \"AppMain\";\n" } ], "docs": "AppShell is the root. Use scroll=\"shell\" for a fixed h-dvh column where only AppMain scrolls, or scroll=\"document\" for a content site. AppHeader floats over the scrollers (variant=\"floating\") or sits sticky in flow; a floating header means each scroller reserves md:pt-14 or md:pt-16 for its height. Tauri apps hang window controls off AppHeader's `chrome` slot.", "meta": { "group": "navigation", "related": [ "scroll-indicator", "mobile-bottom-nav", "use-route-focus" ], "exports": [ "AppShell", "AppHeader", "HeaderNav", "AppMain" ], "siteSlug": "app-shell" } }