mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
chore: initial Untitled UI Vite scaffold with FontAwesome Pro
This commit is contained in:
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { Avatar, type AvatarProps } from "./avatar";
|
||||
|
||||
const styles = {
|
||||
sm: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-xs" },
|
||||
md: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-sm" },
|
||||
lg: { root: "gap-3", title: "text-md font-semibold", subtitle: "text-md" },
|
||||
xl: { root: "gap-4", title: "text-lg font-semibold", subtitle: "text-md" },
|
||||
};
|
||||
|
||||
interface AvatarLabelGroupProps extends AvatarProps {
|
||||
size: "sm" | "md" | "lg" | "xl";
|
||||
title: string | ReactNode;
|
||||
subtitle: string | ReactNode;
|
||||
}
|
||||
|
||||
export const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
|
||||
return (
|
||||
<figure className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
|
||||
<Avatar {...props} />
|
||||
<figcaption className="min-w-0 flex-1">
|
||||
<p className={cx("text-primary", styles[props.size].title)}>{title}</p>
|
||||
<p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState } from "react";
|
||||
import { User01 } from "@untitledui/icons";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { type AvatarProps } from "./avatar";
|
||||
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||
|
||||
const styles = {
|
||||
sm: {
|
||||
root: "size-18 p-0.75",
|
||||
rootWithPlaceholder: "p-1",
|
||||
content: "",
|
||||
icon: "size-9",
|
||||
initials: "text-display-sm font-semibold",
|
||||
badge: "bottom-0.5 right-0.5",
|
||||
},
|
||||
md: {
|
||||
root: "size-24 p-1",
|
||||
rootWithPlaceholder: "p-1.25",
|
||||
content: "shadow-xl",
|
||||
icon: "size-12",
|
||||
initials: "text-display-md font-semibold",
|
||||
badge: "bottom-1 right-1",
|
||||
},
|
||||
lg: {
|
||||
root: "size-40 p-1.5",
|
||||
rootWithPlaceholder: "p-1.75",
|
||||
content: "shadow-2xl",
|
||||
icon: "size-20",
|
||||
initials: "text-display-xl font-semibold",
|
||||
badge: "bottom-2 right-2",
|
||||
},
|
||||
};
|
||||
|
||||
const tickSizeMap = {
|
||||
sm: "2xl",
|
||||
md: "3xl",
|
||||
lg: "4xl",
|
||||
} as const;
|
||||
|
||||
interface AvatarProfilePhotoProps extends AvatarProps {
|
||||
size: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const AvatarProfilePhoto = ({
|
||||
contrastBorder = true,
|
||||
size = "md",
|
||||
src,
|
||||
alt,
|
||||
initials,
|
||||
placeholder,
|
||||
placeholderIcon: PlaceholderIcon,
|
||||
verified,
|
||||
badge,
|
||||
status,
|
||||
className,
|
||||
}: AvatarProfilePhotoProps) => {
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
const renderMainContent = () => {
|
||||
if (src && !isFailed) {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onError={() => setIsFailed(true)}
|
||||
className={cx(
|
||||
"size-full rounded-full object-cover",
|
||||
contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
|
||||
styles[size].content,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
<span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (PlaceholderIcon) {
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
<PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
{placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBadgeContent = () => {
|
||||
if (status) {
|
||||
return <AvatarOnlineIndicator status={status} size={tickSizeMap[size]} className={styles[size].badge} />;
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
return <VerifiedTick size={tickSizeMap[size]} className={cx("absolute", styles[size].badge)} />;
|
||||
}
|
||||
|
||||
return badge;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex shrink-0 items-center justify-center rounded-full bg-primary ring-1 ring-secondary_alt",
|
||||
styles[size].root,
|
||||
(!src || isFailed) && styles[size].rootWithPlaceholder,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{renderMainContent()}
|
||||
{renderBadgeContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
129
src/components/base/avatar/avatar.tsx
Normal file
129
src/components/base/avatar/avatar.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { User01 } from "@untitledui/icons";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||
|
||||
type AvatarSize = "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
|
||||
export interface AvatarProps {
|
||||
size?: AvatarSize;
|
||||
className?: string;
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
/**
|
||||
* Display a contrast border around the avatar.
|
||||
*/
|
||||
contrastBorder?: boolean;
|
||||
/**
|
||||
* Display a badge (i.e. company logo).
|
||||
*/
|
||||
badge?: ReactNode;
|
||||
/**
|
||||
* Display a status indicator.
|
||||
*/
|
||||
status?: "online" | "offline";
|
||||
/**
|
||||
* Display a verified tick icon.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
verified?: boolean;
|
||||
|
||||
/**
|
||||
* The initials of the user to display if no image is available.
|
||||
*/
|
||||
initials?: string;
|
||||
/**
|
||||
* An icon to display if no image is available.
|
||||
*/
|
||||
placeholderIcon?: FC<{ className?: string }>;
|
||||
/**
|
||||
* A placeholder to display if no image is available.
|
||||
*/
|
||||
placeholder?: ReactNode;
|
||||
|
||||
/**
|
||||
* Whether the avatar should show a focus ring when the parent group is in focus.
|
||||
* For example, when the avatar is wrapped inside a link.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
xxs: { root: "size-4 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-3" },
|
||||
xs: { root: "size-6 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-4" },
|
||||
sm: { root: "size-8 outline-[0.75px] -outline-offset-[0.75px]", initials: "text-sm font-semibold", icon: "size-5" },
|
||||
md: { root: "size-10 outline-1 -outline-offset-1", initials: "text-md font-semibold", icon: "size-6" },
|
||||
lg: { root: "size-12 outline-1 -outline-offset-1", initials: "text-lg font-semibold", icon: "size-7" },
|
||||
xl: { root: "size-14 outline-1 -outline-offset-1", initials: "text-xl font-semibold", icon: "size-8" },
|
||||
"2xl": { root: "size-16 outline-1 -outline-offset-1", initials: "text-display-xs font-semibold", icon: "size-8" },
|
||||
};
|
||||
|
||||
export const Avatar = ({
|
||||
contrastBorder = true,
|
||||
size = "md",
|
||||
src,
|
||||
alt,
|
||||
initials,
|
||||
placeholder,
|
||||
placeholderIcon: PlaceholderIcon,
|
||||
badge,
|
||||
status,
|
||||
verified,
|
||||
focusable = false,
|
||||
className,
|
||||
}: AvatarProps) => {
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
const renderMainContent = () => {
|
||||
if (src && !isFailed) {
|
||||
return <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return <span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>;
|
||||
}
|
||||
|
||||
if (PlaceholderIcon) {
|
||||
return <PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||
}
|
||||
|
||||
return placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||
};
|
||||
|
||||
const renderBadgeContent = () => {
|
||||
if (status) {
|
||||
return <AvatarOnlineIndicator status={status} size={size === "xxs" ? "xs" : size} />;
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
return (
|
||||
<VerifiedTick
|
||||
size={size === "xxs" ? "xs" : size}
|
||||
className={cx("absolute right-0 bottom-0", (size === "xxs" || size === "xs") && "-right-px -bottom-px")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return badge;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-avatar
|
||||
className={cx(
|
||||
"relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg outline-transparent",
|
||||
// Focus styles
|
||||
focusable && "group-outline-focus-ring group-focus-visible:outline-2 group-focus-visible:outline-offset-2",
|
||||
contrastBorder && "outline outline-avatar-contrast-border",
|
||||
styles[size].root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{renderMainContent()}
|
||||
{renderBadgeContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Plus } from "@untitledui/icons";
|
||||
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||
import { Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: { root: "size-6", icon: "size-4" },
|
||||
sm: { root: "size-8", icon: "size-4" },
|
||||
md: { root: "size-10", icon: "size-5" },
|
||||
};
|
||||
|
||||
interface AvatarAddButtonProps extends AriaButtonProps {
|
||||
size: "xs" | "sm" | "md";
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarAddButton = ({ size, className, title = "Add user", ...props }: AvatarAddButtonProps) => (
|
||||
<AriaTooltip title={title}>
|
||||
<AriaTooltipTrigger
|
||||
{...props}
|
||||
aria-label={title}
|
||||
className={cx(
|
||||
"flex cursor-pointer items-center justify-center rounded-full border border-dashed border-primary bg-primary 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 disabled:border-gray-200 disabled:bg-secondary disabled:text-gray-200",
|
||||
sizes[size].root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Plus className={cx("text-current transition-inherit-all", sizes[size].icon)} />
|
||||
</AriaTooltipTrigger>
|
||||
</AriaTooltip>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: "size-2",
|
||||
sm: "size-3",
|
||||
md: "size-3.5",
|
||||
lg: "size-4",
|
||||
xl: "size-4.5",
|
||||
"2xl": "size-5 ring-[1.67px]",
|
||||
};
|
||||
|
||||
interface AvatarCompanyIconProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
src: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: "size-1.5",
|
||||
sm: "size-2",
|
||||
md: "size-2.5",
|
||||
lg: "size-3",
|
||||
xl: "size-3.5",
|
||||
"2xl": "size-4",
|
||||
"3xl": "size-4.5",
|
||||
"4xl": "size-5",
|
||||
};
|
||||
|
||||
interface AvatarOnlineIndicatorProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
status: "online" | "offline";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarOnlineIndicator = ({ size, status, className }: AvatarOnlineIndicatorProps) => (
|
||||
<span
|
||||
className={cx(
|
||||
"absolute right-0 bottom-0 rounded-full ring-[1.5px] ring-bg-primary",
|
||||
status === "online" ? "bg-fg-success-secondary" : "bg-fg-disabled_subtle",
|
||||
sizes[size],
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
4
src/components/base/avatar/base-components/index.tsx
Normal file
4
src/components/base/avatar/base-components/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./avatar-add-button";
|
||||
export * from "./avatar-company-icon";
|
||||
export * from "./avatar-online-indicator";
|
||||
export * from "./verified-tick";
|
||||
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: { root: "size-2.5", tick: "size-[4.38px" },
|
||||
sm: { root: "size-3", tick: "size-[5.25px]" },
|
||||
md: { root: "size-3.5", tick: "size-[6.13px]" },
|
||||
lg: { root: "size-4", tick: "size-[7px]" },
|
||||
xl: { root: "size-4.5", tick: "size-[7.88px]" },
|
||||
"2xl": { root: "size-5", tick: "size-[8.75px]" },
|
||||
"3xl": { root: "size-6", tick: "size-[10.5px]" },
|
||||
"4xl": { root: "size-8", tick: "size-[14px]" },
|
||||
};
|
||||
|
||||
interface VerifiedTickProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VerifiedTick = ({ size, className }: VerifiedTickProps) => (
|
||||
<svg data-verified className={cx("z-10 text-utility-blue-500", sizes[size].root, className)} viewBox="0 0 10 10" fill="none">
|
||||
<path
|
||||
d="M7.72237 1.77098C7.81734 2.00068 7.99965 2.18326 8.2292 2.27858L9.03413 2.61199C9.26384 2.70714 9.44635 2.88965 9.5415 3.11936C9.63665 3.34908 9.63665 3.60718 9.5415 3.83689L9.20833 4.64125C9.11313 4.87106 9.113 5.12943 9.20863 5.35913L9.54122 6.16325C9.58839 6.27702 9.61268 6.39897 9.6127 6.52214C9.61272 6.6453 9.58847 6.76726 9.54134 6.88105C9.4942 6.99484 9.42511 7.09823 9.33801 7.18531C9.2509 7.27238 9.14749 7.34144 9.03369 7.38854L8.22934 7.72171C7.99964 7.81669 7.81706 7.99899 7.72174 8.22855L7.38833 9.03348C7.29318 9.26319 7.11067 9.4457 6.88096 9.54085C6.65124 9.636 6.39314 9.636 6.16343 9.54085L5.35907 9.20767C5.12935 9.11276 4.87134 9.11295 4.64177 9.20821L3.83684 9.54115C3.60725 9.63608 3.34937 9.636 3.11984 9.54092C2.89032 9.44585 2.70791 9.26356 2.6127 9.03409L2.27918 8.22892C2.18421 7.99923 2.0019 7.81665 1.77235 7.72133L0.967421 7.38792C0.737807 7.29281 0.555355 7.11041 0.460169 6.88083C0.364983 6.65125 0.364854 6.39327 0.45981 6.16359L0.792984 5.35924C0.8879 5.12952 0.887707 4.87151 0.792445 4.64193L0.459749 3.83642C0.41258 3.72265 0.388291 3.60069 0.388272 3.47753C0.388252 3.35436 0.412501 3.2324 0.459634 3.11861C0.506767 3.00482 0.57586 2.90144 0.662965 2.81436C0.75007 2.72728 0.853479 2.65822 0.967283 2.61113L1.77164 2.27795C2.00113 2.18306 2.1836 2.00099 2.27899 1.7717L2.6124 0.966768C2.70755 0.737054 2.89006 0.554547 3.11978 0.459397C3.34949 0.364246 3.60759 0.364246 3.83731 0.459397L4.64166 0.792571C4.87138 0.887487 5.12939 0.887293 5.35897 0.792031L6.16424 0.459913C6.39392 0.364816 6.65197 0.364836 6.88164 0.459968C7.11131 0.555099 7.29379 0.737554 7.38895 0.967208L7.72247 1.77238L7.72237 1.77098Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.95829 3.68932C7.02509 3.58439 7.04747 3.45723 7.02051 3.3358C6.99356 3.21437 6.91946 3.10862 6.81454 3.04182C6.70961 2.97502 6.58245 2.95264 6.46102 2.97959C6.33959 3.00655 6.23384 3.08064 6.16704 3.18557L4.33141 6.06995L3.49141 5.01995C3.41375 4.92281 3.30069 4.8605 3.17709 4.84673C3.05349 4.83296 2.92949 4.86885 2.83235 4.94651C2.73522 5.02417 2.67291 5.13723 2.65914 5.26083C2.64536 5.38443 2.68125 5.50843 2.75891 5.60557L4.00891 7.16807C4.0555 7.22638 4.11533 7.27271 4.18344 7.30323C4.25154 7.33375 4.32595 7.34757 4.40047 7.34353C4.47499 7.3395 4.54747 7.31773 4.61188 7.28004C4.67629 7.24234 4.73077 7.18981 4.77079 7.12682L6.95829 3.68932Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
12
src/components/base/avatar/utils.ts
Normal file
12
src/components/base/avatar/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Extracts the initials from a full name.
|
||||
*
|
||||
* @param name - The full name from which to extract initials.
|
||||
* @returns The initials of the provided name. If the name contains only one word,
|
||||
* it returns the first character of that word. If the name contains two words,
|
||||
* it returns the first character of each word.
|
||||
*/
|
||||
export const getInitials = (name: string) => {
|
||||
const [firstName, lastName] = name.split(" ");
|
||||
return firstName.charAt(0) + (lastName ? lastName.charAt(0) : "");
|
||||
};
|
||||
Reference in New Issue
Block a user