
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 React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar-02.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar-02.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar-02.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar-02.jsonnpx shadcn-vue@latest add @uipkge/sidebar-02Installs to:app/components/blocks/sidebar-02/<script setup lang="ts">
import { BookOpen, Bot, Frame, LifeBuoy, Map, PieChart, Send, Settings2, SquareTerminal } from 'lucide-vue-next'
import NavMain from './NavMain.vue'
import NavProjects from './NavProjects.vue'
import NavSecondary from './NavSecondary.vue'
import NavUser from './NavUser.vue'
import TeamSwitcher from './TeamSwitcher.vue'
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from '@/components/ui/sidebar'
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,
},
],
}
</script>
<template>
<Sidebar data-slot="sidebar-02" collapsible="icon">
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain :items="data.navMain" />
<NavProjects :projects="data.projects" />
<NavSecondary :items="data.navSecondary" class="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser :user="data.user" />
</SidebarFooter>
<SidebarRail />
</Sidebar>
</template>
<script setup lang="ts">
// 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 { h, ref } from 'vue'
import { AudioWaveform, Check, ChevronsUpDown, Command, Plus } from 'lucide-vue-next'
// 4-quadrant uipkge logo. Inline functional component so it can sit
// next to Lucide icons in the teams array without an extra file.
const UipkgeMark = (props: { class?: string }) =>
h('svg', { viewBox: '0 0 24 24', class: props.class, fill: 'currentColor', 'aria-hidden': 'true' }, [
h('rect', { x: 2, y: 2, width: 9, height: 9, rx: 1.5 }),
h('rect', { x: 13, y: 2, width: 9, height: 9, rx: 1.5, opacity: 0.55 }),
h('rect', { x: 2, y: 13, width: 9, height: 9, rx: 1.5, opacity: 0.55 }),
h('rect', { x: 13, y: 13, width: 9, height: 9, rx: 1.5 }),
])
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
const teams = [
{ name: 'uipkge', logo: UipkgeMark, plan: 'UI registry' },
{ name: 'uipkge HRMS', logo: AudioWaveform, plan: 'Vertical' },
{ name: 'uipkge Hospital', logo: Command, plan: 'Vertical' },
]
const { isMobile } = useSidebar()
const activeTeam = ref(teams[0]!)
function setActive(team: (typeof teams)[number]) {
activeTeam.value = team
}
</script>
<template>
<SidebarMenu data-slot="sidebar-02">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
>
<div
class="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"
>
<component :is="activeTeam.logo" class="size-4 group-data-[collapsible=icon]:size-3.5" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span class="font-display truncate font-bold">{{ activeTeam.name }}</span>
<span class="text-muted-foreground truncate text-xs">{{ activeTeam.plan }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
:side="isMobile ? 'bottom' : 'right'"
align="start"
:side-offset="4"
>
<DropdownMenuLabel class="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
<DropdownMenuItem v-for="(team, i) in teams" :key="team.name" class="gap-2 p-2" @select="setActive(team)">
<div class="flex size-6 items-center justify-center rounded-sm border">
<component :is="team.logo" class="size-3.5 shrink-0" />
</div>
{{ team.name }}
<Check v-if="activeTeam === team" class="ml-auto size-4" />
<DropdownMenuShortcut v-else>⌘{{ i + 1 }}</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="gap-2 p-2">
<div class="bg-background flex size-6 items-center justify-center rounded-md border">
<Plus class="size-4" />
</div>
<div class="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</template>
<script setup lang="ts">
import type { LucideIcon } from 'lucide-vue-next'
import { ChevronRight } from 'lucide-vue-next'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from '@/components/ui/sidebar'
defineProps<{
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
isActive?: boolean
}[]
}[]
}>()
</script>
<template>
<SidebarGroup data-slot="sidebar-02">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<Collapsible v-for="item in items" :key="item.title" as-child :default-open="item.isActive">
<SidebarMenuItem>
<SidebarMenuButton as-child :tooltip="item.title" :is-active="item.isActive">
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a>
</SidebarMenuButton>
<template v-if="item.items?.length">
<CollapsibleTrigger as-child>
<SidebarMenuAction class="data-[state=open]:rotate-90">
<ChevronRight />
<span class="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
<SidebarMenuSubButton as-child :is-active="subItem.isActive">
<a :href="subItem.url">
<span>{{ subItem.title }}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</template>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
</template>
<script setup lang="ts">
import type { LucideIcon } from 'lucide-vue-next'
import { Folder, Forward, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
defineProps<{
projects: {
name: string
url: string
icon: LucideIcon
}[]
}>()
const { isMobile } = useSidebar()
</script>
<template>
<!-- Upstream shadcn-vue 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>
<SidebarMenuItem v-for="item in projects" :key="item.name">
<SidebarMenuButton as-child>
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuAction show-on-hover>
<MoreHorizontal />
<span class="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-48 rounded-lg"
:side="isMobile ? 'bottom' : 'right'"
:align="isMobile ? 'end' : 'start'"
>
<DropdownMenuItem>
<Folder class="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward class="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 class="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<MoreHorizontal />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</template>
<script setup lang="ts">
import type { LucideIcon } from 'lucide-vue-next'
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar'
const props = defineProps<{
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
}[]
}>()
</script>
<template>
<SidebarGroup data-slot="sidebar-02">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton as-child size="sm" :is-active="item.isActive">
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Monitor, Moon, Palette, Sun, Zap } from 'lucide-vue-next'
import { useTheme } from '~/composables/useTheme'
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'
const props = defineProps<{
user: {
name: string
email: string
}
}>()
const { isMobile } = useSidebar()
const { theme, setTheme } = useTheme()
const initials = computed(() => {
const parts = props.user.name.trim().split(/\s+/).slice(0, 2)
return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U'
})
</script>
<template>
<SidebarMenu data-slot="sidebar-02">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
>
<Avatar class="h-8 w-8 shrink-0 rounded-lg group-data-[collapsible=icon]:size-6">
<AvatarFallback class="rounded-lg text-xs">{{ initials }}</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
<span class="truncate font-medium">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
:side="isMobile ? 'bottom' : 'right'"
align="end"
:side-offset="4"
>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarFallback class="rounded-lg text-xs">{{ initials }}</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="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 class="text-muted-foreground ml-auto text-xs capitalize">{{ theme }}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent class="min-w-36">
<DropdownMenuRadioGroup
:model-value="theme"
@update:model-value="(v) => setTheme(v as 'light' | 'dark' | 'system')"
>
<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>
</template>
Raw manifest:https://uipkge.dev/r/vue/sidebar-02.json