/** * UserMenu — avatar button + flyout menu * * RBAC-gated items based on sw.can() / sw.isAdmin / sw.isTeamAdmin(). * Surfaces mount this wherever they want; the shell does not own placement. * * Props: * placement — Menu direction: 'down-right' (default) | 'down-left' * onAction — Override handler; if omitted, uses default routing */ const { html } = window; const { useState, useRef, useMemo } = hooks; import { Avatar } from '../primitives/avatar.js'; import { Menu } from '../primitives/menu.js'; export function UserMenu({ placement = 'down-right', onAction }) { const [open, setOpen] = useState(false); const btnRef = useRef(null); const sw = window.sw; const user = sw?.auth?.user; const authenticated = sw?.auth?.isAuthenticated; const items = useMemo(() => { const list = [ { label: 'Settings', action: 'settings', icon: '\u2699\ufe0f' }, ]; // Admin — requires admin role if (!authenticated || sw?.isAdmin) { list.push({ label: 'Admin', action: 'admin', icon: '\ud83d\udee1\ufe0f' }); } // Team Admin — requires admin role on at least one team const hasTeamAdmin = sw?.auth?.teams?.some(t => t.my_role === 'admin'); if (!authenticated || hasTeamAdmin) { list.push({ label: 'Team Admin', action: 'team-admin', icon: '\ud83d\udc65' }); } // Debug — admin only if (!authenticated || sw?.isAdmin) { list.push({ label: 'Debug', action: 'debug', icon: '\ud83d\udc1b' }); } list.push({ divider: true }); list.push({ label: 'Sign Out', action: 'sign-out' }); return list; }, [user, authenticated]); function handleSelect(action) { setOpen(false); if (onAction) return onAction(action); if (action === 'sign-out') { sw?.auth?.logout(); } else { sw?.emit('shell.navigate', { target: action }); } } const displayName = user?.display_name || user?.username || 'Demo User'; const avatar = user?.avatar || null; return html` <${Menu} open=${open} anchor=${btnRef.current} direction=${placement} items=${items} onSelect=${handleSelect} onClose=${() => setOpen(false)} /> `; }