源码
这里展示 registry 中会被 shadcn 安装到目标项目的组件源码。
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>
)
}