mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
419 lines
15 KiB
TypeScript
419 lines
15 KiB
TypeScript
import type { FC, MouseEventHandler, ReactNode } from "react";
|
|
import { faXmark } from "@fortawesome/pro-duotone-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { Dot } from "@/components/foundations/dot-icon";
|
|
import { cx } from "@/utils/cx";
|
|
import type { BadgeColors, BadgeTypeToColorMap, BadgeTypes, FlagTypes, IconComponentType, Sizes } from "./badge-types";
|
|
import { badgeTypes } from "./badge-types";
|
|
|
|
const CloseX: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faXmark} className={className} />;
|
|
|
|
export const filledColors: Record<BadgeColors, { root: string; addon: string; addonButton: string }> = {
|
|
gray: {
|
|
root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200",
|
|
addon: "text-utility-gray-500",
|
|
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
|
},
|
|
brand: {
|
|
root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200",
|
|
addon: "text-utility-brand-500",
|
|
addonButton: "hover:bg-utility-brand-100 text-utility-brand-400 hover:text-utility-brand-500",
|
|
},
|
|
error: {
|
|
root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200",
|
|
addon: "text-utility-error-500",
|
|
addonButton: "hover:bg-utility-error-100 text-utility-error-400 hover:text-utility-error-500",
|
|
},
|
|
warning: {
|
|
root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200",
|
|
addon: "text-utility-warning-500",
|
|
addonButton: "hover:bg-utility-warning-100 text-utility-warning-400 hover:text-utility-warning-500",
|
|
},
|
|
success: {
|
|
root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200",
|
|
addon: "text-utility-success-500",
|
|
addonButton: "hover:bg-utility-success-100 text-utility-success-400 hover:text-utility-success-500",
|
|
},
|
|
"gray-blue": {
|
|
root: "bg-utility-gray-blue-50 text-utility-gray-blue-700 ring-utility-gray-blue-200",
|
|
addon: "text-utility-gray-blue-500",
|
|
addonButton: "hover:bg-utility-gray-blue-100 text-utility-gray-blue-400 hover:text-utility-gray-blue-500",
|
|
},
|
|
"blue-light": {
|
|
root: "bg-utility-blue-light-50 text-utility-blue-light-700 ring-utility-blue-light-200",
|
|
addon: "text-utility-blue-light-500",
|
|
addonButton: "hover:bg-utility-blue-light-100 text-utility-blue-light-400 hover:text-utility-blue-light-500",
|
|
},
|
|
blue: {
|
|
root: "bg-utility-blue-50 text-utility-blue-700 ring-utility-blue-200",
|
|
addon: "text-utility-blue-500",
|
|
addonButton: "hover:bg-utility-blue-100 text-utility-blue-400 hover:text-utility-blue-500",
|
|
},
|
|
indigo: {
|
|
root: "bg-utility-indigo-50 text-utility-indigo-700 ring-utility-indigo-200",
|
|
addon: "text-utility-indigo-500",
|
|
addonButton: "hover:bg-utility-indigo-100 text-utility-indigo-400 hover:text-utility-indigo-500",
|
|
},
|
|
purple: {
|
|
root: "bg-utility-purple-50 text-utility-purple-700 ring-utility-purple-200",
|
|
addon: "text-utility-purple-500",
|
|
addonButton: "hover:bg-utility-purple-100 text-utility-purple-400 hover:text-utility-purple-500",
|
|
},
|
|
pink: {
|
|
root: "bg-utility-pink-50 text-utility-pink-700 ring-utility-pink-200",
|
|
addon: "text-utility-pink-500",
|
|
addonButton: "hover:bg-utility-pink-100 text-utility-pink-400 hover:text-utility-pink-500",
|
|
},
|
|
orange: {
|
|
root: "bg-utility-orange-50 text-utility-orange-700 ring-utility-orange-200",
|
|
addon: "text-utility-orange-500",
|
|
addonButton: "hover:bg-utility-orange-100 text-utility-orange-400 hover:text-utility-orange-500",
|
|
},
|
|
};
|
|
|
|
const addonOnlyColors = Object.fromEntries(Object.entries(filledColors).map(([key, value]) => [key, { root: "", addon: value.addon }])) as Record<
|
|
BadgeColors,
|
|
{ root: string; addon: string }
|
|
>;
|
|
|
|
const withPillTypes = {
|
|
[badgeTypes.pillColor]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
|
styles: filledColors,
|
|
},
|
|
[badgeTypes.badgeColor]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
|
styles: filledColors,
|
|
},
|
|
[badgeTypes.badgeModern]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset shadow-xs",
|
|
styles: {
|
|
gray: {
|
|
root: "bg-primary text-secondary ring-primary",
|
|
addon: "text-gray-500",
|
|
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const withBadgeTypes = {
|
|
[badgeTypes.pillColor]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
|
styles: filledColors,
|
|
},
|
|
[badgeTypes.badgeColor]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
|
styles: filledColors,
|
|
},
|
|
[badgeTypes.badgeModern]: {
|
|
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset bg-primary text-secondary ring-primary shadow-xs",
|
|
styles: addonOnlyColors,
|
|
},
|
|
};
|
|
|
|
export type BadgeColor<T extends BadgeTypes> = BadgeTypeToColorMap<typeof withPillTypes>[T];
|
|
|
|
interface BadgeProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
color?: BadgeColor<T>;
|
|
children: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const Badge = <T extends BadgeTypes>(props: BadgeProps<T>) => {
|
|
const { type = "pill-color", size = "md", color = "gray", children } = props;
|
|
const colors = withPillTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "py-0.5 px-2 text-xs font-medium",
|
|
md: "py-0.5 px-2.5 text-sm font-medium",
|
|
lg: "py-1 px-3 text-sm font-medium",
|
|
};
|
|
const badgeSizes = {
|
|
sm: "py-0.5 px-1.5 text-xs font-medium",
|
|
md: "py-0.5 px-2 text-sm font-medium",
|
|
lg: "py-1 px-2.5 text-sm font-medium rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return <span className={cx(colors.common, sizes[type][size], colors.styles[color].root, props.className)}>{children}</span>;
|
|
};
|
|
|
|
interface BadgeWithDotProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
|
className?: string;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const BadgeWithDot = <T extends BadgeTypes>(props: BadgeWithDotProps<T>) => {
|
|
const { size = "md", color = "gray", type = "pill-color", className, children } = props;
|
|
|
|
const colors = withBadgeTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "gap-1 py-0.5 pl-1.5 pr-2 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 pl-2 pr-2.5 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 pl-2.5 pr-3 text-sm font-medium",
|
|
};
|
|
|
|
const badgeSizes = {
|
|
sm: "gap-1 py-0.5 px-1.5 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 px-2 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 px-2.5 text-sm font-medium rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root, className)}>
|
|
<Dot className={colors.styles[color].addon} size="sm" />
|
|
{children}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
interface BadgeWithIconProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
|
iconLeading?: IconComponentType;
|
|
iconTrailing?: IconComponentType;
|
|
children: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const BadgeWithIcon = <T extends BadgeTypes>(props: BadgeWithIconProps<T>) => {
|
|
const { size = "md", color = "gray", type = "pill-color", iconLeading: IconLeading, iconTrailing: IconTrailing, children, className } = props;
|
|
|
|
const colors = withBadgeTypes[type];
|
|
|
|
const icon = IconLeading ? "leading" : "trailing";
|
|
|
|
const pillSizes = {
|
|
sm: {
|
|
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
|
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
|
},
|
|
md: {
|
|
trailing: "gap-1 py-0.5 pl-2.5 pr-2 text-sm font-medium",
|
|
leading: "gap-1 py-0.5 pr-2.5 pl-2 text-sm font-medium",
|
|
},
|
|
lg: {
|
|
trailing: "gap-1 py-1 pl-3 pr-2.5 text-sm font-medium",
|
|
leading: "gap-1 py-1 pr-3 pl-2.5 text-sm font-medium",
|
|
},
|
|
};
|
|
const badgeSizes = {
|
|
sm: {
|
|
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
|
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
|
},
|
|
md: {
|
|
trailing: "gap-1 py-0.5 pl-2 pr-1.5 text-sm font-medium",
|
|
leading: "gap-1 py-0.5 pr-2 pl-1.5 text-sm font-medium",
|
|
},
|
|
lg: {
|
|
trailing: "gap-1 py-1 pl-2.5 pr-2 text-sm font-medium rounded-lg",
|
|
leading: "gap-1 py-1 pr-2.5 pl-2 text-sm font-medium rounded-lg",
|
|
},
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size][icon], colors.styles[color].root, className)}>
|
|
{IconLeading && <IconLeading className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
|
{children}
|
|
{IconTrailing && <IconTrailing className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
interface BadgeWithFlagProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
flag?: FlagTypes;
|
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const BadgeWithFlag = <T extends BadgeTypes>(props: BadgeWithFlagProps<T>) => {
|
|
const { size = "md", color = "gray", flag = "AU", type = "pill-color", children } = props;
|
|
|
|
const colors = withPillTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
|
};
|
|
const badgeSizes = {
|
|
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
<img src={`https://www.untitledui.com/images/flags/${flag}.svg`} className="size-4 max-w-none rounded-full" alt={`${flag} flag`} />
|
|
{children}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
interface BadgeWithImageProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
imgSrc: string;
|
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const BadgeWithImage = <T extends BadgeTypes>(props: BadgeWithImageProps<T>) => {
|
|
const { size = "md", color = "gray", type = "pill-color", imgSrc, children } = props;
|
|
|
|
const colors = withPillTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
|
};
|
|
const badgeSizes = {
|
|
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
|
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
|
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
<img src={imgSrc} className="size-4 max-w-none rounded-full" alt="Badge image" />
|
|
{children}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
interface BadgeWithButtonProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
icon?: IconComponentType;
|
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
|
children: ReactNode;
|
|
/**
|
|
* The label for the button.
|
|
*/
|
|
buttonLabel?: string;
|
|
/**
|
|
* The click event handler for the button.
|
|
*/
|
|
onButtonClick?: MouseEventHandler<HTMLButtonElement>;
|
|
}
|
|
|
|
export const BadgeWithButton = <T extends BadgeTypes>(props: BadgeWithButtonProps<T>) => {
|
|
const { size = "md", color = "gray", type = "pill-color", icon: Icon = CloseX, buttonLabel, children } = props;
|
|
|
|
const colors = withPillTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "gap-0.5 py-0.5 pl-2 pr-0.75 text-xs font-medium",
|
|
md: "gap-0.5 py-0.5 pl-2.5 pr-1 text-sm font-medium",
|
|
lg: "gap-0.5 py-1 pl-3 pr-1.5 text-sm font-medium",
|
|
};
|
|
const badgeSizes = {
|
|
sm: "gap-0.5 py-0.5 pl-1.5 pr-0.75 text-xs font-medium",
|
|
md: "gap-0.5 py-0.5 pl-2 pr-1 text-sm font-medium",
|
|
lg: "gap-0.5 py-1 pl-2.5 pr-1.5 text-sm font-medium rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
{children}
|
|
<button
|
|
type="button"
|
|
aria-label={buttonLabel}
|
|
onClick={props.onButtonClick}
|
|
className={cx(
|
|
"flex cursor-pointer items-center justify-center p-0.5 outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2",
|
|
colors.styles[color].addonButton,
|
|
type === "pill-color" ? "rounded-full" : "rounded-[3px]",
|
|
)}
|
|
>
|
|
<Icon className="size-3 stroke-[3px] transition-inherit-all" />
|
|
</button>
|
|
</span>
|
|
);
|
|
};
|
|
|
|
interface BadgeIconProps<T extends BadgeTypes> {
|
|
type?: T;
|
|
size?: Sizes;
|
|
icon: IconComponentType;
|
|
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
|
children?: ReactNode;
|
|
}
|
|
|
|
export const BadgeIcon = <T extends BadgeTypes>(props: BadgeIconProps<T>) => {
|
|
const { size = "md", color = "gray", type = "pill-color", icon: Icon } = props;
|
|
|
|
const colors = withPillTypes[type];
|
|
|
|
const pillSizes = {
|
|
sm: "p-1.25",
|
|
md: "p-1.5",
|
|
lg: "p-2",
|
|
};
|
|
|
|
const badgeSizes = {
|
|
sm: "p-1.25",
|
|
md: "p-1.5",
|
|
lg: "p-2 rounded-lg",
|
|
};
|
|
|
|
const sizes = {
|
|
[badgeTypes.pillColor]: pillSizes,
|
|
[badgeTypes.badgeColor]: badgeSizes,
|
|
[badgeTypes.badgeModern]: badgeSizes,
|
|
};
|
|
|
|
return (
|
|
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
|
<Icon className={cx("size-3 stroke-[3px]", colors.styles[color].addon)} />
|
|
</span>
|
|
);
|
|
};
|