// The account chip at the foot of the sidebar, and the menu it opens.
// Asked for 2026-09-15 with ChatGPT's sidebar as the reference: an avatar with
// the name and what kind of account it is, opening upward into everything that
// used to be a loose row of links.
//
// The menu holds what the dark banner used to: the guides, the lab-only pages,
// the admin link, and the way in or out. Keyboard and outside-click handling
// follow ConstructPicker, which is the house pattern for a popup.
import { useEffect, useRef, useState } from "react";
const ROLE_LABELS = {
external: "External user",
lab: "Lab member",
maintainer: "Maintainer",
pi: "Principal investigator",
};
// "Deva Anand" -> "DA"; "mohammad" -> "M". Two letters at most, because three
// stop being legible at 26px.
function initials(name) {
const parts = (name || "").trim().split(/\s+/).filter(Boolean);
if (!parts.length) return "";
return (parts[0][0] + (parts.length > 1 ? parts[parts.length - 1][0] : "")).toUpperCase();
}
function Icon({ d, circle }) {
return (
);
}
const ICONS = {
about: ,
guide: ,
testing: ,
product: ,
admin: ,
signout: ,
signin: ,
};
export default function AccountMenu({ auth, onAbout, onSignIn, onSignOut }) {
const [open, setOpen] = useState(false);
const rootRef = useRef(null);
const buttonRef = useRef(null);
useEffect(() => {
if (!open) return undefined;
const onDown = (e) => {
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
};
const onKey = (e) => {
if (e.key !== "Escape") return;
// Stop here: the drawer also closes on Escape, and dismissing both with
// one key would leave you looking at a workspace you did not ask for.
e.stopPropagation();
setOpen(false);
buttonRef.current?.focus();
};
document.addEventListener("mousedown", onDown);
document.addEventListener("keydown", onKey, true);
return () => {
document.removeEventListener("mousedown", onDown);
document.removeEventListener("keydown", onKey, true);
};
}, [open]);
const signedIn = !!auth?.signed_in;
const name = signedIn ? auth.name : "Not signed in";
// The line under the name is ChatGPT's plan slot. Ours says what kind of
// account this is, which is the thing that changes what you can do here.
const subtitle = signedIn
? ROLE_LABELS[auth.role] || "Account"
: "Sign in to keep your work";
const run = (fn) => () => {
setOpen(false);
fn?.();
};
return (