Skip to content

Commit

Permalink
chore: adding initial avatar network component
Browse files Browse the repository at this point in the history
  • Loading branch information
georgewrmarshall committed Jan 31, 2025
1 parent 9ca05a2 commit befddee
Show file tree
Hide file tree
Showing 17 changed files with 608 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AvatarBaseSize } from './AvatarBase.types';

export const AVATAR_BASE_SIZE_DIMENSIONS: Record<AvatarBaseSize, string> = {
[AvatarBaseSize.Xs]: 'h-4 w-4 text-s-body-xs',
[AvatarBaseSize.Sm]: 'h-6 w-6 text-s-body-sm',
[AvatarBaseSize.Md]: 'h-8 w-8 text-s-body-md',
[AvatarBaseSize.Lg]: 'h-10 w-10 text-s-body-lg',
[AvatarBaseSize.Xl]: 'h-12 w-12 text-s-body-lg',
export const AVATAR_BASE_SIZE_CLASS_MAP: Record<AvatarBaseSize, string> = {
[AvatarBaseSize.Xs]: 'h-4 w-4',
[AvatarBaseSize.Sm]: 'h-6 w-6',
[AvatarBaseSize.Md]: 'h-8 w-8',
[AvatarBaseSize.Lg]: 'h-10 w-10',
[AvatarBaseSize.Xl]: 'h-12 w-12',
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Icon, IconName, IconSize } from '..';
import { Icon, IconName, IconSize, Text, TextVariant, TextColor } from '..';
import { AvatarBase } from './AvatarBase';
import { AvatarBaseSize } from './AvatarBase.types';
import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';
import README from './README.mdx';

const meta: Meta<typeof AvatarBase> = {
Expand Down Expand Up @@ -31,26 +31,79 @@ const meta: Meta<typeof AvatarBase> = {
mapping: AvatarBaseSize,
description: 'Optional prop to control the size of the AvatarBase',
},
shape: {
control: 'select',
options: Object.keys(AvatarBaseShape),
mapping: AvatarBaseShape,
description: 'Optional prop to control the shape of the AvatarBase',
},
},
};

export default meta;
type Story = StoryObj<typeof AvatarBase>;

export const Default: Story = {
render: (args) => (
<AvatarBase {...args}>
<Text>{args.children}</Text>
</AvatarBase>
),
args: {
children: 'A',
},
};

export const Size: Story = {
export const Shape: Story = {
render: () => (
<div className="flex gap-2 items-center">
<AvatarBase size={AvatarBaseSize.Xs}>Xs</AvatarBase>
<AvatarBase size={AvatarBaseSize.Sm}>Sm</AvatarBase>
<AvatarBase size={AvatarBaseSize.Md}>Md</AvatarBase>
<AvatarBase size={AvatarBaseSize.Lg}>Lg</AvatarBase>
<AvatarBase size={AvatarBaseSize.Xl}>Xl</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Circle}>
<Text variant={TextVariant.BodySm}>C</Text>
</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Square}>
<Text variant={TextVariant.BodySm}>S</Text>
</AvatarBase>
</div>
),
};

export const Size: Story = {
render: () => (
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<AvatarBase size={AvatarBaseSize.Xs}>
<Text variant={TextVariant.BodyXs}>Xs</Text>
</AvatarBase>
<AvatarBase size={AvatarBaseSize.Sm}>
<Text variant={TextVariant.BodyXs}>Sm</Text>
</AvatarBase>
<AvatarBase size={AvatarBaseSize.Md}>
<Text variant={TextVariant.BodySm}>Md</Text>
</AvatarBase>
<AvatarBase size={AvatarBaseSize.Lg}>
<Text variant={TextVariant.BodyMd}>Lg</Text>
</AvatarBase>
<AvatarBase size={AvatarBaseSize.Xl}>
<Text variant={TextVariant.BodyMd}>Xl</Text>
</AvatarBase>
</div>
<div className="flex gap-2 items-center">
<AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Xs}>
<Text variant={TextVariant.BodyXs}>Xs</Text>
</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Sm}>
<Text variant={TextVariant.BodyXs}>Sm</Text>
</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Md}>
<Text variant={TextVariant.BodySm}>Md</Text>
</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Lg}>
<Text variant={TextVariant.BodyMd}>Lg</Text>
</AvatarBase>
<AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Xl}>
<Text variant={TextVariant.BodyMd}>Xl</Text>
</AvatarBase>
</div>
</div>
),
};
Expand All @@ -59,7 +112,9 @@ export const Children: Story = {
render: () => (
<div className="flex gap-2 items-center">
{/* Text */}
<AvatarBase>A</AvatarBase>
<AvatarBase>
<Text>A</Text>
</AvatarBase>
{/* Image */}
<AvatarBase>
<img
Expand All @@ -70,25 +125,7 @@ export const Children: Story = {
</AvatarBase>
{/* Icon */}
<AvatarBase>
<Icon
name={IconName.User}
size={IconSize.Sm}
className="text-inherit"
/>
</AvatarBase>
</div>
),
};

export const ClassName: Story = {
render: () => (
<div className="flex gap-2">
<AvatarBase className="bg-success-default text-success-inverse">
S
</AvatarBase>
<AvatarBase className="bg-error-default text-error-inverse">E</AvatarBase>
<AvatarBase className="bg-warning-default text-warning-inverse">
W
<Icon name={IconName.User} size={IconSize.Sm} />
</AvatarBase>
</div>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,23 @@ import { render, screen } from '@testing-library/react';
import React from 'react';

import { AvatarBase } from './AvatarBase';
import { AVATAR_BASE_SIZE_DIMENSIONS } from './AvatarBase.constants';
import { AvatarBaseSize } from './AvatarBase.types';
import { AVATAR_BASE_SIZE_CLASS_MAP } from './AvatarBase.constants';
import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';

describe('AvatarBase', () => {
it('renders with default styles', () => {
render(<AvatarBase>A</AvatarBase>);

const avatar = screen.getByText('A');
expect(avatar).toHaveClass(
'inline-flex',
'items-center',
'justify-center',
'rounded-full',
'bg-background-alternative',
'text-default',
'uppercase',
);
expect(avatar).toBeInTheDocument();
});

it('applies size classes correctly', () => {
const { rerender } = render(
<AvatarBase size={AvatarBaseSize.Xs}>A</AvatarBase>,
);

Object.entries(AVATAR_BASE_SIZE_DIMENSIONS).forEach(([size, classes]) => {
Object.entries(AVATAR_BASE_SIZE_CLASS_MAP).forEach(([size, classes]) => {
rerender(<AvatarBase size={size as AvatarBaseSize}>A</AvatarBase>);
const avatar = screen.getByText('A');
const classArray = classes.split(' ');
Expand Down Expand Up @@ -71,4 +63,24 @@ describe('AvatarBase', () => {
const avatar = screen.getByText('A');
expect(avatar).toHaveStyle({ backgroundColor: 'red' });
});

it('applies correct shape classes', () => {
const { rerender } = render(
<AvatarBase shape={AvatarBaseShape.Circle}>A</AvatarBase>,
);

let avatar = screen.getByText('A');
expect(avatar).toHaveClass('rounded-full');

rerender(<AvatarBase shape={AvatarBaseShape.Square}>A</AvatarBase>);
avatar = screen.getByText('A');
expect(avatar).toHaveClass('rounded-lg');
});

it('uses circle shape by default', () => {
render(<AvatarBase>A</AvatarBase>);

const avatar = screen.getByText('A');
expect(avatar).toHaveClass('rounded-full');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,33 @@ import { Slot } from '@radix-ui/react-slot';
import React from 'react';

import { twMerge } from '../../utils/tw-merge';
import { AVATAR_BASE_SIZE_DIMENSIONS } from './AvatarBase.constants';
import { AVATAR_BASE_SIZE_CLASS_MAP } from './AvatarBase.constants';
import type { AvatarBaseProps } from './AvatarBase.types';
import { AvatarBaseSize } from './AvatarBase.types';
import { AvatarBaseShape, AvatarBaseSize } from './AvatarBase.types';

export const AvatarBase = React.forwardRef<HTMLDivElement, AvatarBaseProps>(
(
{ children, className, size = AvatarBaseSize.Md, asChild, style, ...props },
{
children,
className,
size = AvatarBaseSize.Md,
shape = AvatarBaseShape.Circle,
asChild,
style,
...props
},
ref,
) => {
const Component = asChild ? Slot : 'div';

const mergedClassName = twMerge(
// Base styles
'inline-flex items-center justify-center',
'rounded-full',
'bg-background-alternative',
'text-default uppercase font-medium',
shape === AvatarBaseShape.Circle ? 'rounded-full' : 'rounded-lg',
'bg-alternative',
'overflow-hidden',
// Size
AVATAR_BASE_SIZE_DIMENSIONS[size],
AVATAR_BASE_SIZE_CLASS_MAP[size],
// Custom classes
className,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export enum AvatarBaseSize {
Xl = 'xl',
}

export enum AvatarBaseShape {
/**
* Circular shape with fully rounded corners
*/
Circle = 'circle',
/**
* Square shape with slight rounded corners
*/
Square = 'square',
}

export type AvatarBaseProps = ComponentProps<'div'> & {
/**
* Required prop for the content to be rendered within the AvatarBase
Expand All @@ -49,4 +60,9 @@ export type AvatarBaseProps = ComponentProps<'div'> & {
* Should be used sparingly and only for dynamic styles that can't be achieved with className.
*/
style?: React.CSSProperties;
/**
* Optional prop to control the shape of the AvatarBase
* @default AvatarBaseShape.Circle
*/
shape?: AvatarBaseShape;
};
27 changes: 20 additions & 7 deletions packages/design-system-react/src/components/avatar-base/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ import * as AvatarBaseStories from './AvatarBase.stories';
The AvatarBase is the base component for avatars

```tsx
import { AvatarBase } from '@metamask/design-system-react';
import { AvatarBase, Text } from '@metamask/design-system-react';

<AvatarBase>A</AvatarBase>;
<AvatarBase>
<Text>A</Text>
</AvatarBase>;
```

<Canvas of={AvatarBaseStories.Default} />

## Props

### Shape

AvatarBase supports two shapes:

- `AvatarBaseShape.Circle` (fully rounded) - default
- `AvatarBaseShape.Square` (slightly rounded corners)

<Canvas of={AvatarBaseStories.Shape} />

### Size

AvatarBase supports five sizes:
Expand All @@ -34,23 +45,25 @@ AvatarBase can contain different types of content including text, images, and ic

<Canvas of={AvatarBaseStories.Children} />

### ClassName
### Class Name

Use the `className` prop to add custom CSS classes to the component. These classes will be merged with the component's default classes using `twMerge`, allowing you to:

- Add new styles that don't exist in the default component
- Override the component's default styles when needed

<Canvas of={AvatarBaseStories.ClassName} />

Example:

```tsx
// Adding new styles
<AvatarBase className="my-4 mx-2">A</AvatarBase>
<AvatarBase className="my-4 mx-2">
<Text>A</Text>
</AvatarBase>

// Overriding default styles
<AvatarBase className="bg-success-default text-success-inverse">S</AvatarBase>
<AvatarBase className="bg-success-default">
<Text color={TextColor.SuccessInverse}>A</Text>
</AvatarBase>
```

### Style
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { AvatarBase } from './AvatarBase';
export type { AvatarBaseProps } from './AvatarBase.types';
export { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Remove this file if it's not needed
export const AVATARNETWORK_CLASSMAP = {};

import { AvatarBaseSize } from '../avatar-base';
import { TextVariant } from '../text';

export const AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP: Record<
AvatarBaseSize,
TextVariant
> = {
[AvatarBaseSize.Xs]: TextVariant.BodyXs,
[AvatarBaseSize.Sm]: TextVariant.BodyXs,
[AvatarBaseSize.Md]: TextVariant.BodySm,
[AvatarBaseSize.Lg]: TextVariant.BodyMd,
[AvatarBaseSize.Xl]: TextVariant.BodyMd,
};
Loading

0 comments on commit befddee

Please sign in to comment.