Files
helix-engage/src/components/application/app-navigation/base-components/nav-account-card.tsx
2026-03-31 15:05:05 +05:30

183 lines
7.8 KiB
TypeScript

import type { FC, HTMLAttributes } from "react";
import { useCallback, useEffect, useRef } from "react";
import type { Placement } from "@react-types/overlays";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser, faGear, faArrowRightFromBracket, faPhoneVolume, faSort } from "@fortawesome/pro-duotone-svg-icons";
const IconUser: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faUser} className={className} />;
const IconSettings: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faGear} className={className} />;
const IconLogout: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faArrowRightFromBracket} className={className} />;
const IconForceReady: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faPhoneVolume} className={className} />;
import { useFocusManager } from "react-aria";
import type { DialogProps as AriaDialogProps } from "react-aria-components";
import { Button as AriaButton, Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
import { useBreakpoint } from "@/hooks/use-breakpoint";
import { cx } from "@/utils/cx";
type NavAccountType = {
/** Unique identifier for the nav item. */
id: string;
/** Name of the account holder. */
name: string;
/** Email address of the account holder. */
email: string;
/** Avatar image URL. */
avatar: string;
/** Online status of the account holder. This is used to display the online status indicator. */
status: "online" | "offline";
};
export const NavAccountMenu = ({
className,
onSignOut,
onForceReady,
...dialogProps
}: AriaDialogProps & { className?: string; accounts?: NavAccountType[]; selectedAccountId?: string; onSignOut?: () => void; onForceReady?: () => void }) => {
const focusManager = useFocusManager();
const dialogRef = useRef<HTMLDivElement>(null);
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
switch (e.key) {
case "ArrowDown":
focusManager?.focusNext({ tabbable: true, wrap: true });
break;
case "ArrowUp":
focusManager?.focusPrevious({ tabbable: true, wrap: true });
break;
}
},
[focusManager],
);
useEffect(() => {
const element = dialogRef.current;
if (element) {
element.addEventListener("keydown", onKeyDown);
}
return () => {
if (element) {
element.removeEventListener("keydown", onKeyDown);
}
};
}, [onKeyDown]);
return (
<AriaDialog
{...dialogProps}
ref={dialogRef}
className={cx("w-66 rounded-xl bg-secondary_alt shadow-lg ring ring-secondary_alt outline-hidden", className)}
>
{({ close }) => (
<>
<div className="rounded-xl bg-primary ring-1 ring-secondary">
<div className="flex flex-col gap-0.5 py-1.5">
<NavAccountCardMenuItem label="View profile" icon={IconUser} shortcut="⌘K->P" />
<NavAccountCardMenuItem label="Account settings" icon={IconSettings} shortcut="⌘S" />
<NavAccountCardMenuItem label="Force Ready" icon={IconForceReady} onClick={() => { close(); onForceReady?.(); }} />
</div>
</div>
<div className="pt-1 pb-1.5">
<NavAccountCardMenuItem label="Sign out" icon={IconLogout} shortcut="⌥⇧Q" onClick={() => { close(); onSignOut?.(); }} />
</div>
</>
)}
</AriaDialog>
);
};
const NavAccountCardMenuItem = ({
icon: Icon,
label,
shortcut,
...buttonProps
}: {
icon?: FC<{ className?: string }>;
label: string;
shortcut?: string;
} & HTMLAttributes<HTMLButtonElement>) => {
return (
<button {...buttonProps} className={cx("group/item w-full cursor-pointer px-1.5 focus:outline-hidden", buttonProps.className)}>
<div
className={cx(
"flex w-full items-center justify-between gap-3 rounded-md p-2 group-hover/item:bg-primary_hover",
// Focus styles.
"outline-focus-ring group-focus-visible/item:outline-2 group-focus-visible/item:outline-offset-2",
)}
>
<div className="flex gap-2 text-sm font-semibold text-secondary group-hover/item:text-secondary_hover">
{Icon && <Icon className="size-5 text-fg-quaternary" />} {label}
</div>
{shortcut && (
<kbd className="flex rounded px-1 py-px font-body text-xs font-medium text-tertiary ring-1 ring-secondary ring-inset">{shortcut}</kbd>
)}
</div>
</button>
);
};
export const NavAccountCard = ({
popoverPlacement,
selectedAccountId,
items = [],
onSignOut,
onForceReady,
}: {
popoverPlacement?: Placement;
selectedAccountId?: string;
items?: NavAccountType[];
onSignOut?: () => void;
onForceReady?: () => void;
}) => {
const triggerRef = useRef<HTMLDivElement>(null);
const isDesktop = useBreakpoint("lg");
const selectedAccount = items.find((account) => account.id === selectedAccountId);
if (!selectedAccount) {
console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`);
return null;
}
return (
<div ref={triggerRef} className="relative flex items-center gap-3 rounded-xl p-3 ring-1 ring-secondary ring-inset">
<AvatarLabelGroup
size="md"
src={selectedAccount.avatar}
title={selectedAccount.name}
subtitle={selectedAccount.email}
status={selectedAccount.status}
/>
<div className="absolute top-1.5 right-1.5">
<AriaDialogTrigger>
<AriaButton className="flex cursor-pointer items-center justify-center rounded-md p-1.5 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 pressed:bg-primary_hover pressed:text-fg-quaternary_hover">
<FontAwesomeIcon icon={faSort} className="size-4 shrink-0" />
</AriaButton>
<AriaPopover
placement={popoverPlacement ?? (isDesktop ? "right bottom" : "top right")}
triggerRef={triggerRef}
offset={8}
className={({ isEntering, isExiting }) =>
cx(
"origin-(--trigger-anchor-point) will-change-transform",
isEntering &&
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
isExiting &&
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
)
}
>
<NavAccountMenu selectedAccountId={selectedAccountId} accounts={items} onSignOut={onSignOut} onForceReady={onForceReady} />
</AriaPopover>
</AriaDialogTrigger>
</div>
</div>
);
};