
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Full-featured app sidebar -- collapsible="icon" rail with TeamSwitcher header, primary nav, projects section, secondary nav, and user dropdown footer. Each section ships as a sibling file in the same folder so consumers can edit one piece at a time.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/sidebar-02.json$npx shadcn@latest add https://uipkge.dev/r/react/sidebar-02.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/sidebar-02.json$bunx shadcn@latest add https://uipkge.dev/r/react/sidebar-02.jsonnpx shadcn@latest add @uipkge-react/sidebar-02Installs to:components/blocks/sidebar-02/Type aliases exported from this item's source. Use these to shape the data you pass in.
Teamtype Team {
name: string
logo: LucideIcon | ((props: { className?: string }) => React.ReactElement)
plan: string
}'use client'
import { BookOpen, Bot, Frame, LifeBuoy, Map, PieChart, Send, Settings2, SquareTerminal } from 'lucide-react'
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from '@/components/ui/sidebar'
import { NavMain } from './NavMain'
import { NavProjects } from './NavProjects'
import { NavSecondary } from './NavSecondary'
import { NavUser } from './NavUser'
import { TeamSwitcher } from './TeamSwitcher'
const data = {
user: {
name: 'shadcn',
email: '[email protected]',
},
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{ title: 'History', url: '#' },
{ title: 'Starred', url: '#' },
{ title: 'Settings', url: '#' },
],
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{ title: 'Genesis', url: '#' },
{ title: 'Explorer', url: '#' },
{ title: 'Quantum', url: '#' },
],
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{ title: 'Introduction', url: '#' },
{ title: 'Get Started', url: '#' },
{ title: 'Tutorials', url: '#' },
{ title: 'Changelog', url: '#' },
],
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{ title: 'General', url: '#' },
{ title: 'Team', url: '#' },
{ title: 'Billing', url: '#' },
{ title: 'Limits', url: '#' },
],
},
],
navSecondary: [
{ title: 'Support', url: '#', icon: LifeBuoy },
{ title: 'Feedback', url: '#', icon: Send },
],
projects: [
{ name: 'Design Engineering', url: '#', icon: Frame },
{ name: 'Sales & Marketing', url: '#', icon: PieChart },
{ name: 'Travel', url: '#', icon: Map },
],
}
export function Sidebar02() {
return (
<Sidebar data-slot="sidebar-02" collapsible="icon">
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}
'use client'
// Edit teams + activeTeam below to match your tenant model. The dropdown
// is the full team switcher pattern -- avatar tile, label, kbd shortcut,
// and a "Add team" footer row. Wire setActive() to your tenant API.
import * as React from 'react'
import { AudioWaveform, Check, ChevronsUpDown, Command, Plus, type LucideIcon } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
// 4-quadrant uipkge logo. Inline component so it can sit next to Lucide
// icons in the teams array without an extra file.
const UipkgeMark = ({ className }: { className?: string }) => (
<svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true">
<rect x="2" y="2" width="9" height="9" rx="1.5" />
<rect x="13" y="2" width="9" height="9" rx="1.5" opacity="0.55" />
<rect x="2" y="13" width="9" height="9" rx="1.5" opacity="0.55" />
<rect x="13" y="13" width="9" height="9" rx="1.5" />
</svg>
)
type Team = {
name: string
logo: LucideIcon | ((props: { className?: string }) => React.ReactElement)
plan: string
}
const teams: Team[] = [
{ name: 'uipkge', logo: UipkgeMark, plan: 'UI registry' },
{ name: 'uipkge HRMS', logo: AudioWaveform, plan: 'Vertical' },
{ name: 'uipkge Hospital', logo: Command, plan: 'Vertical' },
]
export function TeamSwitcher() {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState<Team>(teams[0]!)
const ActiveLogo = activeTeam.logo
return (
<SidebarMenu data-slot="sidebar-02">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg group-data-[collapsible=icon]:size-6">
<ActiveLogo className="size-4 group-data-[collapsible=icon]:size-3.5" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="font-display truncate font-bold">{activeTeam.name}</span>
<span className="text-muted-foreground truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? 'bottom' : 'right'}
align="start"
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
{teams.map((team, i) => {
const Logo = team.logo
return (
<DropdownMenuItem key={team.name} className="gap-2 p-2" onSelect={() => setActiveTeam(team)}>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Logo className="size-3.5 shrink-0" />
</div>
{team.name}
{activeTeam === team ? (
<Check className="ml-auto size-4" />
) : (
<DropdownMenuShortcut>⌘{i + 1}</DropdownMenuShortcut>
)}
</DropdownMenuItem>
)
})}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
'use client'
import { ChevronRight, type LucideIcon } from 'lucide-react'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from '@/components/ui/sidebar'
export interface NavMainProps {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
isActive?: boolean
}[]
}[]
}
export function NavMain({ items }: NavMainProps) {
return (
<SidebarGroup data-slot="sidebar-02">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title} isActive={item.isActive}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild isActive={subItem.isActive}>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}
'use client'
import { Folder, Forward, MoreHorizontal, Trash2, type LucideIcon } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
export interface NavProjectsProps {
projects: {
name: string
url: string
icon: LucideIcon
}[]
}
export function NavProjects({ projects }: NavProjectsProps) {
const { isMobile } = useSidebar()
return (
// Upstream shadcn-ui hides this entire group on icon-mode collapse
// with `group-data-[collapsible=icon]:hidden`. We keep it visible so
// the project icons stay reachable in the narrow column; the
// SidebarGroupLabel below and the `<span>` children of each
// SidebarMenuButton already self-hide on collapse, leaving an
// icon-only column that lines up with NavMain.
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton>
<MoreHorizontal />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}
'use client'
import * as React from 'react'
import { type LucideIcon } from 'lucide-react'
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar'
export interface NavSecondaryProps extends React.ComponentProps<typeof SidebarGroup> {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
}[]
}
export function NavSecondary({ items, ...props }: NavSecondaryProps) {
return (
<SidebarGroup data-slot="sidebar-02" {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm" isActive={item.isActive}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
'use client'
import * as React from 'react'
import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Monitor, Moon, Palette, Sun, Zap } from 'lucide-react'
import { useTheme } from '@/lib/use-theme'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
export interface NavUserProps {
user: {
name: string
email: string
}
}
export function NavUser({ user }: NavUserProps) {
const { isMobile } = useSidebar()
const { theme, setTheme } = useTheme()
const initials = React.useMemo(() => {
const parts = user.name.trim().split(/\s+/).slice(0, 2)
return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U'
}, [user.name])
return (
<SidebarMenu data-slot="sidebar-02">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
>
<Avatar className="h-8 w-8 shrink-0 rounded-lg group-data-[collapsible=icon]:size-6">
<AvatarFallback className="rounded-lg text-xs">{initials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? 'bottom' : 'right'}
align="end"
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">
<AvatarFallback className="rounded-lg text-xs">{initials}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Zap />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette />
Theme
<span className="text-muted-foreground ml-auto text-xs capitalize">{theme}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="min-w-36">
<DropdownMenuRadioGroup value={theme} onValueChange={(v) => setTheme(v)}>
<DropdownMenuRadioItem value="light">
<Sun /> Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Moon /> Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<Monitor /> System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
// Demo page wired by `npx shadcn@latest add https://uipkge.dev/r/react/sidebar-02.json`.
// Visit /sidebar-02-demo after install to see the block without
// editing your own pages. Free to edit or delete me.
import { Sidebar02 } from '@/components/blocks/sidebar-02/Sidebar02'
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
export default function Page() {
return (
<SidebarProvider data-slot="sidebar-02">
<Sidebar02 />
<SidebarInset>
<header className="border-border flex h-14 shrink-0 items-center gap-3 border-b px-4">
<SidebarTrigger className="size-7" />
<h1 className="text-sm font-semibold">sidebar-02 demo</h1>
</header>
<div className="flex-1 p-6">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="bg-muted/50 aspect-video rounded-xl" />
<div className="bg-muted/50 aspect-video rounded-xl" />
<div className="bg-muted/50 aspect-video rounded-xl" />
</div>
<div className="bg-muted/50 mt-4 min-h-[60vh] rounded-xl" />
</div>
</SidebarInset>
</SidebarProvider>
)
}
Raw manifest:https://uipkge.dev/r/react/sidebar-02.json