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 shadcn@latest add https://uipkge.dev/r/lit/profile-menu.json
Manifest:https://uipkge.dev/r/lit/profile-menu.jsonInstalls to:src/uip/blocks/profile-menu/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
user-namestringAlex Morganoptional
user-emailstringalex@example.comoptional
user-avatarstring—optional

Schema

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

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-profile-menu': UipProfileMenu
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/profile-menu/profile-menu.ts5.8 kB
    import { LitElement, css, html } from 'lit'
    import { BadgeCheck, Bell, CreditCard, LogOut, Settings, Zap } 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/dropdown-menu/dropdown-menu'
    
    export type ProfileMenuKey = 'upgrade' | 'account' | 'billing' | 'notifications' | 'settings' | 'logout'
    
    /**
     * <uip-profile-menu> — the registry ProfileMenu block as a web component.
     *
     * Class strings are React's ProfileMenu verbatim. React's `trigger` render
     * prop becomes a `trigger` slot (default avatar button when empty);
     * `onSelect` becomes a `select` CustomEvent (bubbles, composed, detail = the
     * item key). `user` is split into `user-name` / `user-email` / `user-avatar`
     * attributes (`user` alone can't cross the HTML boundary).
     */
    export class UipProfileMenu 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: inline-block; }`]
    
      static properties = {
        userName: { attribute: 'user-name' },
        userEmail: { attribute: 'user-email' },
        userAvatar: { attribute: 'user-avatar' },
      }
    
      userName = 'Alex Morgan'
      userEmail = 'alex@example.com'
      userAvatar = ''
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'profile-menu')
      }
    
      private get initials() {
        return (
          this.userName
            .trim()
            .split(/\s+/)
            .slice(0, 2)
            .map((p) => p[0]?.toUpperCase())
            .join('') || 'U'
        )
      }
    
      private select(key: ProfileMenuKey) {
        this.dispatchEvent(new CustomEvent('select', { detail: key, bubbles: true, composed: true }))
      }
    
      render() {
        return html`
          <uip-dropdown-menu
            class="[&::part(content)]:w-60 [&::part(content)]:rounded-lg [&::part(user-label)]:p-0 [&::part(user-label)]:font-normal"
          >
            <slot name="trigger" slot="trigger">
              <button
                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"
              >
                <uip-avatar class="[&::part(base)]:size-7">
                  ${this.userAvatar
                    ? html`<uip-avatar-image src=${this.userAvatar} alt=${this.userName}></uip-avatar-image>`
                    : ''}
                  <uip-avatar-fallback class="text-xs font-semibold [&::part(base)]:bg-primary/10 [&::part(base)]:text-primary"
                    >${this.initials}</uip-avatar-fallback
                  >
                </uip-avatar>
              </button>
            </slot>
            <uip-dropdown-menu-content class="w-60 rounded-lg" align="end" side-offset="8">
              <uip-dropdown-menu-label part="user-label">
                <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
                  <uip-avatar class="[&::part(base)]:size-9 [&::part(base)]:rounded-lg">
                    ${this.userAvatar
                      ? html`<uip-avatar-image src=${this.userAvatar} alt=${this.userName}></uip-avatar-image>`
                      : ''}
                    <uip-avatar-fallback
                      class="text-xs font-semibold [&::part(base)]:rounded-lg [&::part(base)]:bg-primary/10 [&::part(base)]:text-primary"
                    >
                      ${this.initials}
                    </uip-avatar-fallback>
                  </uip-avatar>
                  <div class="grid flex-1 text-left leading-tight">
                    <span class="truncate text-sm font-semibold">${this.userName}</span>
                    <span class="text-muted-foreground truncate text-xs">${this.userEmail}</span>
                  </div>
                </div>
              </uip-dropdown-menu-label>
              <uip-dropdown-menu-separator></uip-dropdown-menu-separator>
              <uip-dropdown-menu-group>
                <uip-dropdown-menu-item @select=${() => this.select('upgrade')}>
                  ${icon(Zap, 'zap', 'size-4')} Upgrade to Pro
                </uip-dropdown-menu-item>
              </uip-dropdown-menu-group>
              <uip-dropdown-menu-separator></uip-dropdown-menu-separator>
              <uip-dropdown-menu-group>
                <uip-dropdown-menu-item @select=${() => this.select('account')}>
                  ${icon(BadgeCheck, 'badge-check', 'size-4')} Account
                </uip-dropdown-menu-item>
                <uip-dropdown-menu-item @select=${() => this.select('billing')}>
                  ${icon(CreditCard, 'credit-card', 'size-4')} Billing
                </uip-dropdown-menu-item>
                <uip-dropdown-menu-item @select=${() => this.select('notifications')}>
                  ${icon(Bell, 'bell', 'size-4')} Notifications
                </uip-dropdown-menu-item>
                <uip-dropdown-menu-item @select=${() => this.select('settings')}>
                  ${icon(Settings, 'settings', 'size-4')} Settings
                </uip-dropdown-menu-item>
              </uip-dropdown-menu-group>
              <uip-dropdown-menu-separator></uip-dropdown-menu-separator>
              <uip-dropdown-menu-item @select=${() => this.select('logout')}>
                ${icon(LogOut, 'log-out', 'size-4')} Log out
              </uip-dropdown-menu-item>
            </uip-dropdown-menu-content>
          </uip-dropdown-menu>
        `
      }
    }
    
    customElements.get('uip-profile-menu') || customElements.define('uip-profile-menu', UipProfileMenu)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-profile-menu': UipProfileMenu
      }
    }
    

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