Skip to content
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This repository contains a library of reusable React components extracted from t
Below is a list of the components available in this library. Each component has its own detailed documentation with usage examples and a complete list of props.

- [BBBAccordion](./src/components/Accordion/README.md)
- [BBBAvatar](./src/components/Avatar/README.md)
- [BBButton](./src/components/Button/README.md)
- [BBBCheckbox](./src/components/Checkbox/README.md)
- [BBBDivider](./src/components/Divider/README.md)
Expand All @@ -17,6 +18,7 @@ Below is a list of the components available in this library. Each component has
- [BBBScrollArea](./src/components/ScrollArea/README.md)
- [BBBSearch](./src/components/Search/README.md)
- [BBBSelect](./src/components/Select/README.md)
- [BBBSlider](./src/components/Slider/README.md)
- [BBBSpinner](./src/components/Spinner//README.md)
- [BBBTextAreaInput](./src/components/TextAreaInput/README.md)
- [BBBTextInput](./src/components/TextInput/README.md)
Expand Down Expand Up @@ -101,6 +103,7 @@ The following table lists the supported CSS variables for color overriding, extr
| `--color-icon-default-dark`| No | rgba(255, 255, 255, 0.35) |
| `--color-overlay` | No | rgba(0, 0, 0, 0.75) |
| `--color-shadow-default` | No | rgb(0 35 11 / 20%) |
| `--color-user-you` | No | #19237C |

**Example Usage**:
```css
Expand Down Expand Up @@ -132,7 +135,7 @@ const StyledDiv = styled.div`
```

`colors` is grouped the same way as the table above: `neutral`, `brand`, `semantic`, `background`,
`border`, `text`, `icon`, `hover`.
`border`, `text`, `icon`, `hover`, `overlay`, `shadow`, `user`.

## Installation

Expand Down
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
"require": "./dist/components/Accordion.js",
"default": "./dist/components/Accordion.js"
},
"./Avatar": {
"types": "./dist/types/components/Avatar/index.d.ts",
"node": "./dist/components/Avatar.js",
"import": "./dist/esm/components/Avatar/index.js",
"require": "./dist/components/Avatar.js",
"default": "./dist/components/Avatar.js"
},
"./Button": {
"types": "./dist/types/components/Button/index.d.ts",
"node": "./dist/components/Button.js",
Expand Down Expand Up @@ -92,6 +99,13 @@
"require": "./dist/components/Select.js",
"default": "./dist/components/Select.js"
},
"./Slider": {
"types": "./dist/types/components/Slider/index.d.ts",
"node": "./dist/components/Slider.js",
"import": "./dist/esm/components/Slider/index.js",
"require": "./dist/components/Slider.js",
"default": "./dist/components/Slider.js"
},
"./Spinner": {
"types": "./dist/types/components/Spinner/index.d.ts",
"node": "./dist/components/Spinner.js",
Expand Down Expand Up @@ -181,6 +195,9 @@
"Accordion": [
"dist/types/components/Accordion/index.d.ts"
],
"Avatar": [
"dist/types/components/Avatar/index.d.ts"
],
"Button": [
"dist/types/components/Button/index.d.ts"
],
Expand Down Expand Up @@ -211,6 +228,9 @@
"Select": [
"dist/types/components/Select/index.d.ts"
],
"Slider": [
"dist/types/components/Slider/index.d.ts"
],
"Spinner": [
"dist/types/components/Spinner/index.d.ts"
],
Expand Down
83 changes: 83 additions & 0 deletions src/components/Avatar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# BBBAvatar

The `BBBAvatar` component renders a user's avatar image, falling back to their initials on a deterministically-colored background when no image is available or the image fails to load. It shows a tooltip with the full name on hover, and can highlight the current user or a speaking user, matching BBB's own avatar treatment.

![Demo](assets/example.png)

## Usage Example

### Avatar with image
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" avatarUrl={user.avatar} />
```

### Avatar with initials fallback
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" />
```

### Avatar with a custom color
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" color="#0F70D7" />
```

### Medium avatar
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" size="medium" />
```

### Large avatar
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" size="large" />
```

### Moderator avatar
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" isModerator />
```

### Current user's avatar
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" isYou />
```

### Talking indicator
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" isTalking />
```

### Avatar without the hover tooltip
```jsx
import { BBBAvatar } from 'bbb-ui-components-react';

<BBBAvatar name="Arthur Kaminski" disableTooltip />
```

## Props

| Property | Type | Default | Description |
| ---------------- | -------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | | Full name of the user; used to render initials, to derive a deterministic fallback color, and as the tooltip content. |
| `avatarUrl` | `string` | | URL of the user's avatar image. Falls back to initials when omitted or if the image fails to load. |
| `color` | `string` | color deterministically derived from `name` | Background color behind the initials (and border color on the image). Overrides `isYou`. |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Size variant of the avatar. |
| `isModerator` | `boolean` | `false` | Renders a rounded-square shape instead of a circle, matching BBB's moderator avatar treatment. |
| `isYou` | `boolean` | `false` | Marks this avatar as belonging to the current user, applying BBB's "you" color in place of the fallback/computed color. Ignored when `color` is set. |
| `isTalking` | `boolean` | `false` | Shows a pulsing ring around the avatar, in its own color, matching BBB's talking indicator. |
| `disableTooltip` | `boolean` | `false` | Disables the tooltip that shows the full `name` on hover. |
Binary file added src/components/Avatar/assets/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions src/components/Avatar/component.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import BBBAvatar from './component';
import { AVATAR_SIZE_VALUES, DEFAULT_AVATAR_SIZE } from './constants';

const meta = {
title: 'BBBAvatar',
component: BBBAvatar,
tags: ['autodocs'],
argTypes: {
name: {
control: 'text',
description: 'Full name of the user; used to render initials and to derive a deterministic fallback color.',
},
avatarUrl: {
control: 'text',
description: "URL of the user's avatar image. Falls back to initials when omitted or if the image fails to load.",
},
color: {
control: 'color',
description: 'Background color behind the initials (and border color on the image).',
},
size: {
control: 'select',
options: AVATAR_SIZE_VALUES,
description: 'Size variant of the avatar.',
table: { defaultValue: { summary: `${DEFAULT_AVATAR_SIZE}` } },
},
isModerator: {
control: 'boolean',
description: "Renders a rounded-square shape instead of a circle, matching BBB's moderator avatar treatment.",
table: { defaultValue: { summary: 'false' } },
},
isYou: {
control: 'boolean',
description: 'Marks this avatar as belonging to the current user, applying BBB\'s "you" color in place of the fallback/computed color. Ignored when `color` is set.',
table: { defaultValue: { summary: 'false' } },
},
isTalking: {
control: 'boolean',
description: "Shows a pulsing ring around the avatar, in its own color, matching BBB's talking indicator.",
table: { defaultValue: { summary: 'false' } },
},
disableTooltip: {
control: 'boolean',
description: 'Disables the tooltip that shows the full `name` on hover.',
table: { defaultValue: { summary: 'false' } },
},
},
} satisfies Meta<typeof BBBAvatar>;

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

/** Falls back to initials when no image is given, next to an avatar rendering a provided image. */
export const Default: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<BBBAvatar {...args} name="Arthur Kaminski" />
<BBBAvatar {...args} name="Arthur Kaminski" avatarUrl="https://i.pravatar.cc/150?img=60" />
</div>
),
};

/** All size variants rendered side by side. */
export const Sizes: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}>
{AVATAR_SIZE_VALUES.map((size) => (
<BBBAvatar {...args} key={size} name="Arthur Kaminski" size={size} />
))}
</div>
),
};

/** The default circular shape next to the rounded-square shape used for moderators. */
export const Types: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<BBBAvatar {...args} name="Arthur Kaminski" />
<BBBAvatar {...args} name="Arthur Kaminski" isModerator />
</div>
),
};

/** Different names deriving different colors from the deterministic fallback, next to an explicit custom color override. */
export const Colors: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<BBBAvatar {...args} name="Arthur Kaminski" />
<BBBAvatar {...args} name="Beatriz Nunes" />
<BBBAvatar {...args} name="Carlos Eduardo" />
<BBBAvatar {...args} name="Diana Prince" />
<BBBAvatar {...args} name="Fernanda Lima" />
<BBBAvatar {...args} name="Gustavo Rocha" />
<BBBAvatar {...args} name="Isabela Torres" />
<BBBAvatar {...args} name="Arthur Kaminski" color="#0F70D7" />
<BBBAvatar {...args} name="Beatriz Nunes" color="#2DD36F" />
<BBBAvatar {...args} name="Arthur Kaminski" isYou />
</div>
),
};

/** Not talking next to the pulsing ring shown while the user is speaking. */
export const Talking: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '2rem', alignItems: 'center' }}>
<BBBAvatar {...args} name="Arthur Kaminski" />
<BBBAvatar {...args} name="Arthur Kaminski" isTalking />
</div>
),
};
72 changes: 72 additions & 0 deletions src/components/Avatar/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { JSX, useEffect, useState } from 'react';
import Tippy from '@tippyjs/react';
import 'tippy.js/dist/tippy.css';
import * as Styled from './styles';
import { AvatarProps } from './types';
import { DEFAULT_AVATAR_SIZE, AVATAR_FALLBACK_COLORS } from './constants';
import { colorUserYou } from '../../stylesheets/palette';

function getInitials(name: string): string {
const words = name.trim().split(/\s+/).filter(Boolean);
if (words.length === 0) return '';
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
return `${words[0][0]}${words[words.length - 1][0]}`.toUpperCase();
}

function getFallbackColor(name: string): string {
const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
return AVATAR_FALLBACK_COLORS[hash % AVATAR_FALLBACK_COLORS.length];
}

/**
* A user avatar component.
*
* Renders the user's avatar image when available, falling back to their initials on a
* deterministically-colored background when no image is provided or it fails to load.
*
*/
function Avatar({
name,
avatarUrl,
color,
size = DEFAULT_AVATAR_SIZE,
isModerator = false,
isYou = false,
isTalking = false,
disableTooltip = false,
}: AvatarProps): JSX.Element {
const [hasImageError, setHasImageError] = useState(false);
const resolvedColor = color || (isYou ? colorUserYou : getFallbackColor(name));

useEffect(() => {
setHasImageError(false);
}, [avatarUrl]);

const avatarElement = avatarUrl && !hasImageError ? (
<Styled.AvatarImage
src={avatarUrl}
alt={name}
$size={size}
$color={resolvedColor}
$isModerator={isModerator}
$isTalking={isTalking}
onError={() => setHasImageError(true)}
/>
) : (
<Styled.AvatarInitials $size={size} $color={resolvedColor} $isModerator={isModerator} $isTalking={isTalking}>
{getInitials(name)}
</Styled.AvatarInitials>
);

if (disableTooltip) {
return avatarElement;
}

return (
<Tippy content={name} placement="top">
{avatarElement}
</Tippy>
);
}

export default Avatar;
15 changes: 15 additions & 0 deletions src/components/Avatar/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const AVATAR_SIZES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large',
} as const;

export const AVATAR_SIZE_VALUES = Object.values(AVATAR_SIZES);
export const DEFAULT_AVATAR_SIZE = AVATAR_SIZES.MEDIUM;

// Mirrors the palette akka-bbb-apps' ColorPicker assigns to users server-side (round-robin per
// meeting), so a name-hash fallback here lands on the same colors real BBB users get.
export const AVATAR_FALLBACK_COLORS = [
'#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0', '#311b92',
'#3949ab', '#303f9f', '#283593', '#1a237e', '#1976d2', '#1565c0', '#0d47a1', '#0277bd', '#01579b',
];
1 change: 1 addition & 0 deletions src/components/Avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as BBBAvatar } from './component';
Loading
Loading