Menu

Change language

GitHub repo

Toolbar Action Sidebar

blocklayout

App sidebar with a bottom action bar -- workspace pill, ⌘K search, grouped nav with section labels (Dashboard / Management / Content), badge counts, user pill above an icon toolbar (Settings, Help, Notifications, Command, Log out). Built on sidebar-01.

Installation

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

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-07': UipSidebar07
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/sidebar-07/sidebar-07.ts12.4 kB
    import { LitElement, css, html } from 'lit'
    import {
      Bell,
      ChartBar,
      ChevronsUpDown,
      Command,
      CreditCard,
      FileText,
      HelpCircle,
      LineChart,
      Mail,
      Moon,
      Package,
      Plus,
      Search,
      Settings as SettingsIcon,
      ShoppingCart,
      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/input/input'
    import type { UipInput } from '../../components/input/input'
    import '../../components/sidebar/sidebar'
    import type { UipSidebarProvider } from '../../components/sidebar/sidebar'
    
    function isDarkActive(): boolean {
      return document.documentElement.classList.contains('dark')
    }
    
    function setDark(dark: boolean) {
      document.documentElement.classList.toggle('dark', dark)
      try {
        localStorage.setItem('uipkge-theme', dark ? 'dark' : 'light')
      } catch {
        // ignore
      }
    }
    
    // App sidebar with a bottom action bar. Workspace pill + search at the top,
    // three grouped nav sections, user pill above an icon toolbar (Settings,
    // Help, Notifications, Command palette, theme toggle) anchored at the very
    // bottom. Every row is spelled out inline so you can edit in place.
    
    /**
     * <uip-sidebar-07> — the registry Sidebar07 block as a web component.
     *
     * Class strings are React's verbatim. The footer toolbar follows the
     * provider's state (React's useSidebar `state`); the block re-renders when
     * the provider's `open-change` fires. The search field is <uip-input> with
     * `prefix`/`suffix` slots (React's SidebarInput prefix/suffix props). The
     * theme toggle writes the `dark` class on documentElement directly (the
     * `next-themes` dependency is not ported). Deviations: the toolbar's last
     * button is React's SidebarTrigger (collapse toggle) — ported as
     * <uip-sidebar-trigger>, which carries its own trigger styling on top of
     * React's row classes. No props, no events: edit the markup in place.
     */
    export class UipSidebar07 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 = {
        isDark: { state: true },
        query: { state: true },
      }
    
      private isDark = false
      private query = ''
      private media?: MediaQueryList
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'sidebar-07')
        this.isDark = isDarkActive()
        // Re-render when the provider opens/closes or the viewport crosses the
        // mobile breakpoint (React's useSidebar re-render).
        this.addEventListener('open-change', this.rerender)
        this.media = window.matchMedia('(max-width: 768px)')
        this.media.addEventListener('change', this.rerender)
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        this.removeEventListener('open-change', this.rerender)
        this.media?.removeEventListener('change', this.rerender)
      }
    
      private rerender = () => this.requestUpdate()
    
      private toolbarButton(label: string, iconName: string, iconNode: typeof SettingsIcon, onClick?: () => void) {
        return html`
          <button
            type="button"
            aria-label=${label}
            class="text-muted-foreground hover:bg-sidebar-accent hover:text-foreground flex aspect-square size-8 items-center justify-center rounded-md transition-colors"
            @click=${() => onClick?.()}
          >
            ${icon(iconNode, iconName, 'size-4')}
          </button>
        `
      }
    
      render() {
        const expanded = (this.closest('uip-sidebar-provider') as UipSidebarProvider | null)?.state !== 'collapsed'
        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 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-semibold">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>K
                  </kbd>
                </uip-input>
              </div>
            </uip-sidebar-header>
    
            <uip-sidebar-content>
              <uip-sidebar-group>
                <uip-sidebar-group-label>Dashboard</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button
                        is-active
                        tooltip="Analytics"
                        class="[&[is-active]::part(base)]:!bg-success/20 [&[is-active]::part(base)]:!text-foreground"
                      >
                        ${icon(ChartBar, 'chart-bar', 'size-4')} <span>Analytics</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Performance">
                        ${icon(LineChart, 'chart-line', 'size-4')} <span>Performance</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>Management</uip-sidebar-group-label>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Products">
                        ${icon(Package, 'package', 'size-4')} <span>Products</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Orders">
                        ${icon(ShoppingCart, 'shopping-cart', 'size-4')} <span>Orders</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Subscribers">
                        ${icon(Users, 'users', 'size-4')} <span>Subscribers</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Transactions">
                        ${icon(CreditCard, 'credit-card', 'size-4')} <span>Transactions</span>
                      </uip-sidebar-menu-button>
                    </uip-sidebar-menu-item>
                  </uip-sidebar-menu>
                </uip-sidebar-group-content>
              </uip-sidebar-group>
    
              <uip-sidebar-group>
                <uip-sidebar-group-label>Content</uip-sidebar-group-label>
                <uip-sidebar-group-action aria-label="Add content">
                  ${icon(Plus, 'plus', 'size-4')}
                </uip-sidebar-group-action>
                <uip-sidebar-group-content>
                  <uip-sidebar-menu>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Posts">
                        ${icon(FileText, 'file-text', 'size-4')} <span>Posts</span>
                      </uip-sidebar-menu-button>
                      <uip-sidebar-menu-badge class="[&::part(base)]:bg-destructive [&::part(base)]:text-white">10</uip-sidebar-menu-badge>
                    </uip-sidebar-menu-item>
                    <uip-sidebar-menu-item>
                      <uip-sidebar-menu-button tooltip="Pages">
                        ${icon(Mail, 'mail', 'size-4')} <span>Pages</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-sidebar-menu-button size="lg" class="group-data-[collapsible=icon]:!justify-center">
                    <uip-avatar class="size-8 rounded-lg group-data-[collapsible=icon]:size-6">
                      <uip-avatar-fallback class="bg-sidebar-accent text-sidebar-accent-foreground rounded-lg text-xs">SW</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">Sarah Wilson</span>
                      <span class="text-muted-foreground truncate text-xs">Admin Account</span>
                    </div>
                    ${icon(ChevronsUpDown, 'chevrons-up-down', 'ml-auto size-4 group-data-[collapsible=icon]:hidden')}
                  </uip-sidebar-menu-button>
                </uip-sidebar-menu-item>
              </uip-sidebar-menu>
    
              ${expanded
                ? html`
                    <uip-sidebar-separator class="[&::part(base)]:my-1"></uip-sidebar-separator>
                    <div class="flex items-center justify-between px-1 pb-1">
                      ${this.toolbarButton('Settings', 'settings', SettingsIcon)}
                      ${this.toolbarButton('Help', 'circle-question-mark', HelpCircle)}
                      ${this.toolbarButton('Notifications', 'bell', Bell)}
                      ${this.toolbarButton('Command palette', 'command', Command)}
                      <button
                        type="button"
                        aria-label=${this.isDark ? 'Switch to light theme' : 'Switch to dark theme'}
                        class="text-muted-foreground hover:bg-sidebar-accent hover:text-foreground flex aspect-square size-8 items-center justify-center rounded-md transition-colors"
                        @click=${() => {
                          this.isDark = !this.isDark
                          setDark(this.isDark)
                        }}
                      >
                        ${this.isDark ? icon(Sun, 'sun', 'size-4') : icon(Moon, 'moon', 'size-4')}
                      </button>
                      <uip-sidebar-trigger class="text-muted-foreground hover:bg-sidebar-accent hover:text-foreground size-8 rounded-md"></uip-sidebar-trigger>
                    </div>
                  `
                : html`
                    <div class="flex items-center justify-center pb-1">
                      <uip-sidebar-trigger class="text-muted-foreground hover:bg-sidebar-accent hover:text-foreground size-8 rounded-md"></uip-sidebar-trigger>
                    </div>
                  `}
            </uip-sidebar-footer>
    
            <uip-sidebar-rail></uip-sidebar-rail>
          </uip-sidebar>
        `
      }
    }
    
    customElements.get('uip-sidebar-07') || customElements.define('uip-sidebar-07', UipSidebar07)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-sidebar-07': UipSidebar07
      }
    }
    

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