Changeset 0.37.4 (#216)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 01:15:09 +00:00
committed by xcaliber
parent fc43618501
commit 05b5affdac
7 changed files with 294 additions and 4 deletions

View File

@@ -0,0 +1,79 @@
/**
* 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`
<button class="sw-user-menu__trigger" ref=${btnRef}
onClick=${() => setOpen(!open)} aria-label="User menu">
<${Avatar} src=${avatar} name=${displayName} size="sm" />
</button>
<${Menu}
open=${open}
anchor=${btnRef.current}
direction=${placement}
items=${items}
onSelect=${handleSelect}
onClose=${() => setOpen(false)}
/>
`;
}