Menu

Change language

GitHub repo

Sales Sidebar

blocklayout

App sidebar for sales workspaces: team switcher header, collapsible platform nav with badges, store list, secondary links, and user footer. Composed from the sidebar, collapsible, dropdown-menu and badge primitives; place inside <ui-sidebar-provider> for the icon rail, Cmd/Ctrl+B and mobile sheet. Selection flows through [(active)].

Installation

$npx uipkge-ng@latest add sales-sidebar
Manifest:https://uipkge.dev/r/angular/sales-sidebar.jsonInstalls to:src/app/components/blocks/sales-sidebar/

Variants

Loading interactive previews…

Schema

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

SalesNavChild
interface SalesNavChild {
  title: string
}
SalesNavItem
interface SalesNavItem {
  title: string
  icon: LucideIconData
  badge?: string
  children?: SalesNavChild[]
}
SalesTeam
interface SalesTeam {
  name: string
  plan: string
  initials: string
}

Files installed (2)

  • src/app/components/blocks/sales-sidebar/sales-sidebar.component.ts14 kB
    import { Component, EventEmitter, Input, Output, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      UiSidebarComponent,
      UiSidebarContentComponent,
      UiSidebarFooterComponent,
      UiSidebarGroupComponent,
      UiSidebarGroupLabelComponent,
      UiSidebarHeaderComponent,
      UiSidebarMenuButtonComponent,
      UiSidebarMenuComponent,
      UiSidebarMenuItemComponent,
      UiSidebarMenuSubButtonComponent,
      UiSidebarMenuSubComponent,
      UiSidebarMenuSubItemComponent,
      UiSidebarRailComponent,
      injectSidebar,
    } from '@/ui/sidebar/sidebar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import {
      UiCollapsibleComponent,
      UiCollapsibleContentComponent,
      UiCollapsibleTriggerComponent,
    } from '@/ui/collapsible/collapsible.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import {
      LucideAngularModule,
      LayoutDashboard,
      ShoppingCart,
      Users,
      Package,
      TrendingUp,
      Settings,
      LifeBuoy,
      Store,
      Plus,
      Check,
      ChevronsUpDown,
      ChevronRight,
      LogOut,
      type LucideIconData,
    } from 'lucide-angular'
    
    export interface SalesNavChild {
      title: string
    }
    
    export interface SalesNavItem {
      title: string
      icon: LucideIconData
      badge?: string
      children?: SalesNavChild[]
    }
    
    export interface SalesTeam {
      name: string
      plan: string
      initials: string
    }
    
    const NAV_MAIN: SalesNavItem[] = [
      { title: 'Dashboard', icon: LayoutDashboard },
      {
        title: 'Orders',
        icon: ShoppingCart,
        badge: '12',
        children: [{ title: 'All orders' }, { title: 'Pending' }, { title: 'Returns' }],
      },
      { title: 'Customers', icon: Users },
      {
        title: 'Products',
        icon: Package,
        children: [{ title: 'Catalog' }, { title: 'Inventory' }, { title: 'Categories' }],
      },
      { title: 'Analytics', icon: TrendingUp },
      { title: 'Settings', icon: Settings },
    ]
    
    const NAV_SECONDARY: SalesNavItem[] = [{ title: 'Support', icon: LifeBuoy }]
    
    const STORES: SalesNavItem[] = [
      { title: 'Flagship store', icon: Store },
      { title: 'Outlet store', icon: Store },
    ]
    
    const TEAMS: SalesTeam[] = [
      { name: 'Acme Inc', plan: 'Flagship store', initials: 'AC' },
      { name: 'Globex', plan: 'Outlet store', initials: 'GX' },
    ]
    
    /**
     * Sales-sidebar navigation: team switcher, collapsible platform groups (one open at a
     * time), store list, secondary links and a user menu. Composed from the sidebar,
     * collapsible, dropdown-menu and badge primitives; renders inside <ui-sidebar>, so it
     * gets the icon rail, tooltips and the mobile sheet from the primitive. Selection is
     * owned by the parent via [(active)].
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-sales-sidebar-nav, [ui-sales-sidebar-nav]',
      standalone: true,
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiCollapsibleComponent,
        UiCollapsibleTriggerComponent,
        UiCollapsibleContentComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuTriggerComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiSidebarHeaderComponent,
        UiSidebarContentComponent,
        UiSidebarFooterComponent,
        UiSidebarGroupComponent,
        UiSidebarGroupLabelComponent,
        UiSidebarMenuComponent,
        UiSidebarMenuItemComponent,
        UiSidebarMenuButtonComponent,
        UiSidebarMenuSubComponent,
        UiSidebarMenuSubItemComponent,
        UiSidebarMenuSubButtonComponent,
      ],
      template: `
        <ui-sidebar-header>
          <ul ui-sidebar-menu>
            <li ui-sidebar-menu-item>
              <ui-dropdown-menu>
                <button
                  type="button"
                  ui-sidebar-menu-button
                  ui-dropdown-menu-trigger
                  size="lg"
                  class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
                >
                  <span
                    class="bg-sidebar-primary text-sidebar-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold group-data-[collapsible=icon]:size-6 group-data-[collapsible=icon]:text-[10px]"
                  >
                    {{ activeTeam().initials }}
                  </span>
                  <span class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                    <span class="truncate font-bold">{{ activeTeam().name }}</span>
                    <span class="text-muted-foreground truncate text-xs">{{ activeTeam().plan }}</span>
                  </span>
                  <lucide-icon
                    [img]="ChevronsUpDown"
                    class="text-muted-foreground ml-auto size-4 group-data-[collapsible=icon]:hidden"
                  />
                </button>
                <ui-dropdown-menu-content
                  class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
                  [side]="sidebar.isMobile ? 'bottom' : 'right'"
                  align="start"
                  [sideOffset]="4"
                >
                  <ui-dropdown-menu-label class="text-muted-foreground text-xs">Stores</ui-dropdown-menu-label>
                  @for (team of teams; track team.name) {
                    <ui-dropdown-menu-item class="gap-2 p-2" (select)="selectTeam(team)">
                      <span class="flex size-6 items-center justify-center rounded-sm border text-xs font-bold">{{
                        team.initials
                      }}</span>
                      {{ team.name }}
                      @if (team.name === activeTeam().name) {
                        <lucide-icon [img]="Check" class="ml-auto size-4" />
                      }
                    </ui-dropdown-menu-item>
                  }
                </ui-dropdown-menu-content>
              </ui-dropdown-menu>
            </li>
          </ul>
        </ui-sidebar-header>
    
        <ui-sidebar-content class="gap-4">
          <ui-sidebar-group>
            <ui-sidebar-group-label>Platform</ui-sidebar-group-label>
            <ul ui-sidebar-menu>
              @for (item of navMain; track item.title) {
                @if (item.children?.length) {
                  <li
                    ui-sidebar-menu-item
                    ui-collapsible
                    [open]="isExpanded(item.title)"
                    (openChange)="setExpanded(item.title, $event)"
                  >
                    <button
                      type="button"
                      ui-sidebar-menu-button
                      ui-collapsible-trigger
                      [tooltip]="item.title"
                      [isActive]="isActive(item.title)"
                      (click)="select(item.title)"
                    >
                      <lucide-icon [img]="item.icon" class="size-4 shrink-0" />
                      <span class="flex-1 text-left">{{ item.title }}</span>
                      @if (item.badge) {
                        <span ui-badge variant="secondary">{{ item.badge }}</span>
                      }
                      <lucide-icon
                        [img]="ChevronRight"
                        class="size-4 shrink-0 transition-transform duration-200"
                        [class.rotate-90]="isExpanded(item.title)"
                      />
                    </button>
                    <ui-collapsible-content>
                      <ul ui-sidebar-menu-sub>
                        @for (child of item.children; track child.title) {
                          <li ui-sidebar-menu-sub-item>
                            <button
                              type="button"
                              ui-sidebar-menu-sub-button
                              [isActive]="isActive(child.title)"
                              (click)="select(child.title)"
                            >
                              <span>{{ child.title }}</span>
                            </button>
                          </li>
                        }
                      </ul>
                    </ui-collapsible-content>
                  </li>
                } @else {
                  <li ui-sidebar-menu-item>
                    <button
                      type="button"
                      ui-sidebar-menu-button
                      [tooltip]="item.title"
                      [isActive]="isActive(item.title)"
                      (click)="select(item.title)"
                    >
                      <lucide-icon [img]="item.icon" class="size-4 shrink-0" />
                      <span class="flex-1 text-left">{{ item.title }}</span>
                      @if (item.badge) {
                        <span ui-badge variant="secondary">{{ item.badge }}</span>
                      }
                    </button>
                  </li>
                }
              }
            </ul>
          </ui-sidebar-group>
    
          <ui-sidebar-group>
            <ui-sidebar-group-label>Stores</ui-sidebar-group-label>
            <ul ui-sidebar-menu>
              @for (store of stores; track store.title) {
                <li ui-sidebar-menu-item>
                  <button
                    type="button"
                    ui-sidebar-menu-button
                    [tooltip]="store.title"
                    [isActive]="isActive(store.title)"
                    (click)="select(store.title)"
                  >
                    <lucide-icon [img]="store.icon" class="size-4 shrink-0" />
                    <span class="flex-1 text-left">{{ store.title }}</span>
                  </button>
                </li>
              }
            </ul>
          </ui-sidebar-group>
    
          <ui-sidebar-group class="mt-auto">
            <ul ui-sidebar-menu>
              @for (item of navSecondary; track item.title) {
                <li ui-sidebar-menu-item>
                  <button
                    type="button"
                    ui-sidebar-menu-button
                    [tooltip]="item.title"
                    [isActive]="isActive(item.title)"
                    (click)="select(item.title)"
                  >
                    <lucide-icon [img]="item.icon" class="size-4 shrink-0" />
                    <span class="flex-1 text-left">{{ item.title }}</span>
                  </button>
                </li>
              }
            </ul>
          </ui-sidebar-group>
        </ui-sidebar-content>
    
        <ui-sidebar-footer>
          <ul ui-sidebar-menu>
            <li ui-sidebar-menu-item>
              <ui-dropdown-menu>
                <button
                  type="button"
                  ui-sidebar-menu-button
                  ui-dropdown-menu-trigger
                  size="lg"
                  class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
                >
                  <span
                    class="bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-bold group-data-[collapsible=icon]:size-6 group-data-[collapsible=icon]:text-[10px]"
                  >
                    JD
                  </span>
                  <span class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                    <span class="truncate font-semibold">Jane Doe</span>
                    <span class="text-muted-foreground truncate text-xs">jane&#64;acme.com</span>
                  </span>
                  <lucide-icon
                    [img]="ChevronsUpDown"
                    class="text-muted-foreground ml-auto size-4 group-data-[collapsible=icon]:hidden"
                  />
                </button>
                <ui-dropdown-menu-content
                  class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
                  [side]="sidebar.isMobile ? 'bottom' : 'right'"
                  align="end"
                  [sideOffset]="4"
                >
                  <ui-dropdown-menu-item><lucide-icon [img]="Settings" /> Account settings</ui-dropdown-menu-item>
                  <ui-dropdown-menu-item><lucide-icon [img]="Plus" /> Invite member</ui-dropdown-menu-item>
                  <ui-dropdown-menu-separator />
                  <ui-dropdown-menu-item><lucide-icon [img]="LogOut" /> Log out</ui-dropdown-menu-item>
                </ui-dropdown-menu-content>
              </ui-dropdown-menu>
            </li>
          </ul>
        </ui-sidebar-footer>
      `,
    })
    export class UiSalesSidebarNavComponent {
      readonly sidebar = injectSidebar()
      @Input() active = 'Dashboard'
      @Output() activeChange = new EventEmitter<string>()
    
      protected readonly navMain = NAV_MAIN
      protected readonly navSecondary = NAV_SECONDARY
      protected readonly stores = STORES
      protected readonly teams = TEAMS
    
      protected readonly Settings = Settings
      protected readonly Plus = Plus
      protected readonly Check = Check
      protected readonly ChevronsUpDown = ChevronsUpDown
      protected readonly ChevronRight = ChevronRight
      protected readonly LogOut = LogOut
    
      private readonly expanded = signal<string | null>('Orders')
      private readonly activeTeamSignal = signal<SalesTeam>(TEAMS[0]!)
    
      activeTeam(): SalesTeam {
        return this.activeTeamSignal()
      }
    
      isActive(title: string): boolean {
        return this.active === title
      }
    
      isExpanded(title: string): boolean {
        return this.expanded() === title
      }
    
      select(title: string): void {
        this.active = title
        this.activeChange.emit(title)
      }
    
      /** Accordion: opening a group closes the others. */
      setExpanded(title: string, open: boolean): void {
        this.expanded.set(open ? title : this.expanded() === title ? null : this.expanded())
      }
    
      toggleGroup(title: string): void {
        this.setExpanded(title, !this.isExpanded(title))
      }
    
      selectTeam(team: SalesTeam): void {
        this.activeTeamSignal.set(team)
      }
    }
    
    /**
     * Sales sidebar block: a `collapsible="icon"` Sidebar around the nav. Place it inside
     * <ui-sidebar-provider> next to <ui-sidebar-inset> (with a <ui-sidebar-trigger>) --
     * the primitive then handles the icon rail, Cmd/Ctrl+B and the mobile sheet.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-sales-sidebar, [ui-sales-sidebar]',
      standalone: true,
      // Renders <ui-sidebar> itself: keep this wrapper out of the provider's flex layout.
      host: { class: 'contents' },
      imports: [UiSidebarComponent, UiSidebarRailComponent, UiSalesSidebarNavComponent],
      template: `
        <ui-sidebar collapsible="icon">
          <ui-sales-sidebar-nav [active]="active" (activeChange)="activeChange.emit($event)" />
          <button ui-sidebar-rail></button>
        </ui-sidebar>
      `,
    })
    export class UiSalesSidebarComponent {
      @Input() active = 'Dashboard'
      @Output() activeChange = new EventEmitter<string>()
    }
    
  • src/app/components/blocks/sales-sidebar/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/sales-sidebar.json