Team Switcher Sidebar
blocklayoutFull-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.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/sidebar-02.json$npx shadcn@latest add https://uipkge.dev/r/lit/sidebar-02.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/sidebar-02.json$bunx shadcn@latest add https://uipkge.dev/r/lit/sidebar-02.jsonManifest:
https://uipkge.dev/r/lit/sidebar-02.jsonInstalls to:src/uip/blocks/sidebar-02/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Teaminterface Team {
name: string
logo: IconNode | 'mark'
plan: string
}NavSubIteminterface NavSubItem {
title: string
url: string
isActive?: boolean
}NavMainIteminterface NavMainItem {
title: string
url: string
icon: IconNode
iconName: string
isActive?: boolean
items?: NavSubItem[]
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-sidebar-02': UipSidebar02
}
}Files installed (1)
src/uip/blocks/sidebar-02/sidebar-02.ts17.8 kB
import { LitElement, css, html } from 'lit' import { AudioWaveform, BadgeCheck, Bell, BookOpen, Bot, Check, ChevronRight, ChevronsUpDown, Command, CreditCard, Folder, Forward, Frame, LifeBuoy, LogOut, Map, Monitor, Moon, MoreHorizontal, Palette, PieChart, Plus, Send, Settings2, SquareTerminal, Sun, Trash2, Zap, type IconNode, } from 'lucide' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/avatar/avatar' import '../../components/collapsible/collapsible' import '../../components/dropdown-menu/dropdown-menu' import '../../components/sidebar/sidebar' type ThemeValue = 'light' | 'dark' | 'system' function getTheme(): ThemeValue { try { const stored = localStorage.getItem('uipkge-theme') if (stored === 'light' || stored === 'dark' || stored === 'system') return stored } catch { // ignore } return document.documentElement.classList.contains('dark') ? 'dark' : 'light' } function setTheme(value: ThemeValue) { try { localStorage.setItem('uipkge-theme', value) } catch { // ignore } const dark = value === 'dark' || (value === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) document.documentElement.classList.toggle('dark', dark) } function brandMark(classes: string) { return html`<svg viewBox="0 0 24 24" class=${classes} 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>` } interface Team { name: string logo: IconNode | 'mark' plan: string } const teams: Team[] = [ { name: 'uipkge', logo: 'mark', plan: 'UI registry' }, { name: 'uipkge HRMS', logo: AudioWaveform, plan: 'Vertical' }, { name: 'uipkge Hospital', logo: Command, plan: 'Vertical' }, ] interface NavSubItem { title: string url: string isActive?: boolean } interface NavMainItem { title: string url: string icon: IconNode iconName: string isActive?: boolean items?: NavSubItem[] } // Sample data (React's `data`, inlined so consumers edit routes in place). const navMain: NavMainItem[] = [ { title: 'Playground', url: '#', icon: SquareTerminal, iconName: 'square-terminal', isActive: true, items: [ { title: 'History', url: '#' }, { title: 'Starred', url: '#' }, { title: 'Settings', url: '#' }, ], }, { title: 'Models', url: '#', icon: Bot, iconName: 'bot', items: [ { title: 'Genesis', url: '#' }, { title: 'Explorer', url: '#' }, { title: 'Quantum', url: '#' }, ], }, { title: 'Documentation', url: '#', icon: BookOpen, iconName: 'book-open', items: [ { title: 'Introduction', url: '#' }, { title: 'Get Started', url: '#' }, { title: 'Tutorials', url: '#' }, { title: 'Changelog', url: '#' }, ], }, { title: 'Settings', url: '#', icon: Settings2, iconName: 'settings-2', items: [ { title: 'General', url: '#' }, { title: 'Team', url: '#' }, { title: 'Billing', url: '#' }, { title: 'Limits', url: '#' }, ], }, ] const navSecondary: { title: string; url: string; icon: IconNode; iconName: string; isActive?: boolean }[] = [ { title: 'Support', url: '#', icon: LifeBuoy, iconName: 'life-buoy' }, { title: 'Feedback', url: '#', icon: Send, iconName: 'send' }, ] const projects = [ { name: 'Design Engineering', url: '#', icon: Frame, iconName: 'frame' }, { name: 'Sales & Marketing', url: '#', icon: PieChart, iconName: 'chart-pie' }, { name: 'Travel', url: '#', icon: Map, iconName: 'map' }, ] const user = { name: 'shadcn', email: 'm@example.com' } /** * <uip-sidebar-02> — the registry Sidebar02 block as a web component * (TeamSwitcher + NavMain + NavProjects + NavSecondary + NavUser inlined — * one block, one install unit). * * Class strings are React's verbatim. Sample data is inlined so consumers * edit routes in place; edit teams + activeTeam below to match your tenant * model. The theme submenu writes the `dark` class on documentElement * directly (the `next-themes` dependency is not ported — same visual result, * persisted to localStorage). No props, no events. */ export class UipSidebar02 extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { activeTeam: { state: true }, theme: { state: true }, } private activeTeam: Team = teams[0]! private theme: ThemeValue = 'light' constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'sidebar-02') this.theme = getTheme() } private get initials() { const parts = user.name.trim().split(/\s+/).slice(0, 2) return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U' } render() { const ActiveLogo = this.activeTeam.logo return html` <uip-sidebar collapsible="icon"> <uip-sidebar-header> <uip-sidebar-menu> <uip-sidebar-menu-item> <uip-dropdown-menu class="[&::part(content)]:min-w-56 [&::part(content)]:rounded-lg"> <uip-sidebar-menu-button slot="trigger" 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"> ${ActiveLogo === 'mark' ? brandMark('size-4 group-data-[collapsible=icon]:size-3.5') : icon(ActiveLogo as IconNode, 'team', '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">${this.activeTeam.name}</span> <span class="text-muted-foreground truncate text-xs">${this.activeTeam.plan}</span> </div> ${icon(ChevronsUpDown, 'chevrons-up-down', 'ml-auto size-4 group-data-[collapsible=icon]:hidden')} </uip-sidebar-menu-button> <uip-dropdown-menu-content side="right" align="start" side-offset="4"> <uip-dropdown-menu-label class="text-muted-foreground text-xs">Teams</uip-dropdown-menu-label> ${teams.map( (team, i) => html` <uip-dropdown-menu-item @select=${() => { this.activeTeam = team }} > <div class="flex size-6 items-center justify-center rounded-sm border"> ${team.logo === 'mark' ? brandMark('size-3.5 shrink-0') : icon(team.logo as IconNode, 'team', 'size-3.5 shrink-0')} </div> ${team.name} ${this.activeTeam === team ? icon(Check, 'check', 'ml-auto size-4') : html`<uip-dropdown-menu-shortcut>⌘${i + 1}</uip-dropdown-menu-shortcut>`} </uip-dropdown-menu-item> `, )} <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-item> <div class="bg-background flex size-6 items-center justify-center rounded-md border"> ${icon(Plus, 'plus', 'size-4')} </div> <div class="text-muted-foreground font-medium">Add team</div> </uip-dropdown-menu-item> </uip-dropdown-menu-content> </uip-dropdown-menu> </uip-sidebar-menu-item> </uip-sidebar-menu> </uip-sidebar-header> <uip-sidebar-content> <uip-sidebar-group> <uip-sidebar-group-label>Platform</uip-sidebar-group-label> <uip-sidebar-menu> ${navMain.map( (item) => html` <uip-collapsible class="contents" ?default-open=${!!item.isActive}> <uip-sidebar-menu-item> <uip-sidebar-menu-button href=${item.url} tooltip=${item.title} ?is-active=${!!item.isActive}> ${icon(item.icon, item.iconName)} <span>${item.title}</span> </uip-sidebar-menu-button> ${item.items?.length ? html` <uip-collapsible-trigger as-child> <uip-sidebar-menu-action show-on-hover aria-label="Toggle"> ${icon(ChevronRight, 'chevron-right')} <span class="sr-only">Toggle</span> </uip-sidebar-menu-action> </uip-collapsible-trigger> <uip-collapsible-content> <uip-sidebar-menu-sub> ${item.items.map( (subItem) => html` <uip-sidebar-menu-sub-item> <uip-sidebar-menu-sub-button href=${subItem.url} ?is-active=${!!subItem.isActive}> <span>${subItem.title}</span> </uip-sidebar-menu-sub-button> </uip-sidebar-menu-sub-item> `, )} </uip-sidebar-menu-sub> </uip-collapsible-content> ` : ''} </uip-sidebar-menu-item> </uip-collapsible> `, )} </uip-sidebar-menu> </uip-sidebar-group> <uip-sidebar-group> <uip-sidebar-group-label>Projects</uip-sidebar-group-label> <uip-sidebar-menu> ${projects.map( (item) => html` <uip-sidebar-menu-item> <uip-sidebar-menu-button href=${item.url}> ${icon(item.icon, item.iconName)} <span>${item.name}</span> </uip-sidebar-menu-button> <uip-dropdown-menu class="[&::part(content)]:w-48 [&::part(content)]:rounded-lg"> <uip-sidebar-menu-action slot="trigger" show-on-hover aria-label="More"> ${icon(MoreHorizontal, 'ellipsis')} <span class="sr-only">More</span> </uip-sidebar-menu-action> <uip-dropdown-menu-content side="right" align="start"> <uip-dropdown-menu-item> ${icon(Folder, 'folder', 'text-muted-foreground')} <span>View Project</span> </uip-dropdown-menu-item> <uip-dropdown-menu-item> ${icon(Forward, 'forward', 'text-muted-foreground')} <span>Share Project</span> </uip-dropdown-menu-item> <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-item> ${icon(Trash2, 'trash-2', 'text-muted-foreground')} <span>Delete Project</span> </uip-dropdown-menu-item> </uip-dropdown-menu-content> </uip-dropdown-menu> </uip-sidebar-menu-item> `, )} <uip-sidebar-menu-item> <uip-sidebar-menu-button> ${icon(MoreHorizontal, 'ellipsis')} <span>More</span> </uip-sidebar-menu-button> </uip-sidebar-menu-item> </uip-sidebar-menu> </uip-sidebar-group> <uip-sidebar-group class="[&::part(base)]:mt-auto"> <uip-sidebar-group-content> <uip-sidebar-menu> ${navSecondary.map( (item) => html` <uip-sidebar-menu-item> <uip-sidebar-menu-button href=${item.url} size="sm" ?is-active=${!!item.isActive}> ${icon(item.icon, item.iconName)} <span>${item.title}</span> </uip-sidebar-menu-button> </uip-sidebar-menu-item> `, )} </uip-sidebar-menu> </uip-sidebar-group-content> </uip-sidebar-group> </uip-sidebar-content> <uip-sidebar-footer> <uip-sidebar-menu> <uip-sidebar-menu-item> <uip-dropdown-menu class="[&::part(content)]:min-w-56 [&::part(content)]:rounded-lg"> <uip-sidebar-menu-button slot="trigger" size="lg" class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center" > <uip-avatar class="h-8 w-8 shrink-0 rounded-lg group-data-[collapsible=icon]:size-6"> <uip-avatar-fallback class="rounded-lg text-xs">${this.initials}</uip-avatar-fallback> </uip-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> ${icon(ChevronsUpDown, 'chevrons-up-down', 'ml-auto size-4 group-data-[collapsible=icon]:hidden')} </uip-sidebar-menu-button> <uip-dropdown-menu-content side="right" align="end" side-offset="4"> <uip-dropdown-menu-label> <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <uip-avatar class="h-8 w-8 rounded-lg"> <uip-avatar-fallback class="rounded-lg text-xs">${this.initials}</uip-avatar-fallback> </uip-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> </uip-dropdown-menu-label> <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-group> <uip-dropdown-menu-item>${icon(Zap, 'zap')} Upgrade to Pro</uip-dropdown-menu-item> </uip-dropdown-menu-group> <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-group> <uip-dropdown-menu-item>${icon(BadgeCheck, 'badge-check')} Account</uip-dropdown-menu-item> <uip-dropdown-menu-item>${icon(CreditCard, 'credit-card')} Billing</uip-dropdown-menu-item> <uip-dropdown-menu-item>${icon(Bell, 'bell')} Notifications</uip-dropdown-menu-item> </uip-dropdown-menu-group> <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-sub> <uip-dropdown-menu-sub-trigger> ${icon(Palette, 'palette')} Theme <span class="text-muted-foreground ml-auto text-xs capitalize">${this.theme}</span> </uip-dropdown-menu-sub-trigger> <uip-dropdown-menu-sub-content> <uip-dropdown-menu-radio-group value=${this.theme} @value-change=${(e: CustomEvent<{ value: string }>) => { this.theme = e.detail.value as ThemeValue setTheme(this.theme) }} > <uip-dropdown-menu-radio-item value="light">${icon(Sun, 'sun')} Light</uip-dropdown-menu-radio-item> <uip-dropdown-menu-radio-item value="dark">${icon(Moon, 'moon')} Dark</uip-dropdown-menu-radio-item> <uip-dropdown-menu-radio-item value="system">${icon(Monitor, 'monitor')} System</uip-dropdown-menu-radio-item> </uip-dropdown-menu-radio-group> </uip-dropdown-menu-sub-content> </uip-dropdown-menu-sub> <uip-dropdown-menu-separator></uip-dropdown-menu-separator> <uip-dropdown-menu-item>${icon(LogOut, 'log-out')} Log out</uip-dropdown-menu-item> </uip-dropdown-menu-content> </uip-dropdown-menu> </uip-sidebar-menu-item> </uip-sidebar-menu> </uip-sidebar-footer> <uip-sidebar-rail></uip-sidebar-rail> </uip-sidebar> ` } } customElements.get('uip-sidebar-02') || customElements.define('uip-sidebar-02', UipSidebar02) declare global { interface HTMLElementTagNameMap { 'uip-sidebar-02': UipSidebar02 } }
Raw manifest:https://uipkge.dev/r/lit/sidebar-02.json