import { Dialog } from "radix-ui";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { Button, cn, Count, Icon, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, SolidBadge, Tip } from "../kit";
import { Link, navigate } from "../router";
import { displayName, useClinician } from "../session";
import { Brand } from "../ui";
import { modKey, navItems, type NavId, type NavItem } from "./nav";
type NavProps = { active: NavId; urgent: number };
const focusRing = "outline-none focus-visible:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-brand)]";
const groups = [
{ id: "workspace", label: "Workspace", visible: false },
{ id: "guides", label: "Guides", visible: true },
] as const;
/** The clinic, who is signed in, Settings, Lock now and Sign out. */
function ClinicMenu({ rail = false }: { rail?: boolean }) {
const { me, clinic, lock, signOut } = useClinician();
const initial = clinic.name.trim().charAt(0).toUpperCase() || "C";
const trigger = rail ? (
<button type="button" aria-label={`${clinic.name}: clinic menu`} className={cn("flex size-[30px] cursor-pointer items-center justify-center rounded-lg border-0 bg-action p-0 font-sans text-[13px] font-semibold text-white", focusRing)}>
{initial}
</button>
) : (
<button
type="button"
className={cn(
"flex h-[34px] w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-2 text-left font-sans transition-colors hover:bg-quiet-hover data-[state=open]:bg-line",
focusRing,
)}
>
<span aria-hidden="true" className="flex size-5 shrink-0 items-center justify-center rounded-[5px] bg-action text-[11px] font-semibold text-white">
{initial}
</span>
<span className="min-w-0 flex-1 truncate text-[14px] font-medium text-ink">{clinic.name}</span>
<Icon name="expand-up-down-line" size={16} className="text-muted" />
</button>
);
return (
<Menu>
{rail ? (
<Tip label={clinic.name} side="right">
<MenuTrigger asChild>{trigger}</MenuTrigger>
</Tip>
) : (
<MenuTrigger asChild>{trigger}</MenuTrigger>
)}
<MenuContent align="start" side={rail ? "right" : "bottom"} className="w-[264px] p-1.5">
<div className="flex gap-2.5 px-1.5 pt-1.5 pb-3">
<span aria-hidden="true" className="flex size-9 shrink-0 items-center justify-center rounded-[9px] bg-action text-[16px] font-semibold text-white">
{initial}
</span>
<span className="flex min-w-0 flex-col gap-0.5">
<strong className="truncate text-[14px] leading-[18px] font-semibold text-ink">{clinic.name}</strong>
<span className="truncate text-[12px] leading-4 text-muted">{displayName(me)}</span>
{me.verifiedAt && (
<span className="mt-1 self-start">
<SolidBadge tone="green" icon="shield-check-fill">
Verified · {me.licensingBody}
</SolidBadge>
</span>
)}
</span>
</div>
<MenuSeparator />
<MenuItem icon="settings-3-line" onSelect={() => navigate("/app/settings")}>
Settings
</MenuItem>
<MenuSeparator />
<MenuItem icon="lock-line" onSelect={lock}>
Lock Context now
</MenuItem>
<MenuItem icon="logout-box-r-line" onSelect={() => void signOut()}>
Sign out
</MenuItem>
</MenuContent>
</Menu>
);
}
function FullLink({ item, on, urgent, onNavigate }: { item: NavItem; on: boolean; urgent: number; onNavigate?: () => void }) {
return (
<Link
to={item.to}
aria-current={on ? "page" : undefined}
onClick={onNavigate}
className={cn(
"flex h-9 items-center gap-2.5 rounded-lg px-2.5 text-[14px] text-[#27272a] no-underline transition-colors hover:bg-quiet-hover",
on && "bg-line font-semibold hover:bg-line",
focusRing,
)}
>
<Icon name={on ? item.activeIcon : item.icon} size={18} className="text-grey-icon" />
<span className="min-w-0 flex-1 truncate">{item.label}</span>
{item.id === "patients" && urgent > 0 && (
<>
<Count tone="red">{urgent}</Count>
<span className="sr-only"> urgent</span>
</>
)}
</Link>
);
}
/** The expanded sidebar; on a phone it is the sheet, with a close button for the fold one. */
export function FullNav({ active, urgent, end, onNavigate }: NavProps & { end: ReactNode; onNavigate?: () => void }) {
return (
<div className="flex min-h-0 w-[240px] max-w-full flex-1 flex-col gap-[18px] px-3 py-3.5">
<div className="flex h-8 items-center justify-between gap-2 pl-2">
<Brand to="/app/today" />
{end}
</div>
<ClinicMenu />
<nav aria-label="Main navigation" className="flex min-h-0 flex-col gap-[18px] overflow-y-auto">
{groups.map((g) => (
<div key={g.id} role="group" aria-label={g.label} className="flex flex-col gap-0.5">
{g.visible && <p className="m-0 px-2.5 pb-1.5 text-[12px] leading-4 text-subtle">{g.label}</p>}
{navItems
.filter((n) => n.group === g.id)
.map((n) => (
<FullLink key={n.id} item={n} on={n.id === active} urgent={urgent} onNavigate={onNavigate} />
))}
</div>
))}
</nav>
<div className="mt-auto flex items-center gap-2.5 px-2.5 py-1">
<Icon name="shield-check-line" size={18} className="text-grey-icon" />
<span className="flex min-w-0 flex-col leading-4">
<strong className="text-[13px] font-medium text-body">Information tool</strong>
<small className="text-[12px] text-muted">A clinician decides what to do</small>
</span>
</div>
</div>
);
}
function RailLink({ item, on, urgent }: { item: NavItem; on: boolean; urgent: number }) {
const dot = item.id === "patients" && urgent > 0;
return (
<Tip label={item.label} keys={["G", item.key]} side="right">
<Link
to={item.to}
aria-current={on ? "page" : undefined}
aria-label={dot ? `${item.label}, ${urgent} urgent` : item.label}
className={cn(
"relative flex h-[38px] w-10 items-center justify-center rounded-[9px] text-grey-icon no-underline transition-colors hover:bg-quiet-hover",
on && "bg-line hover:bg-line",
focusRing,
)}
>
<Icon name={on ? item.activeIcon : item.icon} size={19} />
{dot && <span aria-hidden="true" className="absolute top-2 right-[9px] size-[7px] rounded-full bg-red-icon shadow-[0_0_0_2px_var(--color-frame)]" />}
</Link>
</Tip>
);
}
/** The slim icon rail: each icon names itself and its keys in a tooltip. */
function Rail({ active, urgent, onToggle }: NavProps & { onToggle: () => void }) {
return (
<div className="flex w-[60px] flex-1 flex-col items-center gap-1 py-3.5">
<div className="flex h-8 items-center [&_span]:hidden">
<Brand to="/app/today" />
</div>
<ClinicMenu rail />
<nav aria-label="Main navigation" className="flex flex-col items-center gap-1">
{groups.map((g) => (
<div key={g.id} role="group" aria-label={g.label} className="flex flex-col items-center gap-1">
<span aria-hidden="true" className="my-2 h-px w-6 bg-line" />
{navItems
.filter((n) => n.group === g.id)
.map((n) => (
<RailLink key={n.id} item={n} on={n.id === active} urgent={urgent} />
))}
</div>
))}
</nav>
<div className="mt-auto flex flex-col items-center gap-1">
<Tip label="Information tool · A clinician decides what to do" side="right">
<span className="flex h-[38px] w-10 items-center justify-center text-subtle">
<Icon name="shield-check-line" size={18} />
<span className="sr-only">Information tool. A clinician decides what to do.</span>
</span>
</Tip>
<FoldButton collapsed onToggle={onToggle} />
</div>
</div>
);
}
export function FoldButton({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
const label = collapsed ? "Open sidebar" : "Close sidebar";
return (
<Tip label={label} keys={[modKey(), "B"]} side="right">
<Button variant="ghost" size="icon-sm" aria-label={label} aria-expanded={!collapsed} onClick={onToggle} className="size-8 rounded-lg text-grey-icon hover:bg-quiet-hover">
<Icon name={collapsed ? "sidebar-unfold-line" : "sidebar-fold-line"} size={18} />
</Button>
</Tip>
);
}
/** Desktop and tablet: the rail or the full sidebar on the grey frame. Hidden on a phone. */
export function Sidebar({ collapsed, onToggle, ...nav }: NavProps & { collapsed: boolean; onToggle: () => void }) {
return (
<aside
aria-label="Context navigation"
className={cn(
"sticky top-0 hidden h-dvh shrink-0 overflow-hidden transition-[width] duration-300 ease-out-expo md:flex motion-reduce:transition-none",
collapsed ? "w-[60px]" : "w-[240px]",
)}
>
{collapsed ? <Rail {...nav} onToggle={onToggle} /> : <FullNav {...nav} end={<FoldButton collapsed={false} onToggle={onToggle} />} />}
</aside>
);
}
/** Phone: the full sidebar as a sheet from the left, with focus kept inside until it closes. */
export function MobileSheet({ open, onOpenChange, ...nav }: NavProps & { open: boolean; onOpenChange: (open: boolean) => void }) {
useEffect(() => {
if (!open || typeof window.matchMedia !== "function") return;
const wide = window.matchMedia("(min-width: 768px)");
const close = () => wide.matches && onOpenChange(false);
wide.addEventListener("change", close);
return () => wide.removeEventListener("change", close);
}, [open, onOpenChange]);
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-40 bg-[rgba(15,23,42,0.32)] md:hidden" />
<Dialog.Content aria-describedby={undefined} className="fixed inset-y-0 left-0 z-50 flex w-[min(300px,86vw)] flex-col bg-frame shadow-pop outline-none md:hidden data-[state=open]:animate-[slide-in_0.3s_var(--ease-out-expo)_both]">
<Dialog.Title className="sr-only">Context navigation</Dialog.Title>
<aside aria-label="Context navigation" className="flex min-h-0 flex-1 flex-col [&>div]:w-full">
<FullNav
{...nav}
onNavigate={() => onOpenChange(false)}
end={
<Dialog.Close asChild>
<Button variant="ghost" size="icon-sm" aria-label="Close navigation" className="size-8 rounded-lg text-grey-icon hover:bg-quiet-hover">
<Icon name="close-line" size={18} />
</Button>
</Dialog.Close>
}
/>
</aside>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}