Menu

Change language

GitHub repo

Dual-Rail Sidebar

blocklayout

Dual-rail sidebar -- a thin always-visible icon column on the far left holds quick shortcuts (a subset of the full nav), paired with a wider main panel for the brand, search, sections, and footer. Uses collapsible="icon": when the user collapses, only the rail stays. On mobile the kit Sheet shows the full dual rail. Requires the parent SidebarProvider to set both widths: style="--sidebar-width: 19.5rem; --sidebar-width-icon: 3.5rem". Modeled on the GR8R HRMS admin shell.

Installation

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

Variants

Loading interactive previews…

Schema

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

RailItem
interface RailItem {
  id: RailId
  icon: LucideIconData
  label: string
}

Files installed (2)

  • src/app/components/blocks/sidebar-05/sidebar-05.component.ts29.4 kB
    // Dual-rail sidebar with master/detail behavior. The thin rail picks the section; the
    // main panel renders the rows for the active section. One <ui-sidebar collapsible="icon">
    // contains rail + panel as inline flex siblings.
    //
    // Desktop: when collapsed (trigger clicked), the kit shrinks the sidebar to
    // --sidebar-width-icon. Set that to 3.5rem so only the rail stays. The panel is hidden
    // via group-data-[collapsible=icon] which only fires on the desktop branch of <ui-sidebar>
    // (the mobile sheet has no `group` ancestor with that attr, so mobile always shows rail+panel).
    //
    // IMPORTANT: the parent <ui-sidebar-provider> must set BOTH custom widths, e.g.
    // <ui-sidebar-provider style="--sidebar-width: 19.5rem; --sidebar-width-icon: 3.5rem">
    // The vars set on <ui-sidebar> itself are too deep -- the layout-allocating wrapper sits
    // above it and reads the provider's vars.
    //
    // To wire to your router, replace the signal-based switch with route matches
    // (e.g. set active = 'projects' on /projects/*).
    //
    // Port of the React block 1:1.
    import { AfterViewInit, ChangeDetectionStrategy, Component, signal } from '@angular/core'
    import {
      Bell,
      Briefcase,
      Calendar,
      ChartLine,
      ChevronsUpDown,
      ClipboardList,
      CreditCard,
      FileText,
      Folder,
      Gauge,
      Headphones,
      HelpCircle,
      Home,
      Inbox,
      LayoutDashboard,
      Lock,
      LucideAngularModule,
      Mail,
      MessageSquare,
      Moon,
      Search,
      Send,
      Settings as SettingsIcon,
      Star,
      Sun,
      Users,
      type LucideIconData,
    } from 'lucide-angular'
    import { injectTheme } from '@/lib/use-theme'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiSidebarComponent,
      UiSidebarContentComponent,
      UiSidebarGroupComponent,
      UiSidebarGroupContentComponent,
      UiSidebarGroupLabelComponent,
      UiSidebarHeaderComponent,
      UiSidebarMenuButtonComponent,
      UiSidebarMenuComponent,
      UiSidebarMenuItemComponent,
      injectSidebar,
    } from '@/ui/sidebar/sidebar.component'
    
    type RailId = 'home' | 'projects' | 'inbox' | 'messages' | 'support' | 'settings'
    
    interface RailItem {
      id: RailId
      icon: LucideIconData
      label: string
    }
    
    const rail: RailItem[] = [
      { id: 'home', icon: Home, label: 'Home' },
      { id: 'projects', icon: Folder, label: 'Projects' },
      { id: 'inbox', icon: Bell, label: 'Inbox' },
      { id: 'messages', icon: MessageSquare, label: 'Messages' },
    ]
    const railLower: RailItem[] = [
      { id: 'support', icon: Headphones, label: 'Support' },
      { id: 'settings', icon: SettingsIcon, label: 'Settings' },
    ]
    
    const titles: Record<RailId, string> = {
      home: 'Workspace',
      projects: 'Projects',
      inbox: 'Inbox',
      messages: 'Messages',
      support: 'Support',
      settings: 'Settings',
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-sidebar-05, [ui-sidebar-05]',
      standalone: true,
      // React renders <Sidebar> itself: keep this wrapper out of the provider's flex layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuTriggerComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuItemComponent,
        UiInputComponent,
        UiSidebarComponent,
        UiSidebarHeaderComponent,
        UiSidebarContentComponent,
        UiSidebarGroupComponent,
        UiSidebarGroupLabelComponent,
        UiSidebarGroupContentComponent,
        UiSidebarMenuComponent,
        UiSidebarMenuItemComponent,
        UiSidebarMenuButtonComponent,
      ],
      template: `
        <!-- Single Sidebar primitive containing BOTH rails inline. On desktop the rail (56px)
             stays visible when collapsed thanks to collapsible="icon" + --sidebar-width-icon:
             3.5rem; the panel hides only when state=collapsed. On mobile the kit sheet renders
             the entire dual rail inside a single sheet pane. -->
        <ui-sidebar collapsible="icon">
          <div class="flex h-full w-full">
            <!-- Thin icon rail -- 56px wide. Clicking an icon updates "active"
                 which swaps the main panel content below. -->
            <aside class="border-sidebar-border flex w-14 shrink-0 flex-col items-center gap-0.5 border-r py-2">
              <button
                type="button"
                aria-label="uipkge"
                class="bg-sidebar-primary text-sidebar-primary-foreground mb-1 flex aspect-square size-9 items-center justify-center rounded-lg shadow-sm ring-1 ring-white/10 transition-transform ring-inset hover:scale-105"
              >
                <svg viewBox="0 0 24 24" class="size-4" 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>
              </button>
    
              @for (item of rail; track item.id) {
                <button
                  type="button"
                  [attr.aria-label]="item.label"
                  [attr.aria-current]="active() === item.id ? 'page' : null"
                  [class]="railButtonClass(item.id)"
                  (click)="selectRail(item.id)"
                >
                  @if (active() === item.id) {
                    <span class="bg-sidebar-primary absolute top-1/2 left-0 h-5 w-0.5 -translate-y-1/2 rounded-r-full"></span>
                  }
                  <lucide-icon [img]="item.icon" class="size-4" />
                </button>
              }
    
              <div class="bg-sidebar-border/70 my-2 h-px w-7"></div>
    
              @for (item of railLower; track item.id) {
                <button
                  type="button"
                  [attr.aria-label]="item.label"
                  [attr.aria-current]="active() === item.id ? 'page' : null"
                  [class]="railButtonClass(item.id)"
                  (click)="selectRail(item.id)"
                >
                  @if (active() === item.id) {
                    <span class="bg-sidebar-primary absolute top-1/2 left-0 h-5 w-0.5 -translate-y-1/2 rounded-r-full"></span>
                  }
                  <lucide-icon [img]="item.icon" class="size-4" />
                </button>
              }
    
              <div class="mt-auto flex flex-col items-center gap-1.5">
                <button
                  type="button"
                  [attr.aria-label]="isDark ? 'Switch to light theme' : 'Switch to dark theme'"
                  class="text-muted-foreground hover:bg-sidebar-accent/60 hover:text-foreground flex aspect-square size-9 items-center justify-center rounded-lg transition-colors"
                  (click)="toggleTheme()"
                >
                  @if (isDark) {
                    <lucide-icon [img]="Sun" class="size-4" />
                  } @else {
                    <lucide-icon [img]="Moon" class="size-4" />
                  }
                </button>
    
                <ui-dropdown-menu>
                  <button
                    type="button"
                    ui-dropdown-menu-trigger
                    aria-label="James Carter"
                    class="ring-offset-sidebar focus-visible:ring-ring data-[state=open]:ring-ring rounded-full transition focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none data-[state=open]:ring-2"
                  >
                    <ui-avatar class="size-8">
                      <ui-avatar-fallback class="bg-sidebar-accent text-sidebar-accent-foreground text-xs"
                        >JC</ui-avatar-fallback
                      >
                    </ui-avatar>
                  </button>
                  <ui-dropdown-menu-content
                    class="min-w-56 rounded-lg"
                    [side]="sidebar.isMobile ? 'top' : 'right'"
                    align="end"
                    [sideOffset]="8"
                  >
                    <ui-dropdown-menu-label class="p-0 font-normal">
                      <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
                        <ui-avatar class="size-8 rounded-lg">
                          <ui-avatar-fallback class="bg-sidebar-accent text-sidebar-accent-foreground rounded-lg text-xs"
                            >JC</ui-avatar-fallback
                          >
                        </ui-avatar>
                        <div class="grid flex-1 leading-tight">
                          <span class="truncate font-medium">James Carter</span>
                          <span class="text-muted-foreground truncate text-xs">Admin</span>
                        </div>
                      </div>
                    </ui-dropdown-menu-label>
                    <ui-dropdown-menu-separator />
                    <ui-dropdown-menu-item>
                      <lucide-icon [img]="Send" class="size-4" /> Invite teammates
                    </ui-dropdown-menu-item>
                    <ui-dropdown-menu-item>
                      <lucide-icon [img]="SettingsIcon" class="size-4" /> Workspace settings
                    </ui-dropdown-menu-item>
                  </ui-dropdown-menu-content>
                </ui-dropdown-menu>
              </div>
            </aside>
    
            <!-- Main panel -- header is the same for every section; the content area swaps
                 based on "active". Each section's rows are spelled out inline so consumers can
                 edit them in place. Hidden on desktop when state=collapsed (rail stays). -->
            <div class="flex min-w-0 flex-1 flex-col group-data-[collapsible=icon]:hidden">
              <ui-sidebar-header>
                <ul ui-sidebar-menu>
                  <li ui-sidebar-menu-item>
                    <button type="button" ui-sidebar-menu-button size="lg">
                      <div
                        class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg"
                      >
                        <svg viewBox="0 0 24 24" class="size-4" 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">
                        <span class="font-display truncate font-bold">uipkge</span>
                        <span class="text-muted-foreground truncate text-xs">{{ titles[active()] }}</span>
                      </div>
                      <lucide-icon [img]="ChevronsUpDown" class="ml-auto size-4" />
                    </button>
                  </li>
                </ul>
    
                <div class="px-1 pt-1">
                  <ui-input
                    type="text"
                    placeholder="Search..."
                    allowClear
                    [prefix]="searchPrefix"
                    [suffix]="searchSuffix"
                    class="bg-background h-8 w-full shadow-none"
                  />
                  <ng-template #searchPrefix>
                    <lucide-icon [img]="Search" class="text-muted-foreground size-3.5" />
                  </ng-template>
                  <ng-template #searchSuffix>
                    <kbd
                      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>
                  </ng-template>
                </div>
              </ui-sidebar-header>
    
              <ui-sidebar-content>
                <!-- HOME: full workspace nav -->
                @if (active() === 'home') {
                  <ui-sidebar-group>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button isActive>
                            <lucide-icon [img]="LayoutDashboard" class="size-4" />
                            <span>Dashboard</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Users" class="size-4" />
                            <span>Employees</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Briefcase" class="size-4" />
                            <span>Clients</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Folder" class="size-4" />
                            <span>Projects</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
    
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Workforce</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Calendar" class="size-4" />
                            <span>Calendar</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="ClipboardList" class="size-4" />
                            <span>Attendance</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Mail" class="size-4" />
                            <span>Mail</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="ChartLine" class="size-4" />
                            <span>Analytics</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                } @else if (active() === 'projects') {
                  <!-- PROJECTS: pinned and recent projects -->
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Pinned</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <div
                              class="bg-chart-1 flex aspect-square size-5 shrink-0 items-center justify-center rounded-md text-white"
                            >
                              <lucide-icon [img]="Folder" class="size-3" />
                            </div>
                            <span>Shift Planner</span>
                            <lucide-icon [img]="Star" class="text-warning ml-auto size-3.5 fill-current" />
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <div
                              class="bg-chart-2 flex aspect-square size-5 shrink-0 items-center justify-center rounded-md text-white"
                            >
                              <lucide-icon [img]="Folder" class="size-3" />
                            </div>
                            <span>Training Portal</span>
                            <lucide-icon [img]="Star" class="text-warning ml-auto size-3.5 fill-current" />
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <div
                              class="bg-chart-3 flex aspect-square size-5 shrink-0 items-center justify-center rounded-md text-white"
                            >
                              <lucide-icon [img]="Folder" class="size-3" />
                            </div>
                            <span>Performance Hub</span>
                            <lucide-icon [img]="Star" class="text-warning ml-auto size-3.5 fill-current" />
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <div
                              class="bg-chart-4 flex aspect-square size-5 shrink-0 items-center justify-center rounded-md text-white"
                            >
                              <lucide-icon [img]="Folder" class="size-3" />
                            </div>
                            <span>Expense Claims</span>
                            <lucide-icon [img]="Star" class="text-warning ml-auto size-3.5 fill-current" />
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
    
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Recent</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Folder" class="size-4" />
                            <span>Onboarding 2026</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Folder" class="size-4" />
                            <span>Q1 hiring plan</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Folder" class="size-4" />
                            <span>Compliance refresh</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                } @else if (active() === 'inbox') {
                  <!-- INBOX: notifications list -->
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Today</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Inbox" class="size-4" />
                            <span>3 PTO requests pending</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Bell" class="size-4" />
                            <span>Payroll cycle closes today</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
    
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>This week</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="FileText" class="size-4" />
                            <span>5 contracts awaiting sign-off</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Users" class="size-4" />
                            <span>2 new hires start Monday</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                } @else if (active() === 'messages') {
                  <!-- MESSAGES: conversations -->
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Direct messages</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <ui-avatar class="size-5">
                              <ui-avatar-fallback class="bg-chart-1/15 text-foreground text-xs">OB</ui-avatar-fallback>
                            </ui-avatar>
                            <span>Olivia Bennett</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <ui-avatar class="size-5">
                              <ui-avatar-fallback class="bg-chart-2/15 text-foreground text-xs">DM</ui-avatar-fallback>
                            </ui-avatar>
                            <span>Daniel Morgan</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <ui-avatar class="size-5">
                              <ui-avatar-fallback class="bg-chart-3/15 text-foreground text-xs">ER</ui-avatar-fallback>
                            </ui-avatar>
                            <span>Ethan Reynolds</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
    
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Channels</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="MessageSquare" class="size-4" />
                            <span># announcements</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="MessageSquare" class="size-4" />
                            <span># hiring</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                } @else if (active() === 'support') {
                  <!-- SUPPORT: help links -->
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Help center</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="HelpCircle" class="size-4" />
                            <span>Getting started</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="FileText" class="size-4" />
                            <span>Onboarding guide</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Send" class="size-4" />
                            <span>Contact support</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                } @else if (active() === 'settings') {
                  <!-- SETTINGS: workspace settings -->
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Workspace</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="SettingsIcon" class="size-4" />
                            <span>General</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Users" class="size-4" />
                            <span>Members</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="CreditCard" class="size-4" />
                            <span>Billing</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Gauge" class="size-4" />
                            <span>Limits</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
    
                  <ui-sidebar-group>
                    <ui-sidebar-group-label>Account</ui-sidebar-group-label>
                    <ui-sidebar-group-content>
                      <ul ui-sidebar-menu>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Lock" class="size-4" />
                            <span>Security</span>
                          </button>
                        </li>
                        <li ui-sidebar-menu-item>
                          <button type="button" ui-sidebar-menu-button>
                            <lucide-icon [img]="Bell" class="size-4" />
                            <span>Notifications</span>
                          </button>
                        </li>
                      </ul>
                    </ui-sidebar-group-content>
                  </ui-sidebar-group>
                }
              </ui-sidebar-content>
            </div>
          </div>
        </ui-sidebar>
      `,
    })
    export class UiSidebar05Component implements AfterViewInit {
      readonly sidebar = injectSidebar()
      readonly theme = injectTheme()
    
      readonly rail = rail
      readonly railLower = railLower
      readonly titles = titles
      readonly active = signal<RailId>('home')
    
      // `mounted` stays false during SSR + the client's first synchronous render, so the
      // template's isDark is also false on both sides -- no hydration mismatch on the
      // Sun/Moon icon. ngAfterViewInit only runs after a real browser render (never in plain
      // instantiation), like Vue's onMounted / React's useEffect.
      private readonly mounted = signal(false)
    
      ngAfterViewInit(): void {
        this.mounted.set(true)
      }
    
      protected readonly Search = Search
      protected readonly Sun = Sun
      protected readonly Moon = Moon
      protected readonly Send = Send
      protected readonly SettingsIcon = SettingsIcon
      protected readonly ChevronsUpDown = ChevronsUpDown
      protected readonly LayoutDashboard = LayoutDashboard
      protected readonly Users = Users
      protected readonly Briefcase = Briefcase
      protected readonly Folder = Folder
      protected readonly Calendar = Calendar
      protected readonly ClipboardList = ClipboardList
      protected readonly Mail = Mail
      protected readonly ChartLine = ChartLine
      protected readonly Inbox = Inbox
      protected readonly Bell = Bell
      protected readonly FileText = FileText
      protected readonly MessageSquare = MessageSquare
      protected readonly HelpCircle = HelpCircle
      protected readonly CreditCard = CreditCard
      protected readonly Gauge = Gauge
      protected readonly Lock = Lock
      protected readonly Star = Star
    
      get isDark(): boolean {
        // Template icon: stays Moon until the browser render settles (mounted guard).
        if (!this.mounted()) return false
        return this.resolvedDark()
      }
    
      private resolvedDark(): boolean {
        const t = this.theme.theme()
        if (t === 'dark') return true
        if (t === 'light') return false
        return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches
      }
    
      railButtonClass(id: RailId): string {
        const base = 'relative flex aspect-square size-9 items-center justify-center rounded-lg transition-colors'
        return this.active() === id
          ? `${base} bg-sidebar-accent text-foreground`
          : `${base} text-muted-foreground hover:bg-sidebar-accent/60 hover:text-foreground`
      }
    
      // Slack-style rail click: expand the panel if collapsed, or collapse if the user clicks
      // the already-active icon while expanded. On mobile the sheet open/close is owned by the
      // inset trigger, so just swap active.
      selectRail(id: RailId): void {
        if (this.sidebar.isMobile) {
          this.active.set(id)
          return
        }
        if (this.sidebar.state === 'expanded' && this.active() === id) {
          this.sidebar.setOpen(false)
          return
        }
        this.active.set(id)
        this.sidebar.setOpen(true)
      }
    
      toggleTheme(): void {
        this.theme.setTheme(this.resolvedDark() ? 'light' : 'dark')
      }
    }
    
  • src/app/components/blocks/sidebar-05/index.ts0.1 kB

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