Menu

Change language

GitHub repo

Team Switcher Sidebar

blocklayout

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.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/sidebar-02.json
Manifest:https://uipkge.dev/r/lit/sidebar-02.jsonInstalls to:src/uip/blocks/sidebar-02/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

Team
interface Team {
  name: string
  logo: IconNode | 'mark'
  plan: string
}
NavSubItem
interface NavSubItem {
  title: string
  url: string
  isActive?: boolean
}
NavMainItem
interface NavMainItem {
  title: string
  url: string
  icon: IconNode
  iconName: string
  isActive?: boolean
  items?: NavSubItem[]
}
HTMLElementTagNameMap
interface 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: '[email protected]' }
    
    /**
     * <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