Sidebar
App shells built on the sidebar: collapsible, inset, floating, and double-sided.
Collapsible sidebar
import { AppSidebar } from "./components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
export default function Page() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mr-2 data-vertical:h-4 data-vertical:self-auto"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Build Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
)
}"use client"
import * as React from "react"
import { NavMain } from "./nav-main"
import { NavProjects } from "./nav-projects"
import { NavUser } from "./nav-user"
import { TeamSwitcher } from "./team-switcher"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@/components/ui/sidebar"
import { GalleryVerticalEndIcon, AudioLinesIcon, TerminalIcon, TerminalSquareIcon, BotIcon, BookOpenIcon, Settings2Icon, FrameIcon, PieChartIcon, MapIcon } from "lucide-react"
// This is sample data.
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Acme Inc",
logo: (
<GalleryVerticalEndIcon
/>
),
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: (
<AudioLinesIcon
/>
),
plan: "Startup",
},
{
name: "Evil Corp.",
logo: (
<TerminalIcon
/>
),
plan: "Free",
},
],
navMain: [
{
title: "Playground",
url: "#",
icon: (
<TerminalSquareIcon
/>
),
isActive: true,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
],
},
{
title: "Models",
url: "#",
icon: (
<BotIcon
/>
),
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
},
{
title: "Documentation",
url: "#",
icon: (
<BookOpenIcon
/>
),
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: (
<Settings2Icon
/>
),
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: (
<FrameIcon
/>
),
},
{
name: "Sales & Marketing",
url: "#",
icon: (
<PieChartIcon
/>
),
},
{
name: "Travel",
url: "#",
icon: (
<MapIcon
/>
),
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}"use client"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
import { ChevronRightIcon } from "lucide-react"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: React.ReactNode
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
defaultOpen={item.isActive}
className="group/collapsible"
render={<SidebarMenuItem />}
>
<CollapsibleTrigger
render={<SidebarMenuButton tooltip={item.title} />}
>
{item.icon}
<span>{item.title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-open/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton render={<a href={subItem.url} />}>
<span>{subItem.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { MoreHorizontalIcon, FolderIcon, ArrowRightIcon, Trash2Icon } from "lucide-react"
export function NavProjects({
projects,
}: {
projects: {
name: string
url: string
icon: React.ReactNode
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton render={<a href={item.url} />}>
{item.icon}
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuAction
showOnHover
className="aria-expanded:bg-muted"
/>
}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-fit"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<FolderIcon
/>
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<ArrowRightIcon
/>
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<Trash2Icon
/>
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}"use client"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { ChevronsUpDownIcon, SparklesIcon, BadgeCheckIcon, CreditCardIcon, BellIcon, LogOutIcon } from "lucide-react"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
}
>
<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDownIcon className="ml-auto size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-fit"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<SparklesIcon
/>
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheckIcon
/>
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon
/>
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<BellIcon
/>
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOutIcon
/>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}"use client"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { ChevronsUpDownIcon, PlusIcon } from "lucide-react"
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ReactNode
plan: string
}[]
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) {
return null
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
size="lg"
className="data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground"
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
{activeTeam.logo}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDownIcon className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-fit"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-md border">
{team.logo}
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<PlusIcon className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add sidebar-07Inset sidebar
import { AppSidebar } from "./components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
export default function Page() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mr-2 data-vertical:h-4 data-vertical:self-auto"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Build Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-screen flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
)
}"use client"
import * as React from "react"
import { NavMain } from "./nav-main"
import { NavProjects } from "./nav-projects"
import { NavSecondary } from "./nav-secondary"
import { NavUser } from "./nav-user"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { TerminalSquareIcon, BotIcon, BookOpenIcon, Settings2Icon, LifeBuoyIcon, SendIcon, FrameIcon, PieChartIcon, MapIcon, TerminalIcon } from "lucide-react"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Playground",
url: "#",
icon: (
<TerminalSquareIcon
/>
),
isActive: true,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
],
},
{
title: "Models",
url: "#",
icon: (
<BotIcon
/>
),
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
},
{
title: "Documentation",
url: "#",
icon: (
<BookOpenIcon
/>
),
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: (
<Settings2Icon
/>
),
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Support",
url: "#",
icon: (
<LifeBuoyIcon
/>
),
},
{
title: "Feedback",
url: "#",
icon: (
<SendIcon
/>
),
},
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: (
<FrameIcon
/>
),
},
{
name: "Sales & Marketing",
url: "#",
icon: (
<PieChartIcon
/>
),
},
{
name: "Travel",
url: "#",
icon: (
<MapIcon
/>
),
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" render={<a href="#" />}>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<TerminalIcon className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Acme Inc</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}"use client"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
import { ChevronRightIcon } from "lucide-react"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon: React.ReactNode
isActive?: boolean
items?: {
title: string
url: string
}[]
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
defaultOpen={item.isActive}
render={<SidebarMenuItem />}
>
<SidebarMenuButton
tooltip={item.title}
render={<a href={item.url} />}
>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger
render={
<SidebarMenuAction className="aria-expanded:rotate-90" />
}
>
<ChevronRightIcon
/>
<span className="sr-only">Toggle</span>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton render={<a href={subItem.url} />}>
<span>{subItem.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { MoreHorizontalIcon, FolderIcon, ShareIcon, Trash2Icon } from "lucide-react"
export function NavProjects({
projects,
}: {
projects: {
name: string
url: string
icon: React.ReactNode
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton render={<a href={item.url} />}>
{item.icon}
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuAction
showOnHover
className="aria-expanded:bg-muted"
/>
}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<FolderIcon className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2Icon className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton>
<MoreHorizontalIcon
/>
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}"use client"
import * as React from "react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: React.ReactNode
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton size="sm" render={<a href={item.url} />}>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}"use client"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { ChevronsUpDownIcon, SparklesIcon, BadgeCheckIcon, CreditCardIcon, BellIcon, LogOutIcon } from "lucide-react"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
}
>
<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDownIcon className="ml-auto size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<SparklesIcon
/>
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheckIcon
/>
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon
/>
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<BellIcon
/>
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOutIcon
/>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add sidebar-08Sidebar Collapsible Sections
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { AppSidebar } from "./components/app-sidebar"
export default function Sidebar01() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="sm:hidden" />
</div>
</header>
</SidebarInset>
</SidebarProvider>
)
}"use client"
import {
MegaphoneIcon,
BellRingIcon,
CalendarIcon,
CalendarDaysIcon,
ListTodoIcon,
NewspaperIcon,
NotebookIcon,
ListChecksIcon,
Settings2Icon,
} from "lucide-react"
import { LayoutDashboard, Package } from "lucide-react"
import { Sidebar, SidebarContent } from "@/components/ui/sidebar"
import { NavCollapsible } from "./nav-collapsible"
import { NavFooter } from "./nav-footer"
import { NavHeader } from "./nav-header"
import { NavMain } from "./nav-main"
import type { SidebarData } from "./types"
const data: SidebarData = {
user: {
name: "ephraim",
email: "ephraim@blocks.so",
avatar: "/avatar-01.png",
},
navMain: [
{
id: "overview",
title: "Overview",
url: "#",
icon: LayoutDashboard,
isActive: true,
},
{
id: "tasks",
title: "Tasks",
url: "#",
icon: ListTodoIcon,
},
{
id: "meetings",
title: "Meetings",
url: "#",
icon: CalendarDaysIcon,
},
{
id: "notes",
title: "Notes",
url: "#",
icon: NotebookIcon,
},
{
id: "calendar",
title: "Calendar",
url: "#",
icon: CalendarIcon,
},
{
id: "completed",
title: "Completed",
url: "#",
icon: ListChecksIcon,
},
{
id: "notifications",
title: "Notifications",
url: "#",
icon: BellRingIcon,
},
],
navCollapsible: {
favorites: [
{
id: "design",
title: "Design",
href: "#",
color: "bg-green-400 dark:bg-green-300",
},
{
id: "development",
title: "Development",
href: "#",
color: "bg-blue-400 dark:bg-blue-300",
},
{
id: "workshop",
title: "Workshop",
href: "#",
color: "bg-orange-400 dark:bg-orange-300",
},
{
id: "personal",
title: "Personal",
href: "#",
color: "bg-red-400 dark:bg-red-300",
},
],
teams: [
{
id: "engineering",
title: "Engineering",
icon: Settings2Icon,
},
{
id: "marketing",
title: "Marketing",
icon: MegaphoneIcon,
},
],
topics: [
{
id: "product-updates",
title: "Product Updates",
icon: Package,
},
{
id: "company-news",
title: "Company News",
icon: NewspaperIcon,
},
],
},
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<NavHeader data={data} />
<SidebarContent>
<NavMain items={data.navMain} />
<NavCollapsible
favorites={data.navCollapsible.favorites}
teams={data.navCollapsible.teams}
topics={data.navCollapsible.topics}
/>
</SidebarContent>
<NavFooter user={data.user} />
</Sidebar>
)
}"use client"
import { ChevronDown } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import type { FavoriteItem, TeamItem, TopicItem } from "./types"
interface NavCollapsibleProps {
favorites: FavoriteItem[]
teams: TeamItem[]
topics: TopicItem[]
}
export function NavCollapsible({
favorites,
teams,
topics,
}: NavCollapsibleProps) {
return (
<div className="space-y-0">
{favorites && favorites.length > 0 && (
<Collapsible className="group/collapsible" defaultOpen>
<SidebarGroup>
<SidebarGroupLabel
className="text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
render={<CollapsibleTrigger />}
>
Favorites
<ChevronDown className="ml-auto transition-transform group-data-open/collapsible:rotate-180" />
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{favorites.map((item) => (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton
render={
<a
className="flex items-center gap-3"
href={item.href}
/>
}
>
<div className={`h-3 w-3 rounded ${item.color}`} />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
)}
{[
{ label: "Teams", items: teams },
{ label: "Topics", items: topics },
].map(
({ label, items }) =>
items &&
items.length > 0 && (
<Collapsible className="group/collapsible" key={label}>
<SidebarGroup>
<SidebarGroupLabel
className="text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
render={<CollapsibleTrigger />}
>
{label}
<ChevronDown className="ml-auto transition-transform group-data-open/collapsible:rotate-180" />
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => {
const Icon = item.icon
return (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton>
<Icon className="mr-2 h-4 w-4" />
{item.title}
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
)
)}
</div>
)
}"use client"
import {
BookmarkPlus,
CircleHelp,
LogOut,
Plus,
PlusCircle,
Puzzle,
Settings,
User,
} from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarFooter,
SidebarMenu,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
export function NavFooter({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
return (
<SidebarFooter className="p-4">
<SidebarMenu>
<SidebarMenuItem>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger
nativeButton={false}
render={<Avatar className="h-8 w-8 rounded-full" />}
>
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-full">CN</AvatarFallback>
</DropdownMenuTrigger>
<DropdownMenuContent className="m-2">
<DropdownMenuItem>
<User aria-hidden="true" className="opacity-80" size={16} />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<Settings
aria-hidden="true"
className="opacity-80"
size={16}
/>
Settings
</DropdownMenuItem>
<DropdownMenuItem>
<LogOut
aria-hidden="true"
className="opacity-80"
size={16}
/>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider delay={0}>
<Tooltip>
<TooltipTrigger
render={
<CircleHelp
aria-hidden="true"
className="cursor-pointer opacity-60 hover:opacity-100"
size={16}
/>
}
/>
<TooltipContent
className="m-2 max-w-[150px] border bg-popover px-2 py-1 text-popover-foreground"
side="top"
>
<div className="space-y-1 text-xs">
<p className="font-medium">User Information</p>
<p className="text-muted-foreground">
More details about the current user or section can be
displayed here.
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label="Open edit menu"
className="rounded-full shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
size="icon"
variant="ghost"
/>
}
>
<Plus aria-hidden="true" size={16} />
</DropdownMenuTrigger>
<DropdownMenuContent className="pb-2">
<DropdownMenuLabel>Add New</DropdownMenuLabel>
<DropdownMenuItem>
<PlusCircle
aria-hidden="true"
className="mr-2 opacity-80"
size={16}
/>
Add New Item
</DropdownMenuItem>
<DropdownMenuItem>
<BookmarkPlus
aria-hidden="true"
className="mr-2 opacity-80"
size={16}
/>
Add Bookmark
</DropdownMenuItem>
<DropdownMenuItem>
<Puzzle
aria-hidden="true"
className="mr-2 opacity-80"
size={16}
/>
Add Integration
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)
}"use client"
import { Search } from "lucide-react"
import * as React from "react"
import { useEffect } from "react"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import { SidebarHeader } from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import type { SidebarData } from "./types"
interface NavHeaderProps {
data: SidebarData
}
export function NavHeader({ data }: NavHeaderProps) {
const [open, setOpen] = React.useState(false)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<>
<SidebarHeader>
<div
className="flex cursor-pointer items-center justify-between px-2 pt-3 pb-0"
onClick={() => setOpen(true)}
>
<div className="flex flex-1 items-center gap-3">
<Search className="h-4 w-4 text-muted-foreground" />
<span className="font-normal text-muted-foreground text-sm">
Search
</span>
</div>
<div className="flex items-center justify-center rounded-md border border-border px-2 py-1">
<kbd className="inline-flex font-[inherit] font-medium text-muted-foreground text-xs">
<span className="opacity-70">⌘K</span>
</kbd>
</div>
</div>
</SidebarHeader>
<CommandDialog onOpenChange={setOpen} open={open}>
<Command>
<CommandInput placeholder="Search everything..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
{data.navMain.map((item) => (
<CommandItem
className="py-2!"
key={item.id}
onSelect={() => {
setOpen(false)
}}
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator className="my-2" />
<CommandGroup heading="Favorites">
{data.navCollapsible.favorites.map((item) => (
<CommandItem
className="py-2!"
key={item.id}
onSelect={() => {
setOpen(false)
}}
>
<div
className={cn("mr-2 h-3 w-3 rounded-full", item.color)}
/>
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator className="my-2" />
<CommandGroup heading="Teams">
{data.navCollapsible.teams.map((item) => (
<CommandItem
className="py-2!"
key={item.id}
onSelect={() => {
setOpen(false)
}}
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator className="my-2" />
<CommandGroup heading="Topics">
{data.navCollapsible.topics.map((item) => (
<CommandItem
className="py-2!"
key={item.id}
onSelect={() => {
setOpen(false)
}}
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>
</>
)
}"use client"
import {
SidebarGroup,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import type { NavItem } from "./types"
export function NavMain({ items }: { items: NavItem[] }) {
return (
<SidebarGroup>
<SidebarMenu>
{items.map((item) => {
const Icon = item.icon
return (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton tooltip={item.title}>
{Icon && <Icon className="mr-2 h-4 w-4" />}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroup>
)
}import type { ElementType } from "react"
export interface NavItem {
id: string
title: string
icon: ElementType
url?: string
isActive?: boolean
}
export interface User {
name: string
email: string
avatar: string
}
export interface FavoriteItem {
id: string
title: string
href: string
color: string
}
export interface TeamItem {
id: string
title: string
icon: ElementType
}
export interface TopicItem {
id: string
title: string
icon: ElementType
}
export interface SidebarData {
user: User
navMain: NavItem[]
navCollapsible: {
favorites: FavoriteItem[]
teams: TeamItem[]
topics: TopicItem[]
}
}npx shadcn@latest add https://blocks.so/r/sidebar-01.jsonSidebar Dashboard Inset
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { DashboardSidebar } from "./components/app-sidebar"
export default function Sidebar02() {
return (
<SidebarProvider>
<div className="relative flex h-dvh w-full">
<DashboardSidebar />
<SidebarInset className="flex flex-col" />
</div>
</SidebarProvider>
)
}"use client"
import { motion } from "motion/react"
import {
Activity,
DollarSign,
Home,
Infinity,
LinkIcon,
Package2,
Percent,
PieChart,
Settings,
ShoppingBag,
Sparkles,
Store,
TrendingUp,
Users,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import { Logo } from "./logo"
import type { Route } from "./nav-main"
import DashboardNavigation from "./nav-main"
import { NotificationsPopover } from "./nav-notifications"
import { TeamSwitcher } from "./team-switcher"
const sampleNotifications = [
{
id: "1",
avatar: "/avatars/01.png",
fallback: "OM",
text: "New order received.",
time: "10m ago",
},
{
id: "2",
avatar: "/avatars/02.png",
fallback: "JL",
text: "Server upgrade completed.",
time: "1h ago",
},
{
id: "3",
avatar: "/avatars/03.png",
fallback: "HH",
text: "New user signed up.",
time: "2h ago",
},
]
const dashboardRoutes: Route[] = [
{
id: "home",
title: "Home",
icon: <Home className="size-4" />,
link: "#",
},
{
id: "products",
title: "Products",
icon: <Package2 className="size-4" />,
link: "#",
subs: [
{
title: "Catalogue",
link: "#",
icon: <Package2 className="size-4" />,
},
{
title: "Checkout Links",
link: "#",
icon: <LinkIcon className="size-4" />,
},
{
title: "Discounts",
link: "#",
icon: <Percent className="size-4" />,
},
],
},
{
id: "usage-billing",
title: "Usage Billing",
icon: <PieChart className="size-4" />,
link: "#",
subs: [
{
title: "Meters",
link: "#",
icon: <PieChart className="size-4" />,
},
{
title: "Events",
link: "#",
icon: <Activity className="size-4" />,
},
],
},
{
id: "benefits",
title: "Benefits",
icon: <Sparkles className="size-4" />,
link: "#",
},
{
id: "customers",
title: "Customers",
icon: <Users className="size-4" />,
link: "#",
},
{
id: "sales",
title: "Sales",
icon: <ShoppingBag className="size-4" />,
link: "#",
subs: [
{
title: "Orders",
link: "#",
icon: <ShoppingBag className="size-4" />,
},
{
title: "Subscriptions",
link: "#",
icon: <Infinity className="size-4" />,
},
],
},
{
id: "storefront",
title: "Storefront",
icon: <Store className="size-4" />,
link: "#",
},
{
id: "analytics",
title: "Analytics",
icon: <TrendingUp className="size-4" />,
link: "#",
},
{
id: "finance",
title: "Finance",
icon: <DollarSign className="size-4" />,
link: "#",
subs: [
{ title: "Incoming", link: "#" },
{ title: "Outgoing", link: "#" },
{ title: "Payout Account", link: "#" },
],
},
{
id: "settings",
title: "Settings",
icon: <Settings className="size-4" />,
link: "#",
subs: [
{ title: "General", link: "#" },
{ title: "Webhooks", link: "#" },
{ title: "Custom Fields", link: "#" },
],
},
]
const teams = [
{ id: "1", name: "Alpha Inc.", logo: Logo, plan: "Free" },
{ id: "2", name: "Beta Corp.", logo: Logo, plan: "Free" },
{ id: "3", name: "Gamma Tech", logo: Logo, plan: "Free" },
]
export function DashboardSidebar() {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
return (
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader
className={cn(
"flex md:pt-3.5",
isCollapsed
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start"
: "flex-row items-center justify-between"
)}
>
<a className="flex items-center gap-2" href="#">
<Logo className="h-8 w-8" />
{!isCollapsed && (
<span className="font-semibold text-black dark:text-white">
Acme
</span>
)}
</a>
<motion.div
animate={{ opacity: 1 }}
className={cn(
"flex items-center gap-2",
isCollapsed ? "flex-row md:flex-col-reverse" : "flex-row"
)}
initial={{ opacity: 0 }}
key={isCollapsed ? "header-collapsed" : "header-expanded"}
transition={{ duration: 0.8 }}
>
<NotificationsPopover notifications={sampleNotifications} />
<SidebarTrigger />
</motion.div>
</SidebarHeader>
<SidebarContent className="gap-4 px-2 py-4">
<DashboardNavigation routes={dashboardRoutes} />
</SidebarContent>
<SidebarFooter className="px-2">
<TeamSwitcher teams={teams} />
</SidebarFooter>
</Sidebar>
)
}import type { SVGProps } from "react"
export const Logo = (props: SVGProps<SVGSVGElement>) => (
<svg
fill="currentColor"
height="48"
viewBox="0 0 40 48"
width="40"
{...props}
>
<clipPath id="a">
<path d="m0 0h40v48h-40z" />
</clipPath>
<g clipPath="url(#a)">
<path d="m25.0887 5.05386-3.933-1.05386-3.3145 12.3696-2.9923-11.16736-3.9331 1.05386 3.233 12.0655-8.05262-8.0526-2.87919 2.8792 8.83271 8.8328-10.99975-2.9474-1.05385625 3.933 12.01860625 3.2204c-.1376-.5935-.2104-1.2119-.2104-1.8473 0-4.4976 3.646-8.1436 8.1437-8.1436 4.4976 0 8.1436 3.646 8.1436 8.1436 0 .6313-.0719 1.2459-.2078 1.8359l10.9227 2.9267 1.0538-3.933-12.0664-3.2332 11.0005-2.9476-1.0539-3.933-12.0659 3.233 8.0526-8.0526-2.8792-2.87916-8.7102 8.71026z" />
<path d="m27.8723 26.2214c-.3372 1.4256-1.0491 2.7063-2.0259 3.7324l7.913 7.9131 2.8792-2.8792z" />
<path d="m25.7665 30.0366c-.9886 1.0097-2.2379 1.7632-3.6389 2.1515l2.8794 10.746 3.933-1.0539z" />
<path d="m21.9807 32.2274c-.65.1671-1.3313.2559-2.0334.2559-.7522 0-1.4806-.102-2.1721-.2929l-2.882 10.7558 3.933 1.0538z" />
<path d="m17.6361 32.1507c-1.3796-.4076-2.6067-1.1707-3.5751-2.1833l-7.9325 7.9325 2.87919 2.8792z" />
<path d="m13.9956 29.8973c-.9518-1.019-1.6451-2.2826-1.9751-3.6862l-10.95836 2.9363 1.05385 3.933z" />
</g>
</svg>
)"use client"
import { ChevronDown, ChevronUp } from "lucide-react"
import Link from "next/link"
import type React from "react"
import { useState } from "react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuItem as SidebarMenuSubItem,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
export type Route = {
id: string
title: string
icon?: React.ReactNode
link: string
subs?: {
title: string
link: string
icon?: React.ReactNode
}[]
}
export default function DashboardNavigation({ routes }: { routes: Route[] }) {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const [openCollapsible, setOpenCollapsible] = useState<string | null>(null)
return (
<SidebarMenu>
{routes.map((route) => {
const isOpen = !isCollapsed && openCollapsible === route.id
const hasSubRoutes = !!route.subs?.length
return (
<SidebarMenuItem key={route.id}>
{hasSubRoutes ? (
<Collapsible
className="w-full"
onOpenChange={(open) =>
setOpenCollapsible(open ? route.id : null)
}
open={isOpen}
>
<CollapsibleTrigger
render={
<SidebarMenuButton
className={cn(
"flex w-full items-center rounded-lg px-2 transition-colors",
isOpen
? "bg-sidebar-muted text-foreground"
: "text-muted-foreground hover:bg-sidebar-muted hover:text-foreground",
isCollapsed && "justify-center"
)}
/>
}
>
{route.icon}
{!isCollapsed && (
<span className="ml-2 flex-1 font-medium text-sm">
{route.title}
</span>
)}
{!isCollapsed && hasSubRoutes && (
<span className="ml-auto">
{isOpen ? (
<ChevronUp className="size-4" />
) : (
<ChevronDown className="size-4" />
)}
</span>
)}
</CollapsibleTrigger>
{!isCollapsed && (
<CollapsibleContent>
<SidebarMenuSub className="my-1 ml-3.5 ">
{route.subs?.map((subRoute) => (
<SidebarMenuSubItem
className="h-auto"
key={`${route.id}-${subRoute.title}`}
>
<SidebarMenuSubButton
render={
<Link
className="flex items-center rounded-md px-4 py-1.5 font-medium text-muted-foreground text-sm hover:bg-sidebar-muted hover:text-foreground"
href={subRoute.link}
prefetch={true}
/>
}
>
{subRoute.title}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
)}
</Collapsible>
) : (
<SidebarMenuButton
render={
<Link
className={cn(
"flex items-center rounded-lg px-2 text-muted-foreground transition-colors hover:bg-sidebar-muted hover:text-foreground",
isCollapsed && "justify-center"
)}
href={route.link}
prefetch={true}
/>
}
tooltip={route.title}
>
{route.icon}
{!isCollapsed && (
<span className="ml-2 font-medium text-sm">
{route.title}
</span>
)}
</SidebarMenuButton>
)}
</SidebarMenuItem>
)
})}
</SidebarMenu>
)
}"use client"
import { BellIcon } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
type Notification = {
id: string
avatar: string
fallback: string
text: string
time: string
}
export function NotificationsPopover({
notifications,
}: {
notifications: Notification[]
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label="Open notifications"
className="rounded-full"
size="icon"
variant="ghost"
/>
}
>
<BellIcon className="size-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="my-6 w-80" side="right">
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
{notifications.map(({ id, avatar, fallback, text, time }) => (
<DropdownMenuItem className="flex items-start gap-3" key={id}>
<Avatar className="size-8">
<AvatarImage alt="Avatar" src={avatar} />
<AvatarFallback>{fallback}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="font-medium text-sm">{text}</span>
<span className="text-muted-foreground text-xs">{time}</span>
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-center text-muted-foreground text-sm hover:text-primary">
View all notifications
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}"use client"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
type Team = {
name: string
logo: React.ElementType
plan: string
}
export function TeamSwitcher({ teams }: { teams: Team[] }) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) return null
const Logo = activeTeam.logo
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-background text-foreground">
<Logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="mb-4 w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add https://blocks.so/r/sidebar-02.jsonSidebar Dashboard Floating
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { DashboardSidebar } from "./components/app-sidebar"
export default function Sidebar03() {
return (
<SidebarProvider>
<div className="relative flex h-dvh w-full">
<DashboardSidebar />
<SidebarInset className="flex flex-col" />
</div>
</SidebarProvider>
)
}"use client"
import { motion } from "motion/react"
import {
Activity,
DollarSign,
Home,
Infinity,
LinkIcon,
Package2,
Percent,
PieChart,
Settings,
ShoppingBag,
Sparkles,
Store,
TrendingUp,
Users,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import { Logo } from "./logo"
import type { Route } from "./nav-main"
import DashboardNavigation from "./nav-main"
import { NotificationsPopover } from "./nav-notifications"
import { TeamSwitcher } from "./team-switcher"
const sampleNotifications = [
{
id: "1",
avatar: "/avatars/01.png",
fallback: "OM",
text: "New order received.",
time: "10m ago",
},
{
id: "2",
avatar: "/avatars/02.png",
fallback: "JL",
text: "Server upgrade completed.",
time: "1h ago",
},
{
id: "3",
avatar: "/avatars/03.png",
fallback: "HH",
text: "New user signed up.",
time: "2h ago",
},
]
const dashboardRoutes: Route[] = [
{
id: "home",
title: "Home",
icon: <Home className="size-4" />,
link: "#",
},
{
id: "products",
title: "Products",
icon: <Package2 className="size-4" />,
link: "#",
subs: [
{
title: "Catalogue",
link: "#",
icon: <Package2 className="size-4" />,
},
{
title: "Checkout Links",
link: "#",
icon: <LinkIcon className="size-4" />,
},
{
title: "Discounts",
link: "#",
icon: <Percent className="size-4" />,
},
],
},
{
id: "usage-billing",
title: "Usage Billing",
icon: <PieChart className="size-4" />,
link: "#",
subs: [
{
title: "Meters",
link: "#",
icon: <PieChart className="size-4" />,
},
{
title: "Events",
link: "#",
icon: <Activity className="size-4" />,
},
],
},
{
id: "benefits",
title: "Benefits",
icon: <Sparkles className="size-4" />,
link: "#",
},
{
id: "customers",
title: "Customers",
icon: <Users className="size-4" />,
link: "#",
},
{
id: "sales",
title: "Sales",
icon: <ShoppingBag className="size-4" />,
link: "#",
subs: [
{
title: "Orders",
link: "#",
icon: <ShoppingBag className="size-4" />,
},
{
title: "Subscriptions",
link: "#",
icon: <Infinity className="size-4" />,
},
],
},
{
id: "storefront",
title: "Storefront",
icon: <Store className="size-4" />,
link: "#",
},
{
id: "analytics",
title: "Analytics",
icon: <TrendingUp className="size-4" />,
link: "#",
},
{
id: "finance",
title: "Finance",
icon: <DollarSign className="size-4" />,
link: "#",
subs: [
{ title: "Incoming", link: "#" },
{ title: "Outgoing", link: "#" },
{ title: "Payout Account", link: "#" },
],
},
{
id: "settings",
title: "Settings",
icon: <Settings className="size-4" />,
link: "#",
subs: [
{ title: "General", link: "#" },
{ title: "Webhooks", link: "#" },
{ title: "Custom Fields", link: "#" },
],
},
]
const teams = [
{ id: "1", name: "Alpha Inc.", logo: Logo, plan: "Free" },
{ id: "2", name: "Beta Corp.", logo: Logo, plan: "Free" },
{ id: "3", name: "Gamma Tech", logo: Logo, plan: "Free" },
]
export function DashboardSidebar() {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
return (
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader
className={cn(
"flex md:pt-3.5",
isCollapsed
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start"
: "flex-row items-center justify-between"
)}
>
<a className="flex items-center gap-2" href="#">
<Logo className="h-8 w-8" />
{!isCollapsed && (
<span className="font-semibold text-black dark:text-white">
Acme
</span>
)}
</a>
<motion.div
animate={{ opacity: 1 }}
className={cn(
"flex items-center gap-2",
isCollapsed ? "flex-row md:flex-col-reverse" : "flex-row"
)}
initial={{ opacity: 0 }}
key={isCollapsed ? "header-collapsed" : "header-expanded"}
transition={{ duration: 0.8 }}
>
<NotificationsPopover notifications={sampleNotifications} />
<SidebarTrigger />
</motion.div>
</SidebarHeader>
<SidebarContent className="gap-4 px-2 py-4">
<DashboardNavigation routes={dashboardRoutes} />
</SidebarContent>
<SidebarFooter className="px-2">
<TeamSwitcher teams={teams} />
</SidebarFooter>
</Sidebar>
)
}import type { SVGProps } from "react"
export const Logo = (props: SVGProps<SVGSVGElement>) => (
<svg
fill="currentColor"
height="48"
viewBox="0 0 40 48"
width="40"
{...props}
>
<title>Logo</title>
<clipPath id="a">
<path d="m0 0h40v48h-40z" />
</clipPath>
<g clipPath="url(#a)">
<path d="m25.0887 5.05386-3.933-1.05386-3.3145 12.3696-2.9923-11.16736-3.9331 1.05386 3.233 12.0655-8.05262-8.0526-2.87919 2.8792 8.83271 8.8328-10.99975-2.9474-1.05385625 3.933 12.01860625 3.2204c-.1376-.5935-.2104-1.2119-.2104-1.8473 0-4.4976 3.646-8.1436 8.1437-8.1436 4.4976 0 8.1436 3.646 8.1436 8.1436 0 .6313-.0719 1.2459-.2078 1.8359l10.9227 2.9267 1.0538-3.933-12.0664-3.2332 11.0005-2.9476-1.0539-3.933-12.0659 3.233 8.0526-8.0526-2.8792-2.87916-8.7102 8.71026z" />
<path d="m27.8723 26.2214c-.3372 1.4256-1.0491 2.7063-2.0259 3.7324l7.913 7.9131 2.8792-2.8792z" />
<path d="m25.7665 30.0366c-.9886 1.0097-2.2379 1.7632-3.6389 2.1515l2.8794 10.746 3.933-1.0539z" />
<path d="m21.9807 32.2274c-.65.1671-1.3313.2559-2.0334.2559-.7522 0-1.4806-.102-2.1721-.2929l-2.882 10.7558 3.933 1.0538z" />
<path d="m17.6361 32.1507c-1.3796-.4076-2.6067-1.1707-3.5751-2.1833l-7.9325 7.9325 2.87919 2.8792z" />
<path d="m13.9956 29.8973c-.9518-1.019-1.6451-2.2826-1.9751-3.6862l-10.95836 2.9363 1.05385 3.933z" />
</g>
</svg>
)"use client"
import { ChevronDown, ChevronUp } from "lucide-react"
import Link from "next/link"
import type React from "react"
import { useState } from "react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuItem as SidebarMenuSubItem,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
export type Route = {
id: string
title: string
icon?: React.ReactNode
link: string
subs?: {
title: string
link: string
icon?: React.ReactNode
}[]
}
export default function DashboardNavigation({ routes }: { routes: Route[] }) {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const [openCollapsible, setOpenCollapsible] = useState<string | null>(null)
return (
<SidebarMenu>
{routes.map((route) => {
const isOpen = !isCollapsed && openCollapsible === route.id
const hasSubRoutes = !!route.subs?.length
return (
<SidebarMenuItem key={route.id}>
{hasSubRoutes ? (
<Collapsible
className="w-full"
onOpenChange={(open) =>
setOpenCollapsible(open ? route.id : null)
}
open={isOpen}
>
<CollapsibleTrigger
render={
<SidebarMenuButton
className={cn(
"flex w-full items-center rounded-lg px-2 transition-colors",
isOpen
? "bg-sidebar-muted text-foreground"
: "text-muted-foreground hover:bg-sidebar-muted hover:text-foreground",
isCollapsed && "justify-center"
)}
/>
}
>
{route.icon}
{!isCollapsed && (
<span className="ml-2 flex-1 font-medium text-sm">
{route.title}
</span>
)}
{!isCollapsed && hasSubRoutes && (
<span className="ml-auto">
{isOpen ? (
<ChevronUp className="size-4" />
) : (
<ChevronDown className="size-4" />
)}
</span>
)}
</CollapsibleTrigger>
{!isCollapsed && (
<CollapsibleContent>
<SidebarMenuSub className="my-1 ml-3.5 ">
{route.subs?.map((subRoute) => (
<SidebarMenuSubItem
className="h-auto"
key={`${route.id}-${subRoute.title}`}
>
<SidebarMenuSubButton
render={
<Link
className="flex items-center rounded-md px-4 py-1.5 font-medium text-muted-foreground text-sm hover:bg-sidebar-muted hover:text-foreground"
href={subRoute.link}
prefetch={true}
/>
}
>
{subRoute.title}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
)}
</Collapsible>
) : (
<SidebarMenuButton
render={
<Link
className={cn(
"flex items-center rounded-lg px-2 text-muted-foreground transition-colors hover:bg-sidebar-muted hover:text-foreground",
isCollapsed && "justify-center"
)}
href={route.link}
prefetch={true}
/>
}
tooltip={route.title}
>
{route.icon}
{!isCollapsed && (
<span className="ml-2 font-medium text-sm">
{route.title}
</span>
)}
</SidebarMenuButton>
)}
</SidebarMenuItem>
)
})}
</SidebarMenu>
)
}"use client"
import { BellIcon } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
type Notification = {
id: string
avatar: string
fallback: string
text: string
time: string
}
export function NotificationsPopover({
notifications,
}: {
notifications: Notification[]
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label="Open notifications"
className="rounded-full"
size="icon"
variant="ghost"
/>
}
>
<BellIcon className="size-5" />
</DropdownMenuTrigger>
<DropdownMenuContent className="my-6 w-80" side="right">
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
{notifications.map(({ id, avatar, fallback, text, time }) => (
<DropdownMenuItem className="flex items-start gap-3" key={id}>
<Avatar className="size-8">
<AvatarImage alt="Avatar" src={avatar} />
<AvatarFallback>{fallback}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="font-medium text-sm">{text}</span>
<span className="text-muted-foreground text-xs">{time}</span>
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-center text-muted-foreground text-sm hover:text-primary">
View all notifications
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}"use client"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
type Team = {
name: string
logo: React.ElementType
plan: string
}
export function TeamSwitcher({ teams }: { teams: Team[] }) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) return null
const Logo = activeTeam.logo
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-background text-foreground">
<Logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="mb-4 w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add https://blocks.so/r/sidebar-03.jsonSidebar Double-Sided Mail
"use client"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/app-sidebar"
import { MailProvider, useMail } from "./components/mail-context"
function MailPreview() {
const { selectedMail } = useMail()
if (!selectedMail) {
return (
<div className="flex h-full items-center justify-center text-muted-foreground text-sm">
Select an email to preview
</div>
)
}
return (
<div className="flex h-full flex-col">
<div className="border-b p-4">
<div className="font-semibold text-lg">{selectedMail.subject}</div>
<div className="text-muted-foreground text-sm">
From: {selectedMail.name} ({selectedMail.email})
</div>
</div>
<div className="whitespace-pre-wrap p-4 text-sm">{`${selectedMail.teaser}\n\nThis is a sample message body for the selected email.\nIt demonstrates the preview area on the right side.`}</div>
</div>
)
}
export default function Page() {
return (
<SidebarProvider>
<MailProvider>
<div className="flex h-dvh w-full">
<AppSidebar />
<SidebarInset>
<div className="m-2 flex-1 rounded-xl border">
<MailPreview />
</div>
</SidebarInset>
</div>
</MailProvider>
</SidebarProvider>
)
}"use client"
import { GhostIcon, StarIcon, HourglassIcon, InboxIcon } from "lucide-react"
import { Archive, Flag } from "lucide-react"
import * as React from "react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { useMail } from "./mail-context"
import { NavUser } from "./nav-user"
// This is sample data
const data = {
user: {
name: "ephraim",
email: "ephraim@blocks.so",
avatar: "/avatar-01.png",
},
navMain: [
{
title: "Inbox",
url: "#",
icon: InboxIcon,
isActive: true,
},
{
title: "Starred",
url: "#",
icon: StarIcon,
isActive: false,
},
{
title: "Important",
url: "#",
icon: Flag,
isActive: false,
},
{
title: "Scheduled",
url: "#",
icon: HourglassIcon,
isActive: false,
},
{
title: "Archive",
url: "#",
icon: Archive,
isActive: false,
},
],
labels: [
{ title: "Personal", color: "bg-green-400 dark:bg-green-300" },
{ title: "Work", color: "bg-blue-400 dark:bg-blue-300" },
{ title: "Travel", color: "bg-orange-400 dark:bg-orange-300" },
{ title: "Receipts", color: "bg-purple-400 dark:bg-purple-300" },
],
mails: [
{
name: "Nora Patel",
email: "nora@acme.co",
subject: "Welcome to Acme Mail",
date: "08:15 AM",
teaser:
"Hi there — here's a quick tour of your new inbox.\nPin, label, and schedule messages to stay organized.",
},
{
name: "Stripe",
email: "no-reply@stripe.com",
subject: "Your payout has arrived",
date: "Yesterday",
teaser:
"A payout of $3,245.90 was sent to your bank account.\nView details in your dashboard.",
},
{
name: "GitHub",
email: "noreply@github.com",
subject: "New activity on acme/app",
date: "Tue",
teaser:
"3 pull requests need your review. CI passed on main.\nClick to open the review queue.",
},
{
name: "Ava Chen",
email: "ava.chen@example.com",
subject: "Agenda for Friday standup",
date: "Mon",
teaser:
"Let's cover onboarding, billing bugs, and Q4 goals.\nReply with anything you want to add.",
},
{
name: "Figma",
email: "updates@figma.com",
subject: "What's new in Figma",
date: "Sep 12",
teaser:
"Variables, auto layout improvements, and dev mode updates.\nWatch the recap to learn more.",
},
{
name: "Linear",
email: "bot@linear.app",
subject: "[ACME-432] Edit modal broken",
date: "Sep 11",
teaser:
"Issue created by Wendy. Repro steps included.\nSeverity: high, priority: P1.",
},
{
name: "Airbnb",
email: "booking@airbnb.com",
subject: "Your trip to Kyoto",
date: "Sep 10",
teaser:
"Get ready for your stay. Check-in details and local recommendations inside.",
},
{
name: "Notion",
email: "team@notion.so",
subject: "Weekly team recap",
date: "Sep 09",
teaser:
"Marketing shipped pricing page revamp. Eng closed 14 issues.\nSee full notes in the doc.",
},
{
name: "Google Calendar",
email: "calendar@google.com",
subject: "Design sync moved",
date: "Sep 09",
teaser:
"Event 'Design sync' moved to 3:30 PM. Meet link updated in the invite.",
},
{
name: "HN Daily",
email: "digest@ycombinator.com",
subject: "Top stories today",
date: "Sep 08",
teaser:
"OpenAI releases new API, SQLite 3.46, and why we left Kubernetes.\nRead the full digest.",
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
// Note: I'm using state to show active item.
// IRL you should use the url/router.
const [activeItem, setActiveItem] = React.useState(data.navMain[0])
const [mails, setMails] = React.useState(data.mails)
const [query, setQuery] = React.useState("")
const { setOpen } = useSidebar()
const { setSelectedMail } = useMail()
const filteredMails = React.useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return mails
return mails.filter((m) =>
[m.name, m.email, m.subject, m.teaser]
.join("\n")
.toLowerCase()
.includes(q)
)
}, [mails, query])
return (
<div className="flex">
{/* This is the first sidebar */}
<Sidebar
className="border-r p-2 px-1"
collapsible="none"
style={{ "--sidebar-width": "12rem" } as React.CSSProperties}
>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
className="md:h-8 md:p-0"
render={<a href="#" />}
size="lg"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
<GhostIcon className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Acme Inc</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent className="px-1.5 md:px-0">
<SidebarMenu>
{data.navMain.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
className="px-2.5 md:px-2"
isActive={activeItem?.title === item.title}
onClick={() => {
setActiveItem(item)
const mail = data.mails.sort(() => Math.random() - 0.5)
setMails(
mail.slice(
0,
Math.max(5, Math.floor(Math.random() * 10) + 1)
)
)
setOpen(true)
}}
>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel className="px-1.5 text-xs md:px-0">
Labels
</SidebarGroupLabel>
<SidebarGroupContent className="px-1.5 md:px-0">
<SidebarMenu>
{data.labels.map((label) => (
<SidebarMenuItem key={label.title}>
<SidebarMenuButton
className="px-2.5 md:px-2"
render={<div className="flex items-center gap-3" />}
>
<div className={`h-3 w-3 rounded ${label.color}`} />
<span>{label.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
{/* This is the second sidebar */}
{/* We disable collapsible and let it fill remaining space */}
<Sidebar
className="hidden min-w-96 flex-1 border-r md:flex"
collapsible="none"
>
<SidebarHeader className="gap-3.5 border-b p-4">
<div className="flex w-full items-center justify-between">
<div className="font-medium text-base text-foreground">
{activeItem?.title}
</div>
</div>
<SidebarInput
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
value={query}
/>
</SidebarHeader>
<SidebarContent>
<SidebarGroup className="px-0 pt-0">
<SidebarGroupContent>
{filteredMails.length === 0 && (
<div className="p-4 text-muted-foreground text-sm">
No results
</div>
)}
{filteredMails.map((mail) => (
<a
className="flex flex-col items-start gap-2 whitespace-nowrap border-b p-4 text-sm leading-tight hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
href="#"
key={mail.email}
onClick={(e) => {
e.preventDefault()
setSelectedMail(mail)
}}
>
<div className="flex w-full items-center gap-2">
<span>{mail.name}</span>{" "}
<span className="ml-auto text-xs">{mail.date}</span>
</div>
<span className="font-medium">{mail.subject}</span>
<span className="line-clamp-2 w-[260px] whitespace-break-spaces text-xs">
{mail.teaser}
</span>
</a>
))}
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</div>
)
}"use client"
import * as React from "react"
export type Mail = {
name: string
email: string
subject: string
date: string
teaser: string
labels?: string[]
}
type MailContextValue = {
selectedMail: Mail | null
setSelectedMail: (mail: Mail | null) => void
}
const MailContext = React.createContext<MailContextValue | undefined>(undefined)
export function MailProvider({ children }: { children: React.ReactNode }) {
const [selectedMail, setSelectedMail] = React.useState<Mail | null>(null)
const value = React.useMemo(
() => ({ selectedMail, setSelectedMail }),
[selectedMail]
)
return <MailContext.Provider value={value}>{children}</MailContext.Provider>
}
export function useMail() {
const ctx = React.useContext(MailContext)
if (!ctx) throw new Error("useMail must be used within MailProvider")
return ctx
}"use client"
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground md:h-8 md:p-0"
size="lg"
/>
}
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add https://blocks.so/r/sidebar-04.jsonSidebar Double-Sided
"use client"
import { SidebarProvider } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/app-sidebar"
export default function Page() {
return (
<SidebarProvider>
<div className="flex h-dvh w-full">
<AppSidebar />
</div>
</SidebarProvider>
)
}"use client"
import {
ActivityIcon,
ArchiveIcon,
LayersIcon,
BellRingIcon,
BugIcon,
ChartColumnIcon,
ChevronRightIcon,
CloudIcon,
DatabaseIcon,
FileTextIcon,
FolderIcon,
FoldersIcon,
GitCommitHorizontalIcon,
GitMergeIcon,
GitPullRequestIcon,
HouseIcon,
KeyIcon,
LockIcon,
LockKeyholeIcon,
LogOutIcon,
SparkleIcon,
PackageOpenIcon,
BoxesIcon,
FingerprintPatternIcon,
PlayIcon,
ScanEyeIcon,
SettingsIcon,
ShieldCheckIcon,
StarIcon,
TargetIcon,
SquareTerminalIcon,
UserIcon,
UserPlusIcon,
WebhookIcon,
XIcon,
} from "lucide-react"
import type React from "react"
import { useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import { TeamSwitcher } from "./team-switcher"
function OpenAILogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M11.217 19.384a3.501 3.501 0 0 0 6.783 -1.217v-5.167l-6 -3.35" />
<path d="M5.214 15.014a3.501 3.501 0 0 0 4.446 5.266l4.34 -2.534v-6.946" />
<path d="M6 7.63c-1.391 -.236 -2.787 .395 -3.534 1.689a3.474 3.474 0 0 0 1.271 4.745l4.263 2.514l6 -3.348" />
<path d="M12.783 4.616a3.501 3.501 0 0 0 -6.783 1.217v5.067l6 3.45" />
<path d="M18.786 8.986a3.501 3.501 0 0 0 -4.446 -5.266l-4.34 2.534v6.946" />
<path d="M18 16.302c1.391 .236 2.787 -.395 3.534 -1.689a3.474 3.474 0 0 0 -1.271 -4.745l-4.308 -2.514l-5.955 3.42" />
</svg>
)
}
function GoogleLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M20.945 11a9 9 0 1 1 -3.284 -5.997l-2.655 2.392a5.5 5.5 0 1 0 2.119 6.605h-4.125v-3h7.945" />
</svg>
)
}
function MetaLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M12 10.174c1.766 -2.784 3.315 -4.174 4.648 -4.174c2 0 3.263 2.213 4 5.217c.704 2.869 .5 6.783 -2 6.783c-1.114 0 -2.648 -1.565 -4.148 -3.652a27.627 27.627 0 0 1 -2.5 -4.174" />
<path d="M12 10.174c-1.766 -2.784 -3.315 -4.174 -4.648 -4.174c-2 0 -3.263 2.213 -4 5.217c-.704 2.869 -.5 6.783 2 6.783c1.114 0 2.648 -1.565 4.148 -3.652c1 -1.391 1.833 -2.783 2.5 -4.174" />
</svg>
)
}
function NpmLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M1 8h22v7h-12v2h-4v-2h-6l0 -7" />
<path d="M7 8v7" />
<path d="M14 8v7" />
<path d="M17 11v4" />
<path d="M4 11v4" />
<path d="M11 11v1" />
<path d="M20 11v4" />
</svg>
)
}
const data = {
teams: [
{
name: "OpenAI",
logo: OpenAILogo,
plan: "Enterprise",
},
{
name: "Anthropic",
logo: SparkleIcon,
plan: "Pro",
},
{
name: "Google",
logo: GoogleLogo,
plan: "Free",
},
{
name: "Meta",
logo: MetaLogo,
plan: "Free",
},
],
}
interface SidebarItem {
id: string
label: string
icon: React.ComponentType<{ className?: string }>
badge?: string
route?: string
subItems?: {
id: string
label: string
icon: React.ComponentType<{ className?: string }>
description?: string
route?: string
}[]
}
const sidebarItems: SidebarItem[] = [
{
id: "overview",
label: "Overview",
icon: HouseIcon,
subItems: [
{
id: "dashboard",
label: "Dashboard",
icon: ChartColumnIcon,
description: "Project overview and activity",
route: "/overview/dashboard",
},
{
id: "activity",
label: "Activity",
icon: ActivityIcon,
description: "Recent commits and changes",
route: "/overview/activity",
},
{
id: "insights",
label: "Insights",
icon: TargetIcon,
description: "Code analytics and metrics",
route: "/overview/insights",
},
],
},
{
id: "repositories",
label: "Repositories",
icon: FoldersIcon,
badge: "12",
subItems: [
{
id: "all-repos",
label: "All Repositories",
icon: FolderIcon,
description: "Browse all your repositories",
route: "/repositories",
},
{
id: "starred",
label: "Starred",
icon: StarIcon,
description: "Your starred repositories",
route: "/repositories/starred",
},
{
id: "archived",
label: "Archived",
icon: ArchiveIcon,
description: "Archived repositories",
route: "/repositories/archived",
},
],
},
{
id: "pull-requests",
label: "Pull Requests",
icon: GitPullRequestIcon,
badge: "3",
subItems: [
{
id: "open-prs",
label: "Open",
icon: GitPullRequestIcon,
description: "Open pull requests",
route: "/pull-requests/open",
},
{
id: "review-requests",
label: "Review Requests",
icon: ScanEyeIcon,
description: "PRs awaiting your review",
route: "/pull-requests/review",
},
{
id: "merged",
label: "Merged",
icon: GitMergeIcon,
description: "Recently merged PRs",
route: "/pull-requests/merged",
},
],
},
{
id: "issues",
label: "Issues",
icon: BugIcon,
badge: "7",
subItems: [
{
id: "open-issues",
label: "Open Issues",
icon: BugIcon,
description: "Active issues and bugs",
route: "/issues/open",
},
{
id: "assigned",
label: "Assigned to Me",
icon: UserPlusIcon,
description: "Issues assigned to you",
route: "/issues/assigned",
},
{
id: "created",
label: "Created by Me",
icon: GitCommitHorizontalIcon,
description: "Issues you've created",
route: "/issues/created",
},
],
},
{
id: "actions",
label: "Actions",
icon: LayersIcon,
subItems: [
{
id: "workflows",
label: "Workflows",
icon: PlayIcon,
description: "CI/CD workflows and pipelines",
route: "/actions/workflows",
},
{
id: "runners",
label: "Runners",
icon: SquareTerminalIcon,
description: "Self-hosted runners",
route: "/actions/runners",
},
{
id: "deployments",
label: "Deployments",
icon: CloudIcon,
description: "Deployment history",
route: "/actions/deployments",
},
],
},
{
id: "packages",
label: "Packages",
icon: BoxesIcon,
subItems: [
{
id: "published",
label: "Published",
icon: PackageOpenIcon,
description: "Your published packages",
route: "/packages/published",
},
{
id: "container-registry",
label: "Container Registry",
icon: DatabaseIcon,
description: "Docker images and containers",
route: "/packages/containers",
},
{
id: "npm-packages",
label: "npm Packages",
icon: NpmLogo,
description: "Node.js packages",
route: "/packages/npm",
},
],
},
{
id: "security",
label: "Security",
icon: LockKeyholeIcon,
badge: "2",
subItems: [
{
id: "alerts",
label: "Security Alerts",
icon: LockIcon,
description: "Vulnerability alerts",
route: "/security/alerts",
},
{
id: "advisories",
label: "Advisories",
icon: ShieldCheckIcon,
description: "Security advisories",
route: "/security/advisories",
},
{
id: "secrets",
label: "Secrets",
icon: FingerprintPatternIcon,
description: "Repository secrets",
route: "/security/secrets",
},
],
},
{
id: "settings",
label: "Settings",
icon: SettingsIcon,
subItems: [
{
id: "profile",
label: "Profile",
icon: UserIcon,
description: "Your profile settings",
route: "/settings/profile",
},
{
id: "notifications",
label: "Notifications",
icon: BellRingIcon,
description: "Notification preferences",
route: "/settings/notifications",
},
{
id: "webhooks",
label: "Webhooks",
icon: WebhookIcon,
description: "Webhook configurations",
route: "/settings/webhooks",
},
{
id: "api-keys",
label: "API Keys",
icon: KeyIcon,
description: "Personal access tokens",
route: "/settings/api-keys",
},
],
},
{
id: "docs",
label: "Documentation",
icon: FileTextIcon,
route: "/docs",
},
]
export function AppSidebar() {
const [activeItem, setActiveItem] = useState<string | null>("overview")
const [selectedSubItem, setSelectedSubItem] = useState<string | null>(null)
const activeItemData = sidebarItems.find((item) => item.id === activeItem)
const handleItemClick = (item: SidebarItem) => {
if (item.subItems) {
const isActive = activeItem === item.id
setActiveItem(isActive ? null : item.id)
if (isActive) {
setSelectedSubItem(null)
}
} else {
if (activeItem) {
setActiveItem(null)
setSelectedSubItem(null)
}
console.log(`[v0] Navigating to: ${item.route}`)
}
}
const handleSubItemClick = (subItem: { id: string; route?: string }) => {
setSelectedSubItem(selectedSubItem === subItem.id ? null : subItem.id)
if (subItem.route) {
console.log(`[v0] Navigating to: ${subItem.route}`)
}
}
return (
<div className="flex h-dvh bg-background">
<Sidebar
className="w-64 border-r"
collapsible="none"
side="left"
variant="sidebar"
>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{sidebarItems.map((item) => {
const Icon = item.icon
const isActive = activeItem === item.id
const chevronIndicator = (
<ChevronRightIcon
className={cn(
"h-4 w-4 shrink-0 transition-transform",
isActive && "rotate-90"
)}
/>
)
return (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton
className="h-10 w-full px-3"
isActive={isActive}
onClick={() => handleItemClick(item)}
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.label}</span>
</div>
<div className="ml-auto flex min-w-fit shrink-0 items-center gap-1">
{(item.badge || item.subItems) &&
(item.badge ? (
<SidebarMenuBadge
className={cn(
"min-w-fit",
item.subItems && "gap-x-3"
)}
>
{item.badge}
{item.subItems && chevronIndicator}
</SidebarMenuBadge>
) : (
chevronIndicator
))}
</div>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton className="h-12 w-full px-3">
<div className="flex min-w-0 flex-1 items-center gap-3">
<Avatar className="h-8 w-8 rounded-full">
<AvatarImage alt="ephraim" src="/avatar-01.png" />
<AvatarFallback className="rounded-full">E</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1 text-left">
<div className="truncate font-medium text-sm">ephraim</div>
<div className="truncate text-muted-foreground text-xs">
ephraim@blocks.so
</div>
</div>
</div>
<LogOutIcon className="h-4 w-4 shrink-0" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
{activeItem && activeItemData?.subItems && (
<Sidebar
className="slide-in-from-left-5 w-72 animate-in border-r duration-200"
collapsible="none"
side="left"
variant="sidebar"
>
<SidebarHeader className="flex flex-row items-center justify-between border-b px-4">
<h3 className="font-medium">{activeItemData.label}</h3>
<button
className="flex h-6 w-6 items-center justify-center rounded-md p-0 hover:bg-sidebar-accent"
onClick={() => setActiveItem(null)}
>
<XIcon className="h-4 w-4" />
</button>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{activeItemData.subItems.map((subItem) => {
const SubIcon = subItem.icon
const isSelected = selectedSubItem === subItem.id
return (
<SidebarMenuItem key={subItem.id}>
<SidebarMenuButton
className="h-auto w-full justify-start gap-3 px-3 py-2"
isActive={isSelected}
onClick={() => handleSubItemClick(subItem)}
>
<SubIcon className="mt-0.5 h-5 w-5 shrink-0 self-start" />
<div className="min-w-0 flex-1 text-left">
<div className="font-medium">{subItem.label}</div>
{subItem.description && (
<div className="mt-0.5 text-muted-foreground text-xs">
{subItem.description}
</div>
)}
</div>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
)}
</div>
)
}"use client"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) {
return null
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add https://blocks.so/r/sidebar-05.jsonSidebar Replacement
"use client"
import { SidebarProvider } from "@/components/ui/sidebar"
import { AppSidebar } from "./components/app-sidebar"
export default function Page() {
return (
<SidebarProvider>
<div className="flex h-dvh w-full">
<AppSidebar />
</div>
</SidebarProvider>
)
}"use client"
import {
ActivityIcon,
ArchiveIcon,
ArrowLeftIcon,
LayersIcon,
BellRingIcon,
BugIcon,
ChartColumnIcon,
ChevronRightIcon,
CloudIcon,
DatabaseIcon,
FileTextIcon,
FolderIcon,
FoldersIcon,
GitCommitHorizontalIcon,
GitMergeIcon,
GitPullRequestIcon,
HouseIcon,
KeyIcon,
LockIcon,
LockKeyholeIcon,
LogOutIcon,
SparkleIcon,
PackageOpenIcon,
BoxesIcon,
FingerprintPatternIcon,
PlayIcon,
ScanEyeIcon,
SettingsIcon,
ShieldCheckIcon,
StarIcon,
TargetIcon,
SquareTerminalIcon,
UserIcon,
UserPlusIcon,
WebhookIcon,
} from "lucide-react"
import type React from "react"
import { useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import { TeamSwitcher } from "./team-switcher"
function OpenAILogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M11.217 19.384a3.501 3.501 0 0 0 6.783 -1.217v-5.167l-6 -3.35" />
<path d="M5.214 15.014a3.501 3.501 0 0 0 4.446 5.266l4.34 -2.534v-6.946" />
<path d="M6 7.63c-1.391 -.236 -2.787 .395 -3.534 1.689a3.474 3.474 0 0 0 1.271 4.745l4.263 2.514l6 -3.348" />
<path d="M12.783 4.616a3.501 3.501 0 0 0 -6.783 1.217v5.067l6 3.45" />
<path d="M18.786 8.986a3.501 3.501 0 0 0 -4.446 -5.266l-4.34 2.534v6.946" />
<path d="M18 16.302c1.391 .236 2.787 -.395 3.534 -1.689a3.474 3.474 0 0 0 -1.271 -4.745l-4.308 -2.514l-5.955 3.42" />
</svg>
)
}
function GoogleLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M20.945 11a9 9 0 1 1 -3.284 -5.997l-2.655 2.392a5.5 5.5 0 1 0 2.119 6.605h-4.125v-3h7.945" />
</svg>
)
}
function MetaLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M12 10.174c1.766 -2.784 3.315 -4.174 4.648 -4.174c2 0 3.263 2.213 4 5.217c.704 2.869 .5 6.783 -2 6.783c-1.114 0 -2.648 -1.565 -4.148 -3.652a27.627 27.627 0 0 1 -2.5 -4.174" />
<path d="M12 10.174c-1.766 -2.784 -3.315 -4.174 -4.648 -4.174c-2 0 -3.263 2.213 -4 5.217c-.704 2.869 -.5 6.783 2 6.783c1.114 0 2.648 -1.565 4.148 -3.652c1 -1.391 1.833 -2.783 2.5 -4.174" />
</svg>
)
}
function NpmLogo(props: React.ComponentProps<"svg">) {
return (
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M1 8h22v7h-12v2h-4v-2h-6l0 -7" />
<path d="M7 8v7" />
<path d="M14 8v7" />
<path d="M17 11v4" />
<path d="M4 11v4" />
<path d="M11 11v1" />
<path d="M20 11v4" />
</svg>
)
}
const data = {
teams: [
{
name: "OpenAI",
logo: OpenAILogo,
plan: "Enterprise",
},
{
name: "Anthropic",
logo: SparkleIcon,
plan: "Pro",
},
{
name: "Google",
logo: GoogleLogo,
plan: "Free",
},
{
name: "Meta",
logo: MetaLogo,
plan: "Free",
},
],
}
interface SidebarItem {
id: string
label: string
icon: React.ComponentType<{ className?: string }>
badge?: string
route?: string
subItems?: {
id: string
label: string
icon: React.ComponentType<{ className?: string }>
route?: string
}[]
}
const sidebarItems: SidebarItem[] = [
{
id: "overview",
label: "Overview",
icon: HouseIcon,
subItems: [
{
id: "dashboard",
label: "Dashboard",
icon: ChartColumnIcon,
route: "/overview/dashboard",
},
{
id: "activity",
label: "Activity",
icon: ActivityIcon,
route: "/overview/activity",
},
{
id: "insights",
label: "Insights",
icon: TargetIcon,
route: "/overview/insights",
},
],
},
{
id: "repositories",
label: "Repositories",
icon: FoldersIcon,
badge: "12",
subItems: [
{
id: "all-repos",
label: "All Repositories",
icon: FolderIcon,
route: "/repositories",
},
{
id: "starred",
label: "Starred",
icon: StarIcon,
route: "/repositories/starred",
},
{
id: "archived",
label: "Archived",
icon: ArchiveIcon,
route: "/repositories/archived",
},
],
},
{
id: "pull-requests",
label: "Pull Requests",
icon: GitPullRequestIcon,
badge: "3",
subItems: [
{
id: "open-prs",
label: "Open",
icon: GitPullRequestIcon,
route: "/pull-requests/open",
},
{
id: "review-requests",
label: "Review Requests",
icon: ScanEyeIcon,
route: "/pull-requests/review",
},
{
id: "merged",
label: "Merged",
icon: GitMergeIcon,
route: "/pull-requests/merged",
},
],
},
{
id: "issues",
label: "Issues",
icon: BugIcon,
badge: "7",
subItems: [
{
id: "open-issues",
label: "Open Issues",
icon: BugIcon,
route: "/issues/open",
},
{
id: "assigned",
label: "Assigned to Me",
icon: UserPlusIcon,
route: "/issues/assigned",
},
{
id: "created",
label: "Created by Me",
icon: GitCommitHorizontalIcon,
route: "/issues/created",
},
],
},
{
id: "actions",
label: "Actions",
icon: LayersIcon,
subItems: [
{
id: "workflows",
label: "Workflows",
icon: PlayIcon,
route: "/actions/workflows",
},
{
id: "runners",
label: "Runners",
icon: SquareTerminalIcon,
route: "/actions/runners",
},
{
id: "deployments",
label: "Deployments",
icon: CloudIcon,
route: "/actions/deployments",
},
],
},
{
id: "packages",
label: "Packages",
icon: BoxesIcon,
subItems: [
{
id: "published",
label: "Published",
icon: PackageOpenIcon,
route: "/packages/published",
},
{
id: "container-registry",
label: "Container Registry",
icon: DatabaseIcon,
route: "/packages/containers",
},
{
id: "npm-packages",
label: "npm Packages",
icon: NpmLogo,
route: "/packages/npm",
},
],
},
{
id: "security",
label: "Security",
icon: LockKeyholeIcon,
badge: "2",
subItems: [
{
id: "alerts",
label: "Security Alerts",
icon: LockIcon,
route: "/security/alerts",
},
{
id: "advisories",
label: "Advisories",
icon: ShieldCheckIcon,
route: "/security/advisories",
},
{
id: "secrets",
label: "Secrets",
icon: FingerprintPatternIcon,
route: "/security/secrets",
},
],
},
{
id: "settings",
label: "Settings",
icon: SettingsIcon,
subItems: [
{
id: "profile",
label: "Profile",
icon: UserIcon,
route: "/settings/profile",
},
{
id: "notifications",
label: "Notifications",
icon: BellRingIcon,
route: "/settings/notifications",
},
{
id: "webhooks",
label: "Webhooks",
icon: WebhookIcon,
route: "/settings/webhooks",
},
{
id: "api-keys",
label: "API Keys",
icon: KeyIcon,
route: "/settings/api-keys",
},
],
},
{
id: "docs",
label: "Documentation",
icon: FileTextIcon,
route: "/docs",
},
]
export function AppSidebar() {
const [activeItem, setActiveItem] = useState<string | null>(null)
const [selectedSubItem, setSelectedSubItem] = useState<string | null>(null)
const activeItemData = sidebarItems.find((item) => item.id === activeItem)
const handleItemClick = (item: SidebarItem) => {
if (item.subItems) {
setActiveItem(item.id)
setSelectedSubItem(null)
} else {
console.log(`[v0] Navigating to: ${item.route}`)
}
}
const handleSubItemClick = (subItem: { id: string; route?: string }) => {
setSelectedSubItem(selectedSubItem === subItem.id ? null : subItem.id)
if (subItem.route) {
console.log(`[v0] Navigating to: ${subItem.route}`)
}
}
const handleBackToMain = () => {
setActiveItem(null)
setSelectedSubItem(null)
}
return (
<div className="flex h-dvh bg-background">
<Sidebar
className="w-64 border-r"
collapsible="none"
side="left"
variant="sidebar"
>
{activeItem ? (
activeItemData?.subItems && (
<>
<SidebarHeader className="flex flex-row items-center justify-between border-b px-4">
<button
className="flex h-8 w-8 items-center justify-center rounded-md p-0 hover:bg-sidebar-accent"
onClick={handleBackToMain}
>
<ArrowLeftIcon className="h-4 w-4" />
</button>
<h3 className="flex-1 text-center font-medium">
{activeItemData.label}
</h3>
<div className="w-8" />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{activeItemData.subItems.map((subItem) => {
const SubIcon = subItem.icon
const isSelected = selectedSubItem === subItem.id
return (
<SidebarMenuItem key={subItem.id}>
<SidebarMenuButton
className="h-10 w-full px-3"
isActive={isSelected}
onClick={() => handleSubItemClick(subItem)}
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<SubIcon className="h-4 w-4 shrink-0" />
<span className="truncate">
{subItem.label}
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</>
)
) : (
<>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{sidebarItems.map((item) => {
const Icon = item.icon
const chevronIndicator = (
<ChevronRightIcon className="h-4 w-4 shrink-0 transition-transform" />
)
return (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton
className="h-10 w-full px-3"
onClick={() => handleItemClick(item)}
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.label}</span>
</div>
<div className="ml-auto flex min-w-fit shrink-0 items-center gap-1">
{(item.badge || item.subItems) &&
(item.badge ? (
<SidebarMenuBadge
className={cn(
"min-w-fit",
item.subItems && "gap-x-3"
)}
>
{item.badge}
{item.subItems && chevronIndicator}
</SidebarMenuBadge>
) : (
chevronIndicator
))}
</div>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton className="h-12 w-full px-3">
<div className="flex min-w-0 flex-1 items-center gap-3">
<Avatar className="h-8 w-8 rounded-full">
<AvatarImage alt="ephraim" src="/avatar-01.png" />
<AvatarFallback className="rounded-full">
E
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1 text-left">
<div className="truncate font-medium text-sm">
ephraim
</div>
<div className="truncate text-muted-foreground text-xs">
ephraim@blocks.so
</div>
</div>
</div>
<LogOutIcon className="h-4 w-4 shrink-0" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</>
)}
</Sidebar>
</div>
)
}"use client"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
if (!activeTeam) {
return null
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
/>
}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}npx shadcn@latest add https://blocks.so/r/sidebar-06.json