App Shell
v0.1.2Collapsible desktop sidebar and sheet-based mobile drawer. Framework-portable: router links and the current path are injected via props.
Install
npx shadcn@latest add @signalos/app-shellRequires a configured @signalos registry and a valid SIGNALOS_REGISTRY_TOKEN - get access. Registry dependencies (@signalos/tokens, @signalos/utils, @signalos/sheet, @signalos/tooltip) are pulled automatically.
In a Next.js app, wire the shell to the router once: const pathname = usePathname() <AppShellSidebar activePath={pathname} LinkComponent={({ href, ...p }) => <Link href={href} {...p} />} ... />
Preview
Example & code
app-shell.example.tsx
// Example: wiring the shell in a plain React app (router-agnostic).
// In Next.js, pass `activePath={usePathname()}` and a Link adapter:
// LinkComponent={({ href, ...p }) => <Link href={href} {...p} />}
import { useState } from "react"
import { Inbox, LayoutDashboard, Settings } from "lucide-react"
import { AppShellSidebar } from "@/components/widgets/app-shell/AppShell"
import type { NavigationItem } from "@/components/widgets/app-shell/AppShell.types"
const navigation: NavigationItem[] = [
{
label: "Dashboard",
collapsedLabel: "Home",
href: "#/dashboard",
icon: LayoutDashboard,
},
{ label: "Signal Feed", collapsedLabel: "Feed", href: "#/feed", icon: Inbox },
{ label: "Settings", href: "#/settings", icon: Settings, section: "bottom" },
]
export default function Example() {
const [collapsed, setCollapsed] = useState(false)
return (
<div className="flex h-[480px] overflow-hidden rounded-lg border border-border [&_aside]:!flex [&_aside]:h-full">
<AppShellSidebar
navigationItems={navigation}
activePath="#/feed"
brand={
<div className="flex items-center gap-2">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground">
A
</span>
<span className="text-lg font-semibold text-sidebar-foreground">
Acme Ops
</span>
</div>
}
isCollapsed={collapsed}
onCollapsedChange={setCollapsed}
/>
<main className="flex-1 bg-background p-6 text-sm text-muted-foreground">
Route content renders here.
</main>
</div>
)
}
Props
AppShellLinkProps
| Prop | Type | Default | Description |
|---|---|---|---|
| href* | string | - | |
| onClick | (() => void) | undefined | - | |
| className | string | undefined | - | |
| aria-current | "page" | undefined | - | |
| children* | ReactNode | - |
NavigationItem
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | - | |
| collapsedLabel | string | undefined | - | Short label shown under the icon when the sidebar is collapsed. Falls back to `label` styled small when omitted. |
| href* | string | - | |
| icon* | ComponentType<{ className?: string | undefined; }> | - | |
| section | "main" | "bottom" | undefined | "main" |
AppShellSidebarProps
| Prop | Type | Default | Description |
|---|---|---|---|
| navigationItems* | NavigationItem[] | - | |
| activePath* | string | - | Current pathname used to highlight the active item (e.g. from usePathname()). |
| isActive | ((href: string, activePath: string) => boolean) | undefined | exact match or prefix match on `href + "/"` | Active-state matcher. |
| LinkComponent | ComponentType<AppShellLinkProps> | undefined | - | Router link renderer; defaults to a plain anchor. |
| brand | ReactNode | - | Brand block at the top (logo + name); fully consumer-owned. |
| brandHref | string | undefined | "/" | Where the brand block links to. |
| isCollapsed | boolean | undefined | false | |
| onCollapsedChange | ((collapsed: boolean) => void) | undefined | - | When provided, renders the collapse toggle. |
| onNavigate | (() => void) | undefined | - | Called on any nav link click (close overlays, telemetry). |
| className | string | undefined | - |
AppShellMobileSidebarProps
| Prop | Type | Default | Description |
|---|---|---|---|
| navigationItems* | NavigationItem[] | - | |
| activePath* | string | - | |
| isActive | ((href: string, activePath: string) => boolean) | undefined | - | |
| LinkComponent | ComponentType<AppShellLinkProps> | undefined | - | |
| brand | ReactNode | - | |
| brandHref | string | undefined | - | |
| isOpen* | boolean | - | |
| onClose* | () => void | - | |
| className | string | undefined | - |
npm dependencies
lucide-react@^1.7.0Changelog
app-shell
0.1.2
- Content sync fix to bring source in line with published registry payload.
0.1.1
- Standardize inline comment formatting (em dash → hyphen); no runtime or API change.
0.1.0
- Initial release, lifted from
signal-core-uidashboard-sidebar.tsx+dashboard-mobile-sidebar.tsx. - Decoupled from Next.js:
usePathname()replaced by anactivePathprop,next/linkby an injectableLinkComponent(plain<a>default),next/imagebrand block by a consumer-ownedbrandslot. - The hardcoded label-abbreviation switch replaced by
NavigationItem.collapsedLabel. - Raw classes (
bg-white/10,text-navy-300,hover:bg-brand-graphite) normalized tosidebar-*semantic tokens. - Mobile drawer rebuilt on the
@signalos/sheetprimitive (focus trap, scrim, esc-to-close).