# App Shell (`app-shell`) - SignalOS widget

> Collapsible desktop sidebar and sheet-based mobile drawer. Framework-portable: router links and the current path are injected via props.

- **Version:** 0.1.2
- **Kind:** widget · **Category:** layout
- **Install:** `npx shadcn@latest add @signalos/app-shell`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/sheet, @signalos/tooltip
- **npm dependencies:** lucide-react@^1.7.0
- **Files installed:** `src/components/widgets/app-shell/AppShell.tsx`, `src/components/widgets/app-shell/AppShell.types.ts`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Setup notes

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} />} ... />

## Usage

```tsx
import { AppShellMobileSidebar, AppShellSidebar } from "@/components/widgets/app-shell/AppShell"
```

## 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 | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `href` | `string` | yes | - |  |
| `onClick` | `(() => void) \| undefined` | no | - |  |
| `className` | `string \| undefined` | no | - |  |
| `aria-current` | `"page" \| undefined` | no | - |  |
| `children` | `ReactNode` | yes | - |  |

### `NavigationItem`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | yes | - |  |
| `collapsedLabel` | `string \| undefined` | no | - | Short label shown under the icon when the sidebar is collapsed. Falls back to `label` styled small when omitted. |
| `href` | `string` | yes | - |  |
| `icon` | `ComponentType<{ className?: string \| undefined; }>` | yes | - |  |
| `section` | `"main" \| "bottom" \| undefined` | no | `"main"` |  |

### `AppShellSidebarProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `navigationItems` | `NavigationItem[]` | yes | - |  |
| `activePath` | `string` | yes | - | Current pathname used to highlight the active item (e.g. from usePathname()). |
| `isActive` | `((href: string, activePath: string) => boolean) \| undefined` | no | `exact match or prefix match on `href + "/"`` | Active-state matcher. |
| `LinkComponent` | `ComponentType<AppShellLinkProps> \| undefined` | no | - | Router link renderer; defaults to a plain anchor. |
| `brand` | `ReactNode` | no | - | Brand block at the top (logo + name); fully consumer-owned. |
| `brandHref` | `string \| undefined` | no | `"/"` | Where the brand block links to. |
| `isCollapsed` | `boolean \| undefined` | no | `false` |  |
| `onCollapsedChange` | `((collapsed: boolean) => void) \| undefined` | no | - | When provided, renders the collapse toggle. |
| `onNavigate` | `(() => void) \| undefined` | no | - | Called on any nav link click (close overlays, telemetry). |
| `className` | `string \| undefined` | no | - |  |

### `AppShellMobileSidebarProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `navigationItems` | `NavigationItem[]` | yes | - |  |
| `activePath` | `string` | yes | - |  |
| `isActive` | `((href: string, activePath: string) => boolean) \| undefined` | no | - |  |
| `LinkComponent` | `ComponentType<AppShellLinkProps> \| undefined` | no | - |  |
| `brand` | `ReactNode` | no | - |  |
| `brandHref` | `string \| undefined` | no | - |  |
| `isOpen` | `boolean` | yes | - |  |
| `onClose` | `() => void` | yes | - |  |
| `className` | `string \| undefined` | no | - |  |

## Changelog

# 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-ui` `dashboard-sidebar.tsx` + `dashboard-mobile-sidebar.tsx`.
- Decoupled from Next.js: `usePathname()` replaced by an `activePath` prop, `next/link`
  by an injectable `LinkComponent` (plain `<a>` default), `next/image` brand block by a
  consumer-owned `brand` slot.
- The hardcoded label-abbreviation switch replaced by `NavigationItem.collapsedLabel`.
- Raw classes (`bg-white/10`, `text-navy-300`, `hover:bg-brand-graphite`) normalized to
  `sidebar-*` semantic tokens.
- Mobile drawer rebuilt on the `@signalos/sheet` primitive (focus trap, scrim, esc-to-close).
