Profile Menu
blocklayoutHeader-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
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/profile-menu.json$npx shadcn@latest add https://uipkge.dev/r/lit/profile-menu.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/profile-menu.json$bunx shadcn@latest add https://uipkge.dev/r/lit/profile-menu.jsonManifest:
https://uipkge.dev/r/lit/profile-menu.jsonInstalls to:src/uip/blocks/profile-menu/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
user-name | string | Alex Morgan | optional |
user-email | string | [email protected] | optional |
user-avatar | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-profile-menu': UipProfileMenu
}
}Includes
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 = '[email protected]' 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