Menu

Change language

GitHub repo

Starred Project Sidebar

blocklayout

Colorful, customised app sidebar -- branded project tiles with star favorites, badge counts, team avatars with status dots, and a user pill footer. Inline search input with ⌘F shortcut. Modeled on a HRMS workspace shell.

Installation

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

Variants

Loading interactive previews…

Schema

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

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-sidebar-06': UipSidebar06
  }
}

Files installed (1)

  • src/uip/blocks/sidebar-06/sidebar-06.ts16 kB
    import { LitElement, css, html } from 'lit'
    import {
      Briefcase,
      Calendar,
      ChartLine,
      ChevronsUpDown,
      ClipboardList,
      Folder,
      GraduationCap,
      HeartPulse,
      HelpCircle,
      Inbox,
      LayoutDashboard,
      Monitor,
      Moon,
      Palette,
      Search,
      Send,
      Settings as SettingsIcon,
      Star,
      Sun,
      Users,
    } 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/dropdown-menu/dropdown-menu'
    import '../../components/input/input'
    import type { UipInput } from '../../components/input/input'
    import '../../components/sidebar/sidebar'
    import type { UipSidebarProvider } from '../../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)
    }
    
    // Colorful, customised HRMS-style sidebar -- branded project tiles, badges,
    // status dots, and a user pill footer. Every row is spelled out inline so
    // you can swap colors and labels in place.
    
    /**
     * <uip-sidebar-06> — the registry Sidebar06 block as a web component.
     *
     * Class strings are React's verbatim. The footer dropdown's `side` follows
     * the provider's isMobile (React's useSidebar); the theme submenu writes
     * the `dark` class on documentElement directly (the `next-themes`
     * dependency is not ported — same visual result, persisted to
     * localStorage). The search field is <uip-input> with `prefix`/`suffix`
     * slots (React's SidebarInput prefix/suffix props). No props, no events:
     * edit the markup in place.
     */
    export class UipSidebar06 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 = {
        theme: { state: true },
        query: { state: true },
      }
    
      private theme: ThemeValue = 'light'
      private query = ''
      private media?: MediaQueryList
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'sidebar-06')
        this.theme = getTheme()
        // Re-render when the viewport crosses the mobile breakpoint so the
        // footer dropdown side follows isMobile (React's useSidebar re-render).
        this.media = window.matchMedia('(max-width: 768px)')
        this.media.addEventListener('change', this.rerender)
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        this.media?.removeEventListener('change', this.rerender)
      }
    
      private rerender = () => this.requestUpdate()
    
      render() {
        const isMobile = (this.closest('uip-sidebar-provider') as UipSidebarProvider | null)?.isMobile ?? false
        return html`
          <uip-sidebar collapsible="icon">
            <uip-sidebar-header>
              <uip-sidebar-menu>
                <uip-sidebar-menu-item>
                  <uip-sidebar-menu-button size="lg" tooltip="uipkge" class="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 shadow-sm group-data-[collapsible=icon]:size-6">
                      <svg viewBox="0 0 24 24" class="size-4 group-data-[collapsible=icon]:size-3.5" 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>
                    </div>
                    <div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                      <span class="font-display truncate font-bold">uipkge</span>
                      <span class="text-muted-foreground truncate text-xs">uipkge.dev</span>
                    </div>
                  </uip-sidebar-menu-button>
                </uip-sidebar-menu-item>
              </uip-sidebar-menu>
    
              <div class="px-1 pt-1 group-data-[collapsible=icon]:hidden">
                <uip-input
                  type="text"
                  placeholder="Search..."
                  allow-clear
                  class="[&::part(control)]:bg-background [&::part(control)]:h-8 [&::part(control)]:shadow-none"
                  .value=${this.query}
                  @input=${(e: Event) => {
                    this.query = (e.target as UipInput).value
                  }}
                >
                  <span slot="prefix">${icon(Search, 'search', 'size-3.5')}</span>
                  <kbd slot="suffix" class="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border px-1 font-mono text-xs font-medium select-none">
                    <span>&#8984;</span>F
                  </kbd>
                </uip-input>
              </div>
            </uip-sidebar-header>
    
            <uip-sidebar-content>
              <uip-sidebar-group>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button is-active tooltip="Dashboard">
                        ${icon(LayoutDashboard, 'layout-dashboard', 'size-4')} <span>Dashboard</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Employees">
                        ${icon(Users, 'users', 'size-4')} <span>Employees</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Clients">
                        ${icon(Briefcase, 'briefcase', 'size-4')} <span>Clients</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Projects">
                        ${icon(Folder, 'folder', 'size-4')} <span>Projects</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-separator></uip-sidebar-separator>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>Workforce</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Calendar">
                        ${icon(Calendar, 'calendar', 'size-4')} <span>Calendar</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Attendance">
                        ${icon(ClipboardList, 'clipboard-list', 'size-4')} <span>Attendance</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Inbox">
                        ${icon(Inbox, 'inbox', 'size-4')} <span>Inbox</span>
                      </uip-sidebar-menu-button>
                      <uip-sidebar-menu-badge class="[&::part(base)]:bg-destructive [&::part(base)]:text-white">6</uip-sidebar-menu-badge>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Analytics">
                        ${icon(ChartLine, 'chart-line', 'size-4')} <span>Analytics</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-separator></uip-sidebar-separator>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>Projects</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    ${[
                      { tile: 'bg-chart-1', icon: Folder, iconName: 'folder', name: 'Shift Planner' },
                      { tile: 'bg-chart-2', icon: GraduationCap, iconName: 'graduation-cap', name: 'Training Portal' },
                      { tile: 'bg-chart-3', icon: HeartPulse, iconName: 'heart-pulse', name: 'Performance Hub' },
                      { tile: 'bg-chart-4', icon: ChartLine, iconName: 'chart-line', name: 'Expense Claims' },
                    ].map(
                      (p) => html`
                        <uip-sidebar-menu-item>
                          <uip-sidebar-menu-button tooltip=${p.name}>
                            <div class=${`${p.tile} flex aspect-square size-5 shrink-0 items-center justify-center rounded-md text-white`}>
                              ${icon(p.icon, p.iconName, 'size-3')}
                            </div>
                            <span>${p.name}</span>
                          </uip-sidebar-menu-button>
                          <uip-sidebar-menu-badge class="[&::part(base)]:text-warning">
                            ${icon(Star, 'star', 'size-3.5 fill-current')}
                          </uip-sidebar-menu-badge>
                        </uip-sidebar-menu-item>
                      `,
                    )}
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-separator></uip-sidebar-separator>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>My team</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    ${[
                      { initials: 'OB', tile: 'bg-chart-1/15', name: 'Olivia Bennett', dot: 'bg-destructive' },
                      { initials: 'DM', tile: 'bg-chart-2/15', name: 'Daniel Morgan', dot: 'bg-warning' },
                      { initials: 'ER', tile: 'bg-chart-3/15', name: 'Ethan Reynolds', dot: 'bg-success' },
                    ].map(
                      (person) => html`
                        <uip-sidebar-menu-item>
                          <uip-sidebar-menu-button tooltip=${person.name}>
                            <uip-avatar class="size-5">
                              <uip-avatar-fallback class=${`${person.tile} text-foreground text-xs`}>${person.initials}</uip-avatar-fallback>
                            </uip-avatar>
                            <span>${person.name}</span>
                          </uip-sidebar-menu-button>
                          <uip-sidebar-menu-badge>
                            <span class=${`${person.dot} size-1.5 rounded-full`}></span>
                          </uip-sidebar-menu-badge>
                        </uip-sidebar-menu-item>
                      `,
                    )}
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-separator></uip-sidebar-separator>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>Administration</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Settings">
                        ${icon(SettingsIcon, 'settings', 'size-4')} <span>Settings</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Help center">
                        ${icon(HelpCircle, 'circle-question-mark', 'size-4')} <span>Help center</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"
                    >
                      <div class="relative shrink-0">
                        <uip-avatar class="h-8 w-8 rounded-lg group-data-[collapsible=icon]:size-6">
                          <uip-avatar-fallback class="bg-sidebar-accent text-sidebar-accent-foreground rounded-lg text-xs">JC</uip-avatar-fallback>
                        </uip-avatar>
                        <span class="bg-destructive ring-sidebar absolute -right-0.5 -bottom-0.5 size-2.5 rounded-full ring-2"></span>
                      </div>
                      <div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                        <span class="truncate font-medium">James Carter</span>
                        <span class="text-muted-foreground truncate text-xs">Admin</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=${isMobile ? 'bottom' : 'right'} align="end" side-offset="4">
                      <uip-dropdown-menu-label>James Carter</uip-dropdown-menu-label>
                      <uip-dropdown-menu-separator></uip-dropdown-menu-separator>
                      <uip-dropdown-menu-item>${icon(Send, 'send', 'size-4')} Invite teammates</uip-dropdown-menu-item>
                      <uip-dropdown-menu-item>${icon(SettingsIcon, 'settings', 'size-4')} Workspace settings</uip-dropdown-menu-item>
                      <uip-dropdown-menu-separator></uip-dropdown-menu-separator>
                      <uip-dropdown-menu-sub>
                        <uip-dropdown-menu-sub-trigger>
                          ${icon(Palette, 'palette', 'size-4')} 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', 'size-4')} Light</uip-dropdown-menu-radio-item>
                            <uip-dropdown-menu-radio-item value="dark">${icon(Moon, 'moon', 'size-4')} Dark</uip-dropdown-menu-radio-item>
                            <uip-dropdown-menu-radio-item value="system">${icon(Monitor, 'monitor', 'size-4')} System</uip-dropdown-menu-radio-item>
                          </uip-dropdown-menu-radio-group>
                        </uip-dropdown-menu-sub-content>
                      </uip-dropdown-menu-sub>
                    </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-06') || customElements.define('uip-sidebar-06', UipSidebar06)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-sidebar-06': UipSidebar06
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/sidebar-06.json