Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Avatar component #346

Merged
merged 1 commit into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/theme/src/components/avatar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { tv, type VariantProps } from '../tw';
const avatar = tv({
slots: {
base: [
'flex',
'relative',
'justify-center',
'items-center',
'box-border',
'overflow-hidden',
'align-middle',
'text-foreground',
'z-0',
'bg-default-200',
'ring-1 ring-default ring-offset-1 ring-offset-background'
],
img: 'size-full',
name: [
'w-full',
'font-normal',
'text-center',
'text-inherit',
'absolute',
'top-1/2',
'left-1/2',
'-translate-x-1/2',
'-translate-y-1/2',
'select-none'
]
},
variants: {
shape: {
square: 'rounded-[20%]',
circle: 'rounded-full'
},
size: {
xs: { base: 'size-5 text-xs' },
sm: { base: 'size-6 text-sm' },
md: { base: 'size-8 text-base' },
lg: { base: 'size-10 text-lg' },
xl: { base: 'size-12 text-xl' }
}
},
defaultVariants: {
size: 'md',
shape: 'circle'
}
});

export type AvatarVariants = VariantProps<typeof avatar>;
export type AvatarSlots = keyof ReturnType<typeof avatar>;
export { avatar };
1 change: 1 addition & 0 deletions packages/theme/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './avatar';
export * from './button';
export * from './chip';
export * from './progress-bar';
Expand Down
1 change: 1 addition & 0 deletions packages/utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"type": "addon",
"main": "addon-main.js",
"app-js": {
"./components/avatar.js": "./dist/_app_/components/avatar.js",
"./components/collapsible.js": "./dist/_app_/components/collapsible.js",
"./components/divider.js": "./dist/_app_/components/divider.js",
"./components/spinner.js": "./dist/_app_/components/spinner.js",
Expand Down
139 changes: 139 additions & 0 deletions packages/utilities/src/components/avatar.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Component from '@glimmer/component';
import {
useStyles,
type AvatarVariants,
type AvatarSlots,
type SlotsToClasses
} from '@frontile/theme';

interface AvatarSignature {
Args: {
/**
* Controls the size of the avatar.
*
* @defaultValue 'md'
*/
size?: AvatarVariants['size'];

/**
* Defines the shape of the avatar.
*
* @defaultValue 'circle'
*/
shape?: AvatarVariants['shape'];

/**
* URL of the image to be displayed in the avatar.
* If provided, the image will be used instead of initials.
*
*/
src?: string | null;

/**
* Full name of the user, used to generate initials.
* If `@firstName` and `@lastName` are not provided, initials will be
* derived from this property.
*/
name?: string;

/**
* First name of the user, used to generate initials.
* If `@name` is not provided, initials will be generated from
* `@firstName` and `@lastName`.
*/
firstName?: string;

/**
* Last name of the user, used to generate initials.
* If `@name` is not provided, initials will be generated from
* `@firstName` and `@lastName`.
*/
lastName?: string;

/**
* Alternative text for accessibility.
* If `@src` is provided, this text will be used as the `alt`
* attribute for the image.
* If only initials are displayed, this text will be read by screen readers.
*/
alt?: string;

/**
* Custom CSS classes for styling different slots within the avatar component.
*/
classes?: SlotsToClasses<AvatarSlots>;
};

/**
* The root element of the avatar component, which is an HTML `<span>` tag.
*/
Element: HTMLSpanElement;
}

class Avatar extends Component<AvatarSignature> {
get classes() {
const { avatar } = useStyles();
return avatar({
size: this.args.size,
shape: this.args.shape
});
}

get initials() {
const { name, firstName, lastName } = this.args;

if (name) {
return name
.trim()
.split(/\s+/)
.slice(0, 2)
.map((word) => word.charAt(0))
.join('')
.toUpperCase();
}

const initials = [firstName?.charAt(0), lastName?.charAt(0)]
.filter(Boolean)
.join('')
.toUpperCase();

return initials;
}

get shouldShowInitials() {
const { src, name, firstName, lastName } = this.args;
if (src) {
return false;
}

return Boolean(name || firstName || lastName);
}

<template>
<span
class={{this.classes.base [email protected]}}
data-component="avatar"
...attributes
>
{{#if this.shouldShowInitials}}
<span
aria-label={{@alt}}
class={{this.classes.name [email protected]}}
role="img"
>
{{this.initials}}
</span>
{{/if}}
{{#if @src}}
<img
class={{this.classes.img [email protected]}}
src={{@src}}
alt={{@alt}}
/>
{{/if}}
</span>
</template>
}

export { Avatar };
export default Avatar;
119 changes: 119 additions & 0 deletions packages/utilities/src/components/avatar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
label: New
---

# Avatar

The Avatar component is used to represent a user by displaying either their initials or an image. It supports customization of size and shape.

## Import

```js
import { Avatar } from '@frontile/utilities';
```

## Usage

### Basic

By default, the Avatar component will display initials derived from the `@name`, `@firstName`, and `@lastName` arguments.

```gts preview
import { Avatar } from '@frontile/utilities';

<template>
<div class='flex items-center space-x-4 py-2'>
<Avatar @name='Jon Snow' />
<Avatar @firstName='Arya' @lastName='Stark' />
</div>
</template>
```

### With an Image

If an `@src` is provided, the avatar will display the image instead of initials.

```gts preview
import { Avatar } from '@frontile/utilities';

<template>
<Avatar @src='https://i.pravatar.cc/150?img=5' @alt='Jon Snow' />
</template>
```

### Different Sizes

The `@size` property allows you to customize the avatar's size.

```gts preview
import { Avatar } from '@frontile/utilities';

<template>
<div class='flex items-center space-x-4 py-2'>
<Avatar @name='Jon Snow' @size='xs' />
<Avatar @name='Jon Snow' @size='sm' />
<Avatar @name='Jon Snow' @size='md' />
<Avatar @name='Jon Snow' @size='lg' />
<Avatar @name='Jon Snow' @size='xl' />
</div>

<div class='flex items-center space-x-4 py-2'>
<Avatar @size='xs' @src='https://i.pravatar.cc/150?img=1' />
<Avatar @size='sm' @src='https://i.pravatar.cc/150?img=2' />
<Avatar @size='md' @src='https://i.pravatar.cc/150?img=3' />
<Avatar @size='lg' @src='https://i.pravatar.cc/150?img=4' />
<Avatar @size='xl' @src='https://i.pravatar.cc/150?img=5' />
</div>
</template>
```

### Shapes

The `@shape` property changes the avatar shape.

```gts preview
import { Avatar } from '@frontile/utilities';

<template>
<div class='flex items-center space-x-4 py-2'>
<Avatar @name='Jon Snow' @shape='circle' />
<Avatar @name='Jon Snow' @shape='square' />
</div>
</template>
```

### Accessibility

To provide better accessibility, the `@alt` attribute should be used when displaying an image.

```gts preview
import { Avatar } from '@frontile/utilities';

<template>
<Avatar @src='https://i.pravatar.cc/150?img=5' @alt='User profile picture' />
</template>
```

If no `@alt` is provided, screen readers will read the initials.

### Custom Styling

You can pass custom `@classes` to override styling:

```gts preview
import { Avatar } from '@frontile/utilities';
import { hash } from '@ember/helper';

<template>
<Avatar
@name='Jon Snow'
@classes={{hash
base='bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white'
}}
/>
</template>
```

## API

<Signature @package="utilities" @component="Avatar" />
1 change: 1 addition & 0 deletions packages/utilities/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'focus-visible/dist/focus-visible.js';

export * from './components/avatar';
export * from './components/visually-hidden';
export * from './components/collapsible';
export * from './components/divider';
Expand Down
Loading
Loading