Files
helix-engage/src/components/application/app-navigation/base-components/nav-item.tsx
2026-04-01 11:29:05 +05:30

112 lines
4.1 KiB
TypeScript

import type { FC, MouseEventHandler, ReactNode } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown, faArrowUpRightFromSquare } from "@fortawesome/pro-duotone-svg-icons";
import { Link as AriaLink } from "react-aria-components";
import { Badge } from "@/components/base/badges/badges";
import { cx, sortCx } from "@/utils/cx";
const styles = sortCx({
root: "group relative flex w-full cursor-pointer items-center rounded-md outline-focus-ring transition duration-100 ease-linear select-none focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
rootSelected: "bg-sidebar-active hover:bg-sidebar-active border-l-2 border-l-brand-600",
});
interface NavItemBaseProps {
/** Whether the nav item shows only an icon. */
iconOnly?: boolean;
/** Whether the collapsible nav item is open. */
open?: boolean;
/** URL to navigate to when the nav item is clicked. */
href?: string;
/** Type of the nav item. */
type: "link" | "collapsible" | "collapsible-child";
/** Icon component to display. */
icon?: FC<Record<string, any>>;
/** Badge to display. */
badge?: ReactNode;
/** Whether the nav item is currently active. */
current?: boolean;
/** Whether to truncate the label text. */
truncate?: boolean;
/** Handler for click events. */
onClick?: MouseEventHandler;
/** Content to display. */
children?: ReactNode;
}
export const NavItemBase = ({ current, type, badge, href, icon: Icon, children, truncate = true, onClick }: NavItemBaseProps) => {
const iconElement = Icon && <Icon aria-hidden="true" className="mr-2 size-5 shrink-0 text-fg-quaternary transition-inherit-all" />;
const badgeElement =
badge && (typeof badge === "string" || typeof badge === "number") ? (
<Badge className="ml-3" color="gray" type="pill-color" size="sm">
{badge}
</Badge>
) : (
badge
);
const labelElement = (
<span
className={cx(
"flex-1 text-md font-semibold text-white transition-inherit-all",
truncate && "truncate",
current ? "text-sidebar-active" : "group-hover:text-sidebar-hover",
)}
>
{children}
</span>
);
const isExternal = href && href.startsWith("http");
const externalIcon = isExternal && <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="size-4 text-fg-quaternary" />;
if (type === "collapsible") {
return (
<summary
className={cx("px-3 py-2 bg-sidebar", !current && "hover:bg-sidebar-hover", styles.root, current && styles.rootSelected)}
onClick={onClick}>
{iconElement}
{labelElement}
{badgeElement}
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" className="ml-3 size-4 shrink-0 text-fg-quaternary in-open:-scale-y-100" />
</summary>
);
}
if (type === "collapsible-child") {
return (
<AriaLink
href={href!}
target={isExternal ? "_blank" : "_self"}
rel="noopener noreferrer"
className={cx("py-2 pr-3 pl-10 bg-sidebar", !current && "hover:bg-sidebar-hover", styles.root, current && styles.rootSelected)}
onClick={onClick}
aria-current={current ? "page" : undefined}
>
{labelElement}
{externalIcon}
{badgeElement}
</AriaLink>
);
}
return (
<AriaLink
href={href!}
target={isExternal ? "_blank" : "_self"}
rel="noopener noreferrer"
className={cx("px-3 py-2 bg-sidebar", !current && "hover:bg-sidebar-hover", styles.root, current && styles.rootSelected)}
onClick={onClick}
aria-current={current ? "page" : undefined}
>
{iconElement}
{labelElement}
{externalIcon}
{badgeElement}
</AriaLink>
);
};