Menu

Change language

GitHub repo

Dashboard Layout

blocklayout

Full dashboard shell — collapsible sidebar + sticky topbar (sidebar trigger, breadcrumb, theme switch, notifications) + main content projection. Drop it around a page: `<ui-dashboard-layout>page</ui-dashboard-layout>`.

Installation

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

Variants

Loading interactive previews…

Schema

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

DashboardCrumb
interface DashboardCrumb {
  label: string
  href?: string
}

Files installed (2)

  • src/app/components/blocks/dashboard-layout/dashboard-layout.component.ts7.3 kB
    // Full dashboard shell — collapsible sidebar + sticky topbar (sidebar trigger, breadcrumb,
    // command palette affordance, theme switch, notifications) + main content projection.
    // Drop it around a page: `<ui-dashboard-layout>page</ui-dashboard-layout>`.
    //
    // NOTE (Angular port): mirrors the React port 1:1 — the Vue original composes the
    // `sidebar-02`, `command-palette`, and `notifications-popover` blocks, which don't exist in
    // the Angular registry. Until they're ported, the sidebar body is a minimal inline nav and
    // notifications is a plain Bell button. Swap them for the dedicated blocks once available.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { Bell, LayoutDashboard, LucideAngularModule, Settings, Users } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import {
      UiBreadcrumbComponent,
      UiBreadcrumbItemComponent,
      UiBreadcrumbLinkComponent,
      UiBreadcrumbListComponent,
      UiBreadcrumbPageComponent,
      UiBreadcrumbSeparatorComponent,
    } from '@/ui/breadcrumb/breadcrumb.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiSidebarComponent,
      UiSidebarContentComponent,
      UiSidebarGroupComponent,
      UiSidebarGroupContentComponent,
      UiSidebarHeaderComponent,
      UiSidebarInsetComponent,
      UiSidebarMenuButtonComponent,
      UiSidebarMenuComponent,
      UiSidebarMenuItemComponent,
      UiSidebarProviderComponent,
      UiSidebarRailComponent,
      UiSidebarTriggerComponent,
    } from '@/ui/sidebar/sidebar.component'
    import { UiThemeSwitchComponent } from '@/ui/theme-switch/theme-switch.component'
    
    export interface DashboardCrumb {
      label: string
      href?: string
    }
    
    const NAV = [
      { label: 'Dashboard', icon: LayoutDashboard },
      { label: 'People', icon: Users },
      { label: 'Settings', icon: Settings },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-dashboard-layout, [ui-dashboard-layout]',
      standalone: true,
      // React renders the provider itself: the host stays out of layout and `class` goes to the provider.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBreadcrumbComponent,
        UiBreadcrumbItemComponent,
        UiBreadcrumbLinkComponent,
        UiBreadcrumbListComponent,
        UiBreadcrumbPageComponent,
        UiBreadcrumbSeparatorComponent,
        UiButtonComponent,
        UiSeparatorComponent,
        UiSidebarComponent,
        UiSidebarContentComponent,
        UiSidebarGroupComponent,
        UiSidebarGroupContentComponent,
        UiSidebarHeaderComponent,
        UiSidebarInsetComponent,
        UiSidebarMenuButtonComponent,
        UiSidebarMenuComponent,
        UiSidebarMenuItemComponent,
        UiSidebarProviderComponent,
        UiSidebarRailComponent,
        UiSidebarTriggerComponent,
        UiThemeSwitchComponent,
      ],
      template: `
        <!-- UiSidebarProvider hardcodes data-slot="sidebar-wrapper" on its host, so the block's
             data-slot lives on this root (matches React's provider-level slot for consumers). -->
        <div data-slot="dashboard-layout" [class]="rootClass">
          <ui-sidebar-provider [class]="rootClass">
            <a
              href="#main-content"
              class="bg-background text-foreground ring-ring sr-only z-50 rounded-md text-sm font-medium shadow-md ring-2 focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:px-3 focus:py-2"
              >Skip to content</a
            >
            <ui-sidebar collapsible="icon">
              <ui-sidebar-header />
              <ui-sidebar-content>
                <ui-sidebar-group>
                  <ui-sidebar-group-content>
                    <ui-sidebar-menu>
                      @for (item of nav; track item.label) {
                        <ui-sidebar-menu-item>
                          <ui-sidebar-menu-button [tooltip]="item.label">
                            <lucide-icon [img]="item.icon" />
                            <span>{{ item.label }}</span>
                          </ui-sidebar-menu-button>
                        </ui-sidebar-menu-item>
                      }
                    </ui-sidebar-menu>
                  </ui-sidebar-group-content>
                </ui-sidebar-group>
              </ui-sidebar-content>
              <ui-sidebar-rail />
            </ui-sidebar>
            <ui-sidebar-inset>
              <header
                class="bg-background/80 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between border-b px-4 backdrop-blur-xl transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
              >
                <div class="flex items-center gap-2">
                  <ui-sidebar-trigger class="-ml-1" />
                  <ui-separator orientation="vertical" class="mr-2 h-4" />
                  <ui-breadcrumb>
                    <ui-breadcrumb-list>
                      @for (crumb of crumbs; track crumb.label; let i = $index; let last = $last) {
                        <ui-breadcrumb-item [class]="i === 0 ? 'hidden md:block' : ''">
                          @if (crumb.href && !last) {
                            <ui-breadcrumb-link
                              [href]="crumb.href"
                              class="text-muted-foreground/70 hover:text-foreground transition-colors"
                            >
                              {{ crumb.label }}
                            </ui-breadcrumb-link>
                          } @else {
                            <ui-breadcrumb-page class="font-medium">{{ crumb.label }}</ui-breadcrumb-page>
                          }
                        </ui-breadcrumb-item>
                        @if (!last) {
                          <ui-breadcrumb-separator [class]="i === 0 ? 'hidden md:block' : ''" />
                        }
                      }
                    </ui-breadcrumb-list>
                  </ui-breadcrumb>
                </div>
                <div class="flex items-center gap-1 px-2 sm:gap-3">
                  <div class="flex items-center gap-0.5">
                    <ui-theme-switch variant="icon-only" />
                    <button
                      ui-button
                      variant="ghost"
                      size="icon"
                      class="text-muted-foreground hover:text-foreground relative size-8 rounded-lg"
                      aria-label="Notifications"
                    >
                      <lucide-icon [img]="Bell" class="size-4" />
                    </button>
                    <!-- ProfileMenu removed from header: the sidebar's NavUser
                       footer already owns the profile/account affordance. The
                       block still accepts a user input so consumers can wire
                       it into the sidebar or a custom slot. -->
                  </div>
                </div>
              </header>
              <main id="main-content" tabindex="-1" class="flex flex-1 flex-col px-4 pt-4 pb-4 outline-none lg:px-6 lg:pt-6 lg:pb-6">
                <ng-content />
              </main>
            </ui-sidebar-inset>
          </ui-sidebar-provider>
        </div>
      `,
    })
    export class UiDashboardLayoutComponent {
      protected readonly Bell = Bell
      protected readonly nav = NAV.map((n) => ({ ...n }))
    
      @Input() breadcrumbs: DashboardCrumb[] = [{ label: 'Dashboard' }]
      @Input() user: { name: string; email: string; avatar?: string } = { name: 'Alex Morgan', email: 'alex@example.com' }
      @Input('class') className?: string
    
      get crumbs(): DashboardCrumb[] {
        return this.breadcrumbs?.length ? this.breadcrumbs : [{ label: 'Dashboard' }]
      }
    
      get rootClass(): string {
        return cn(this.className)
      }
    }
    
  • src/app/components/blocks/dashboard-layout/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/dashboard-layout.json