mirror of
https://github.com/makeplane/plane.git
synced 2025-12-23 07:09:34 +01:00
style: new avatar and avatar group components (#2584)
* style: new avatar components * chore: bug fixes * chore: add pixel to size * chore: add comments to helper functions * fix: build errors
This commit is contained in:
committed by
GitHub
parent
1a24f9ec25
commit
490e032ac6
85
packages/ui/src/avatar/avatar-group.tsx
Normal file
85
packages/ui/src/avatar/avatar-group.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Tooltip } from "../tooltip";
|
||||
// types
|
||||
import { TAvatarSize, getSizeInfo, isAValidNumber } from "./avatar";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The children of the avatar group.
|
||||
* These should ideally should be `Avatar` components
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* The maximum number of avatars to display.
|
||||
* If the number of children exceeds this value, the additional avatars will be replaced by a count of the remaining avatars.
|
||||
* @default 2
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* Whether to show the tooltip or not
|
||||
* @default true
|
||||
*/
|
||||
showTooltip?: boolean;
|
||||
/**
|
||||
* The size of the avatars
|
||||
* Possible values: "sm", "md", "base", "lg"
|
||||
* @default "md"
|
||||
*/
|
||||
size?: TAvatarSize;
|
||||
};
|
||||
|
||||
export const AvatarGroup: React.FC<Props> = (props) => {
|
||||
const { children, max = 2, showTooltip = true, size = "md" } = props;
|
||||
|
||||
// calculate total length of avatars inside the group
|
||||
const totalAvatars = React.Children.toArray(children).length;
|
||||
|
||||
// slice the children to the maximum number of avatars
|
||||
const avatars = React.Children.toArray(children).slice(0, max);
|
||||
|
||||
// assign the necessary props from the AvatarGroup component to the Avatar components
|
||||
const avatarsWithUpdatedProps = avatars.map((avatar) => {
|
||||
const updatedProps: Partial<Props> = {
|
||||
showTooltip,
|
||||
size,
|
||||
};
|
||||
|
||||
return React.cloneElement(avatar as React.ReactElement, updatedProps);
|
||||
});
|
||||
|
||||
// get size details based on the size prop
|
||||
const sizeInfo = getSizeInfo(size);
|
||||
|
||||
return (
|
||||
<div className={`flex ${sizeInfo.spacing}`}>
|
||||
{avatarsWithUpdatedProps.map((avatar, index) => (
|
||||
<div key={index} className="ring-1 ring-custom-border-200 rounded-full">
|
||||
{avatar}
|
||||
</div>
|
||||
))}
|
||||
{max < totalAvatars && (
|
||||
<Tooltip
|
||||
tooltipContent={`${totalAvatars} total`}
|
||||
disabled={!showTooltip}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
|
||||
} ring-1 ring-custom-border-200 bg-custom-primary-500 text-white rounded-full grid place-items-center text-[9px]`}
|
||||
style={
|
||||
isAValidNumber(size)
|
||||
? {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
+{totalAvatars - max}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
168
packages/ui/src/avatar/avatar.tsx
Normal file
168
packages/ui/src/avatar/avatar.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The name of the avatar which will be displayed on the tooltip
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The background color if the avatar image fails to load
|
||||
*/
|
||||
fallbackBackgroundColor?: string;
|
||||
/**
|
||||
* The text to display if the avatar image fails to load
|
||||
*/
|
||||
fallbackText?: string;
|
||||
/**
|
||||
* The text color if the avatar image fails to load
|
||||
*/
|
||||
fallbackTextColor?: string;
|
||||
/**
|
||||
* Whether to show the tooltip or not
|
||||
* @default true
|
||||
*/
|
||||
showTooltip?: boolean;
|
||||
/**
|
||||
* The size of the avatars
|
||||
* Possible values: "sm", "md", "base", "lg"
|
||||
* @default "md"
|
||||
*/
|
||||
size?: TAvatarSize;
|
||||
/**
|
||||
* The shape of the avatar
|
||||
* Possible values: "circle", "square"
|
||||
* @default "circle"
|
||||
*/
|
||||
shape?: "circle" | "square";
|
||||
/**
|
||||
* The source of the avatar image
|
||||
*/
|
||||
src?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the size details based on the size prop
|
||||
* @param size The size of the avatar
|
||||
* @returns The size details
|
||||
*/
|
||||
export const getSizeInfo = (size: TAvatarSize) => {
|
||||
switch (size) {
|
||||
case "sm":
|
||||
return {
|
||||
avatarSize: "h-4 w-4",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
case "md":
|
||||
return {
|
||||
avatarSize: "h-5 w-5",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
case "base":
|
||||
return {
|
||||
avatarSize: "h-6 w-6",
|
||||
fontSize: "text-sm",
|
||||
spacing: "-space-x-1.5",
|
||||
};
|
||||
case "lg":
|
||||
return {
|
||||
avatarSize: "h-7 w-7",
|
||||
fontSize: "text-sm",
|
||||
spacing: "-space-x-1.5",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
avatarSize: "h-5 w-5",
|
||||
fontSize: "text-xs",
|
||||
spacing: "-space-x-1",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the border radius based on the shape prop
|
||||
* @param shape The shape of the avatar
|
||||
* @returns The border radius
|
||||
*/
|
||||
export const getBorderRadius = (shape: "circle" | "square") => {
|
||||
switch (shape) {
|
||||
case "circle":
|
||||
return "rounded-full";
|
||||
case "square":
|
||||
return "rounded-md";
|
||||
default:
|
||||
return "rounded-full";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the value is a valid number
|
||||
* @param value The value to check
|
||||
* @returns Whether the value is a valid number or not
|
||||
*/
|
||||
export const isAValidNumber = (value: any) => {
|
||||
return typeof value === "number" && !isNaN(value);
|
||||
};
|
||||
|
||||
export const Avatar: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
fallbackBackgroundColor,
|
||||
fallbackText,
|
||||
fallbackTextColor,
|
||||
showTooltip = true,
|
||||
size = "md",
|
||||
shape = "circle",
|
||||
src,
|
||||
} = props;
|
||||
|
||||
// get size details based on the size prop
|
||||
const sizeInfo = getSizeInfo(size);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={fallbackText ?? name ?? "?"}
|
||||
disabled={!showTooltip}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
|
||||
} overflow-hidden grid place-items-center ${getBorderRadius(shape)}`}
|
||||
style={
|
||||
isAValidNumber(size)
|
||||
? {
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{src ? (
|
||||
<img
|
||||
src={src}
|
||||
className={`h-full w-full ${getBorderRadius(shape)}`}
|
||||
alt={name}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`${
|
||||
sizeInfo.fontSize
|
||||
} grid place-items-center h-full w-full ${getBorderRadius(shape)}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
|
||||
color: fallbackTextColor ?? "#ffffff",
|
||||
}}
|
||||
>
|
||||
{name ? name[0].toUpperCase() : fallbackText ?? "?"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
2
packages/ui/src/avatar/index.ts
Normal file
2
packages/ui/src/avatar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./avatar-group";
|
||||
export * from "./avatar";
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from "./avatar";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./button";
|
||||
export * from "./dropdowns";
|
||||
export * from "./form-fields";
|
||||
export * from "./icons";
|
||||
export * from "./progress";
|
||||
export * from "./spinners";
|
||||
export * from "./loader";
|
||||
export * from "./tooltip";
|
||||
export * from "./icons";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./dropdowns";
|
||||
export * from "./loader";
|
||||
Reference in New Issue
Block a user