@spaced-out/ui-design-system
Version:
Sense UI components library
270 lines (252 loc) • 6.45 kB
Flow
// @flow strict
import * as React from 'react';
import {
JumboSmall,
SubTitleLarge,
SubTitleMedium,
SubTitleSmall,
} from '../../components/Text';
import * as COLORS from '../../styles/variables/_color';
import {
colorDangerLightest,
colorGrayLightest,
colorInformationLightest,
colorNeutralLightest,
colorSuccessLightest,
colorWarningLightest,
} from '../../styles/variables/_color';
import classify from '../../utils/classify';
import type {IconType} from '../Icon';
import {Icon} from '../Icon';
import type {StatusSemanticType} from '../StatusIndicator';
import {StatusIndicator} from '../StatusIndicator';
import css from './Avatar.module.css';
type ClassNames = $ReadOnly<{
wrapper?: string,
ring?: string,
content?: string,
image?: string,
icon?: string,
text?: string,
}>;
export const AVATAR_SIZE = Object.freeze({
small: 'small',
medium: 'medium',
large: 'large',
extraLarge: 'extraLarge',
});
export const avatarSizeOptions: Array<mixed> = [...Object.keys(AVATAR_SIZE)];
export type AvatarSize = $Values<typeof AVATAR_SIZE>;
export const AVATAR_COLOR = Object.freeze({
red: colorDangerLightest,
orange: colorWarningLightest,
green: colorSuccessLightest,
blue: colorInformationLightest,
gray: colorGrayLightest,
indigo: colorNeutralLightest,
});
export type AvatarColorType = $Keys<typeof AVATAR_COLOR>;
export type AvatarProps = {
classNames?: ClassNames,
size?: AvatarSize,
imageSrc?: string,
ring?: boolean,
icon?: boolean,
text?: string,
color?: AvatarColorType,
style?: mixed,
iconName?: string,
iconType?: IconType,
status?: StatusSemanticType,
statusBorderColorToken?: $Keys<typeof COLORS>,
};
const getAvatarText = (text = '') => {
const [firstWord, secondWord] = text.split(' ');
const firstChar = firstWord ? firstWord[0].toUpperCase() : '';
const secondChar = secondWord ? secondWord[0].toUpperCase() : '';
return firstChar + secondChar;
};
const AvatarTextDecorator = ({
avatarText,
size,
className,
}: {
avatarText: string,
size: AvatarSize,
className?: string,
}) => {
switch (size) {
case 'small':
return <SubTitleSmall className={className}>{avatarText}</SubTitleSmall>;
case 'medium':
return (
<SubTitleMedium className={className}>{avatarText}</SubTitleMedium>
);
case 'large':
return <SubTitleLarge className={className}>{avatarText}</SubTitleLarge>;
case 'extraLarge':
return <JumboSmall className={className}>{avatarText}</JumboSmall>;
}
};
// mapping of iconSize for each avatar size;
const iconSize = Object.freeze({
small: 'small',
medium: 'medium',
large: 'medium',
extraLarge: 'large',
});
export type AvatarContentProps = {
imageSrc?: string,
text?: string,
iconName?: string,
iconType?: IconType,
size: AvatarSize,
color?: AvatarColorType,
ring?: boolean,
status?: StatusSemanticType,
statusBorderColorToken?: $Keys<typeof COLORS>,
classNames?: ClassNames,
};
const AvatarConditionalContent = ({
imageSrc,
text,
iconName,
size,
iconType,
color = 'blue',
status,
statusBorderColorToken,
classNames,
}: AvatarContentProps) => (
<div
className={classify(
css.innerContainer,
{[css.alignCenter]: !imageSrc},
classNames?.content,
)}
style={{
'--background-color': AVATAR_COLOR[color],
}}
>
{!!status && (
<div className={classify(css.statusIndicatorWrapper, css[size])}>
<StatusIndicator
status={status}
withBorder
borderColorToken={statusBorderColorToken}
/>
</div>
)}
{imageSrc ? (
<img
src={imageSrc}
alt="Avatar"
className={classify(css.avatar, classNames?.image)}
></img>
) : iconName ? (
<Icon
name={iconName}
type={iconType}
size={iconSize[size]}
className={classNames?.icon}
/>
) : text ? (
<AvatarTextDecorator
avatarText={getAvatarText(text)}
size={size}
className={classNames?.text}
/>
) : null}
</div>
);
export const Avatar: React$AbstractComponent<AvatarProps, HTMLDivElement> =
React.forwardRef<AvatarProps, HTMLDivElement>(
(
{
classNames,
size = AVATAR_SIZE.medium,
imageSrc,
text,
style,
iconName,
iconType,
color,
ring = false,
status,
statusBorderColorToken,
}: AvatarProps,
ref,
) => (
<div
className={classify(
css.container,
{
[css.extraLargeSize]: size === 'extraLarge',
[css.largeSize]: size === 'large',
[css.mediumSize]: size === 'medium',
[css.smallSize]: size === 'small',
},
classNames?.wrapper,
)}
style={style}
ref={ref}
>
{ring ? (
<div className={classify(css.ring, classNames?.ring)}>
<div className={css.whiteCircle}>
<AvatarConditionalContent
imageSrc={imageSrc}
text={text}
iconName={iconName}
iconType={iconType}
color={color}
size={size}
status={status}
statusBorderColorToken={statusBorderColorToken}
classNames={classNames}
/>
</div>
</div>
) : (
<AvatarConditionalContent
imageSrc={imageSrc}
text={text}
iconName={iconName}
iconType={iconType}
color={color}
size={size}
status={status}
statusBorderColorToken={statusBorderColorToken}
classNames={classNames}
/>
)}
</div>
),
);
export const BaseAvatar = ({
classNames,
size = AVATAR_SIZE.medium,
text = '',
style,
color = 'blue',
}: AvatarProps): React.Node => (
<div
className={classify(
css.container,
{
[css.baseExtraLargeSize]: size === 'extraLarge',
[css.baseLargeSize]: size === 'large',
[css.baseMediumSize]: size === 'medium',
[css.baseSmallSize]: size === 'small',
},
classNames?.wrapper,
)}
style={{...style, background: AVATAR_COLOR[color]}}
>
<AvatarTextDecorator
avatarText={text}
size={size}
className={classNames?.text}
/>
</div>
);