返回组件列表
布局

应用外壳

带侧边栏导航、面包屑、操作、指标卡和用户控制的应用页面框架。

页面预览
源码
这里展示 registry 中会被 shadcn 安装到目标项目的组件源码。
registry/default/app-shell/app-shell.tsx
import {
  ActivityIcon,
  BellIcon,
  BoxesIcon,
  HomeIcon,
  LayoutDashboardIcon,
  PlusIcon,
  SearchIcon,
  SettingsIcon,
  UsersIcon,
} from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarSeparator,
  SidebarTrigger,
} from "@/components/ui/sidebar"

type AppShellCopy = {
  brand: string
  workspace: string
  navLabel: string
  navItems: [string, string, string]
  section: string
  title: string
  description: string
  action: string
  cards: Array<{
    label: string
    value: string
    description: string
  }>
  activityTitle: string
  activityDescription: string
  user: string
  role: string
}

type AppShellProps = {
  copy?: AppShellCopy
}

const defaultCopy: AppShellCopy = {
  brand: "Registry UI",
  workspace: "Component workspace",
  navLabel: "Product",
  navItems: ["Overview", "Components", "Team"],
  section: "Dashboard",
  title: "Reusable interface system",
  description:
    "A full application shell with navigation, breadcrumbs, summary cards, and user controls.",
  action: "New component",
  cards: [
    {
      label: "Components",
      value: "42",
      description: "12 ready for release",
    },
    {
      label: "Installs",
      value: "1.8k",
      description: "Across active projects",
    },
    {
      label: "Coverage",
      value: "96%",
      description: "Preview and source checks",
    },
  ],
  activityTitle: "Release activity",
  activityDescription: "Registry build completed and install verification passed.",
  user: "Chen Ning",
  role: "Design systems lead",
}

const navIcons = [LayoutDashboardIcon, BoxesIcon, UsersIcon]

export function AppShell({ copy = defaultCopy }: AppShellProps) {
  return (
    <SidebarProvider className="min-h-[520px] w-full max-w-5xl overflow-hidden rounded-xl border">
      <Sidebar collapsible="none" className="border-r">
        <SidebarHeader>
          <div className="flex items-center gap-2 px-2 py-1">
            <div className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
              <BoxesIcon />
            </div>
            <div className="flex min-w-0 flex-col">
              <span className="truncate text-sm font-medium">{copy.brand}</span>
              <span className="truncate text-xs text-muted-foreground">
                {copy.workspace}
              </span>
            </div>
          </div>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>{copy.navLabel}</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                {copy.navItems.map((item, index) => {
                  const Icon = navIcons[index]

                  return (
                    <SidebarMenuItem key={item}>
                      <SidebarMenuButton
                        isActive={index === 0}
                        render={<a href={`#${item.toLowerCase()}`} />}
                      >
                        <Icon />
                        <span>{item}</span>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  )
                })}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarSeparator />
        <SidebarFooter>
          <div className="flex items-center gap-2 rounded-lg px-2 py-1">
            <Avatar size="sm">
              <AvatarFallback>CN</AvatarFallback>
            </Avatar>
            <div className="min-w-0 flex-1">
              <div className="truncate text-sm font-medium">{copy.user}</div>
              <div className="truncate text-xs text-muted-foreground">
                {copy.role}
              </div>
            </div>
          </div>
        </SidebarFooter>
      </Sidebar>
      <SidebarInset className="min-w-0 overflow-hidden">
        <header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b px-4">
          <div className="flex min-w-0 items-center gap-2">
            <SidebarTrigger />
            <Breadcrumb>
              <BreadcrumbList>
                <BreadcrumbItem>
                  <BreadcrumbLink href="#">
                    <HomeIcon data-icon="inline-start" />
                  </BreadcrumbLink>
                </BreadcrumbItem>
                <BreadcrumbSeparator />
                <BreadcrumbItem>
                  <BreadcrumbPage>{copy.section}</BreadcrumbPage>
                </BreadcrumbItem>
              </BreadcrumbList>
            </Breadcrumb>
          </div>
          <div className="flex shrink-0 gap-1">
            <Button size="icon-sm" variant="ghost" aria-label="Search">
              <SearchIcon />
            </Button>
            <Button size="icon-sm" variant="ghost" aria-label="Notifications">
              <BellIcon />
            </Button>
            <Button size="icon-sm" variant="ghost" aria-label="Settings">
              <SettingsIcon />
            </Button>
          </div>
        </header>
        <main className="flex min-w-0 flex-1 flex-col gap-4 overflow-auto p-4">
          <div className="flex flex-wrap items-start justify-between gap-3">
            <div className="flex min-w-0 flex-col gap-1">
              <Badge className="w-fit" variant="secondary">
                {copy.section}
              </Badge>
              <h2 className="text-2xl font-semibold">{copy.title}</h2>
              <p className="max-w-2xl text-sm text-muted-foreground">
                {copy.description}
              </p>
            </div>
            <Button>
              <PlusIcon data-icon="inline-start" />
              {copy.action}
            </Button>
          </div>
          <div className="grid gap-3 md:grid-cols-3">
            {copy.cards.map((card) => (
              <Card key={card.label}>
                <CardHeader className="pb-0">
                  <CardDescription>{card.label}</CardDescription>
                  <CardTitle>{card.value}</CardTitle>
                </CardHeader>
                <CardContent>
                  <p className="text-sm text-muted-foreground">
                    {card.description}
                  </p>
                </CardContent>
              </Card>
            ))}
          </div>
          <Card>
            <CardHeader>
              <div className="flex items-start gap-3">
                <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
                  <ActivityIcon />
                </div>
                <div className="flex min-w-0 flex-col gap-1">
                  <CardTitle>{copy.activityTitle}</CardTitle>
                  <CardDescription>{copy.activityDescription}</CardDescription>
                </div>
              </div>
            </CardHeader>
          </Card>
        </main>
      </SidebarInset>
    </SidebarProvider>
  )
}