import { type ComponentPropsWithRef, type FocusEvent, type KeyboardEvent, type PointerEvent, type ReactNode, useRef, useState, } from "react"; import * as format from "../../lib/format"; import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu"; import { cn } from "../../lib/utils"; type Props = Omit, "children"> & { /** Display name shown in the menu label and used for the avatar fallback. */ name: string; /** GitHub handle, when the current identity has been linked to GitHub. */ handle?: string; /** Avatar image URL; defaults to the GitHub avatar for `handle`. */ avatar?: string; /** Alignment for the dropdown content. */ align?: "start" | "center" | "end"; /** Side for the dropdown content. */ side?: "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end"; /** Fires when the Sign out item is selected. */ onSignOut?: () => void; /** Fires when the GitHub profile item is selected. */ onProfile?: () => void; /** Fires when the Settings item is selected. */ onSettings?: () => void; /** Fires when the Download app item is selected. */ onDownload?: () => void; /** Extra menu items rendered above the Sign out section. */ children?: ReactNode; }; /** Two-letter avatar fallback derived from the display name. */ function initials(name: string) { let parts = name.trim().split(/\s+/); let head = parts[0]?.[0] ?? ""; let tail = parts.length > 1 ? parts[parts.length - 1]![0] : ""; return (head + tail).toUpperCase() || "?"; } /** Avatar trigger that opens a dropdown with account actions. */ function UserMenu({ name, handle, avatar = handle && format.avatar(handle), align = "start", side = "bottom", onSignOut, onProfile, onSettings, onDownload, children, className, "aria-label": label = "Open account menu", onBlurCapture, onFocusCapture, onKeyDownCapture, onPointerDownCapture, ref, ...rest }: Props) { let [active, setActive] = useState(false); let pointer = useRef(false); let profile = onProfile || (handle ? () => window.open(`https://github.com/${handle}`, "_blank", "noopener,noreferrer") : undefined); function enter(event: FocusEvent) { let visible = event.currentTarget.matches(":focus-visible"); if (visible) pointer.current = false; setActive(visible); onFocusCapture?.(event); } function leave(event: FocusEvent) { setActive(false); onBlurCapture?.(event); } function press(event: PointerEvent) { pointer.current = true; onPointerDownCapture?.(event); } function key(event: KeyboardEvent) { pointer.current = false; onKeyDownCapture?.(event); } function focus() { let restore = !pointer.current; pointer.current = false; return restore; } return ( {avatar && } {initials(name)} GitHub profile Settings {onDownload ? Download app : null} {children} Sign out {name} ); } export { UserMenu };