Menu

Change language

GitHub repo

Profile Menu

blocklayout

Header-grade profile/account dropdown. Slot-trigger DropdownMenu the consumer wraps around their own avatar button (default trigger ships an Avatar fallback). Sections: upgrade, account/billing/notifications/settings, log out. Emits `select` with the chosen item key so consumers wire navigation and auth themselves.

Installation

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

Variants

Loading interactive previews…

Schema

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

ProfileMenuUser
interface ProfileMenuUser {
  name: string
  email: string
  avatar?: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/profile-menu/profile-menu.component.ts5.6 kB
    // Header-grade profile/account dropdown: default Avatar trigger (consumers can project their
    // own button), sections for upgrade / account / billing / notifications / settings / log out.
    // Emits `select` with the chosen item key. Port of the Vue block 1:1.
    import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'
    import { BadgeCheck, Bell, CreditCard, LogOut, LucideAngularModule, Settings, Zap } from 'lucide-angular'
    import { UiAvatarFallbackComponent, UiAvatarImageComponent, UiAvatarComponent } from '@/ui/avatar/avatar.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuGroupComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    
    export interface ProfileMenuUser {
      name: string
      email: string
      avatar?: string
    }
    
    export type ProfileMenuKey = 'upgrade' | 'account' | 'billing' | 'notifications' | 'settings' | 'logout'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-profile-menu, [ui-profile-menu]',
      standalone: true,
      // Vue renders the DropdownMenu root itself: the host stays out of layout.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiAvatarImageComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuGroupComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
      ],
      template: `
        <ui-dropdown-menu data-slot="profile-menu" [open]="open()" (openChange)="open.set($event)">
          <button
            ui-dropdown-menu-trigger
            type="button"
            class="hover:bg-accent focus-visible:ring-ring flex size-8 items-center justify-center rounded-lg transition-colors focus-visible:ring-1 focus-visible:outline-none"
            aria-label="Open profile menu"
          >
            <ng-content>
              <ui-avatar class="size-7">
                @if (user().avatar) {
                  <ui-avatar-image [src]="user().avatar!" [alt]="user().name" />
                }
                <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-semibold">
                  {{ initials() }}
                </ui-avatar-fallback>
              </ui-avatar>
            </ng-content>
          </button>
          <ui-dropdown-menu-content class="w-60 rounded-lg" 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-9 rounded-lg">
                  @if (user().avatar) {
                    <ui-avatar-image [src]="user().avatar!" [alt]="user().name" />
                  }
                  <ui-avatar-fallback class="bg-primary/10 text-primary rounded-lg text-xs font-semibold">
                    {{ initials() }}
                  </ui-avatar-fallback>
                </ui-avatar>
                <div class="grid flex-1 text-left leading-tight">
                  <span class="truncate text-sm font-semibold">{{ user().name }}</span>
                  <span class="text-muted-foreground truncate text-xs">{{ user().email }}</span>
                </div>
              </div>
            </ui-dropdown-menu-label>
            <ui-dropdown-menu-separator />
            <ui-dropdown-menu-group>
              <ui-dropdown-menu-item (select)="handleSelect('upgrade')">
                <lucide-icon [img]="Zap" class="size-4" />
                Upgrade to Pro
              </ui-dropdown-menu-item>
            </ui-dropdown-menu-group>
            <ui-dropdown-menu-separator />
            <ui-dropdown-menu-group>
              <ui-dropdown-menu-item (select)="handleSelect('account')">
                <lucide-icon [img]="BadgeCheck" class="size-4" />
                Account
              </ui-dropdown-menu-item>
              <ui-dropdown-menu-item (select)="handleSelect('billing')">
                <lucide-icon [img]="CreditCard" class="size-4" />
                Billing
              </ui-dropdown-menu-item>
              <ui-dropdown-menu-item (select)="handleSelect('notifications')">
                <lucide-icon [img]="Bell" class="size-4" />
                Notifications
              </ui-dropdown-menu-item>
              <ui-dropdown-menu-item (select)="handleSelect('settings')">
                <lucide-icon [img]="Settings" class="size-4" />
                Settings
              </ui-dropdown-menu-item>
            </ui-dropdown-menu-group>
            <ui-dropdown-menu-separator />
            <ui-dropdown-menu-item (select)="handleSelect('logout')">
              <lucide-icon [img]="LogOut" class="size-4" />
              Log out
            </ui-dropdown-menu-item>
          </ui-dropdown-menu-content>
        </ui-dropdown-menu>
      `,
    })
    export class UiProfileMenuComponent {
      protected readonly BadgeCheck = BadgeCheck
      protected readonly Bell = Bell
      protected readonly CreditCard = CreditCard
      protected readonly LogOut = LogOut
      protected readonly Settings = Settings
      protected readonly Zap = Zap
    
      private readonly _user = signal<ProfileMenuUser>({ name: 'Alex Morgan', email: '[email protected]', avatar: '' })
      readonly user = this._user.asReadonly()
    
      @Input('user') set userInput(v: ProfileMenuUser | undefined) {
        if (v) this._user.set(v)
      }
    
      @Output() select = new EventEmitter<ProfileMenuKey>()
    
      readonly open = signal(false)
    
      readonly initials = computed(() => {
        const parts = this._user().name.trim().split(/\s+/).slice(0, 2)
        return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U'
      })
    
      handleSelect(key: ProfileMenuKey): void {
        this.select.emit(key)
      }
    }
    
  • src/app/components/blocks/profile-menu/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/profile-menu.json