From 3d9d10b3e6be62b1b593fc489252d5af82010ee8 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 2 Sep 2026 19:46:11 -0300 Subject: [PATCH 1/7] fix(BBButton): make `size` resize `circle` and `squared` layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `circle` hardcoded a 3rem width/height regardless of `size`, and `squared` hardcoded its padding, overriding the size-derived padding from commonButtonStyles — both contradicted the `size` doc, which claims it affects padding and the circle's diameter. Add a shared `iconBoxSize` per size to CSS_SIZE_PROPERTIES and an `iconOnlyBoxStyles` fragment reused by both layouts, so `circle` and `squared` always render at the same fixed box size for a given `size` (sm: 2rem, md: 3rem, the latter promoted to a new `spacingXXLarge` token to match the previous circle default). This changes squared's default look: it no longer sizes itself from icon + padding, and now matches circle's box size instead. --- src/components/Button/component.stories.tsx | 40 ++++++++++++++++++++- src/components/Button/constants.ts | 8 +++-- src/components/Button/styles.ts | 29 ++++++++------- src/components/Button/type.ts | 3 +- src/stylesheets/sizing.ts | 1 + 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/components/Button/component.stories.tsx b/src/components/Button/component.stories.tsx index 8020311..83d251f 100644 --- a/src/components/Button/component.stories.tsx +++ b/src/components/Button/component.stories.tsx @@ -55,7 +55,7 @@ const meta = { size: { control: 'select', options: SIZE_VALUES, - description: `Button size. Allowed values: ${SIZE_VALUES.join(', ')}. Note: size affects padding and, for \`circle\` layout, the overall diameter.`, + description: `Button size. Allowed values: ${SIZE_VALUES.join(', ')}. Note: size affects padding and, for \`circle\`/\`squared\` layouts, the overall box size (both render at the same size).`, table: { defaultValue: { summary: `${DEFAULT_SIZE}` } }, }, layout: { @@ -323,6 +323,44 @@ export const AllSizes: Story = { ), }; +/** Renders the icon-only `circle` and `squared` layouts across every available `size`, showing that both render at the same box size for a given `size`. */ +export const IconOnlySizes: Story = { + name: 'Icon-Only Layout Sizes', + args: { + variant: 'primary', + }, + render: (args) => ( +
+
+ {SIZE_VALUES.map((size) => ( + } + ariaLabel={`Circle ${size}`} + tooltipLabel={`Circle ${size}`} + /> + ))} +
+
+ {SIZE_VALUES.map((size) => ( + } + ariaLabel={`Squared ${size}`} + tooltipLabel={`Squared ${size}`} + /> + ))} +
+
+ ), +}; + /** Shows the button in its disabled state. */ export const Disabled: Story = { args: { diff --git a/src/components/Button/constants.ts b/src/components/Button/constants.ts index 22517be..5df5521 100644 --- a/src/components/Button/constants.ts +++ b/src/components/Button/constants.ts @@ -15,7 +15,9 @@ import { colorGray, colorBrandAux, } from '../../stylesheets/palette'; -import { spacingMedium, spacingSmall, spacingSmallMedium } from '../../stylesheets/sizing'; +import { + spacingSmall, spacingSmallMedium, spacingMedium, spacingXLarge, spacingXXLarge, +} from '../../stylesheets/sizing'; import { CSSColorPropertiesType, CSSSizePropertiesType, EssentialColorProperties } from './type'; const VARIANTS = { @@ -193,9 +195,11 @@ const CSS_COLOR_PROPERTIES_DISABLED: EssentialColorProperties = { const CSS_SIZE_PROPERTIES: CSSSizePropertiesType = { sm: { padding: `${spacingSmall} ${spacingSmallMedium}`, + iconBoxSize: spacingXLarge, }, md: { - padding: spacingMedium + padding: spacingMedium, + iconBoxSize: spacingXXLarge, }, }; diff --git a/src/components/Button/styles.ts b/src/components/Button/styles.ts index d384e80..b44c596 100644 --- a/src/components/Button/styles.ts +++ b/src/components/Button/styles.ts @@ -119,10 +119,15 @@ const stackedLayoutStyles = css` align-items: center; `; -const circleLayoutStyles = css` - border-radius: 50%; - width: 3rem; - height: 3rem; +// Shared by `circle` and `squared`: both are icon-only, fixed-box layouts that +// must render at the same width/height for a given `size` — they differ only +// in border-radius (round vs. slightly rounded square). +const iconOnlyBoxStyles = css` + ${({ $size }) => { + const safeSize = $size ?? DEFAULT_SIZE; + const { iconBoxSize } = CSS_SIZE_PROPERTIES[safeSize] ?? CSS_SIZE_PROPERTIES[DEFAULT_SIZE]; + return `width: ${iconBoxSize}; height: ${iconBoxSize};`; + }} aspect-ratio: 1; padding: 0; display: flex; @@ -130,16 +135,14 @@ const circleLayoutStyles = css` align-items: center; `; -// Icon-only, sized by its own (equal, non-text) padding rather than a fixed -// box — stays compact next to a single line of text instead of forcing a -// fixed height like circle/stacked do. -const squaredLayoutStyles = css` +const circleLayoutStyles = css` + ${iconOnlyBoxStyles} + border-radius: 50%; +`; + +const squaredLayoutStyles = css` + ${iconOnlyBoxStyles} border-radius: ${borderRadiusSmall}; - aspect-ratio: 1; - padding: ${spacingSmall}; - display: flex; - justify-content: center; - align-items: center; `; const defaultLayoutStyles = css` diff --git a/src/components/Button/type.ts b/src/components/Button/type.ts index 30c9281..bc38ae0 100644 --- a/src/components/Button/type.ts +++ b/src/components/Button/type.ts @@ -30,6 +30,7 @@ export type CSSColorPropertiesType = { export type SizeProperties = { padding: string; + iconBoxSize: string; } export type CSSSizePropertiesType = { @@ -97,7 +98,7 @@ type BaseButtonProps = { /** Visual variant controlling emphasis and background/border styling. @default 'primary' */ variant?: VariantType; - /** Button size; affects padding and, for the `circle` layout, the overall diameter. @default 'md' */ + /** Button size; affects padding and, for the `circle`/`squared` layouts, the overall box size (both render at the same size). @default 'md' */ size?: SizeType; /** Disables interaction and applies disabled styling. @default false */ diff --git a/src/stylesheets/sizing.ts b/src/stylesheets/sizing.ts index 1e0c76a..af3ef60 100644 --- a/src/stylesheets/sizing.ts +++ b/src/stylesheets/sizing.ts @@ -8,6 +8,7 @@ export const spacingMedium = '1rem'; // 16px export const spacingMediumLarge = '1.25rem'; // 20px export const spacingLarge = '1.5rem'; // 24px export const spacingXLarge = '2rem'; // 32px +export const spacingXXLarge = '3rem'; // 48px // Border sizes export const borderRadiusDefault = '1rem'; //16px From 9523721f2e88d6b8b8729d25497ab9e97eb5eeae Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 2 Sep 2026 19:52:06 -0300 Subject: [PATCH 2/7] feat(BBBModal): add hideCloseButton prop to hide header close button BBBModal always renders a close button in the header, with no way to suppress it. This blocks use cases that require a non-dismissible modal, such as mandatory-action dialogs or modals whose closing is controlled entirely by external logic. Add a hideCloseButton prop (default false) that conditionally renders the close button, following the same pattern already used by noFooter. --- src/components/Modal/README.md | 1 + src/components/Modal/component.stories.tsx | 13 +++++++++++++ src/components/Modal/component.tsx | 19 +++++++++++-------- src/components/Modal/types.ts | 3 +++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/components/Modal/README.md b/src/components/Modal/README.md index 0558370..769c640 100644 --- a/src/components/Modal/README.md +++ b/src/components/Modal/README.md @@ -39,6 +39,7 @@ import { BBBModal } from 'bbb-ui-components-react'; | `shouldCloseOnEsc` | `boolean` | `false` | Allows closing with ESC key. | | `allowScroll` | `boolean` | `true` | Enables scroll in the modal body. | | `noFooter` | `boolean` | `false` | Hides the modal footer. | +| `hideCloseButton` | `boolean` | `false` | Hides the header close button. | | `footerContent` | `React.ReactNode` | `null` | Custom content for the footer. | | `stickyFooter` | `boolean` | `true` | Makes the footer sticky. | | `testId` | `string` | — | Test identifier applied to the modal element (native `ReactModal` prop). | diff --git a/src/components/Modal/component.stories.tsx b/src/components/Modal/component.stories.tsx index 9367b1e..731eb33 100644 --- a/src/components/Modal/component.stories.tsx +++ b/src/components/Modal/component.stories.tsx @@ -59,6 +59,10 @@ const meta = { control: 'boolean', description: 'Hide the footer section.', }, + hideCloseButton: { + control: 'boolean', + description: 'Hide the header close button.', + }, footerContent: { control: false, description: 'Custom React node to render inside the footer.', @@ -190,3 +194,12 @@ export const Default: Story = { }, render: (args) => , }; + +/** Modal without a header close button, for mandatory-action or externally-controlled flows. */ +export const HideCloseButton: Story = { + args: { + ...Default.args, + hideCloseButton: true, + }, + render: (args) => , +}; diff --git a/src/components/Modal/component.tsx b/src/components/Modal/component.tsx index b1d9d8b..6376cf1 100644 --- a/src/components/Modal/component.tsx +++ b/src/components/Modal/component.tsx @@ -25,6 +25,7 @@ const Modal: React.FC = ({ shouldCloseOnEsc = false, allowScroll = true, noFooter = false, + hideCloseButton = false, footerContent = null, stickyFooter = true, testId, @@ -65,14 +66,16 @@ const Modal: React.FC = ({ > {title} - } - onClick={onRequestClose} - variant="subtle" - ariaLabel="close" - {...(_closeButtonDataTest ? { dataTest: _closeButtonDataTest } : {})} - /> + {!hideCloseButton && ( + } + onClick={onRequestClose} + variant="subtle" + ariaLabel="close" + {...(_closeButtonDataTest ? { dataTest: _closeButtonDataTest } : {})} + /> + )} {showDividers && } diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index d82a8f3..5cc7b41 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -28,6 +28,9 @@ export interface ModalProps extends Omit { /** Hide the footer section. @default false */ noFooter?: boolean; + /** Hide the header close button. @default false */ + hideCloseButton?: boolean; + /** When true, the footer is sticky to the bottom instead of scrolling with the body. @default true */ stickyFooter?: boolean; From eceb66a2a871d9b4f823532b8e5abf4b24466343 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 2 Sep 2026 19:52:17 -0300 Subject: [PATCH 3/7] chore(BBBModal): remove unused CloseButton styled-component styles.ts defines a CloseButton styled-component that is never referenced: the modal's close button has always been rendered via BBButton in component.tsx instead. Remove the dead code. --- src/components/Modal/styles.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/Modal/styles.ts b/src/components/Modal/styles.ts index 708c871..1002ac9 100644 --- a/src/components/Modal/styles.ts +++ b/src/components/Modal/styles.ts @@ -45,15 +45,6 @@ export const ModalHeader = styled.div` align-items: center; `; -export const CloseButton = styled.button` - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - padding: 0; - line-height: 1; -`; - export const ModalScrollArea = styled.div` flex-grow: 1; overflow-y: ${({ $allowScroll }) => $allowScroll ? 'auto' : 'hidden'}; From 3d1ad707731a8e3423a47ca5d7fcecfb08e445c5 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 2 Sep 2026 20:01:21 -0300 Subject: [PATCH 4/7] fix(BBBCheckbox): respect caller-provided id prop BBBCheckbox always rendered with an internally generated id from useId(), because {...props} was spread before the explicit id attribute, so the generated value silently overwrote any id passed by the caller. This made it impossible to target the rendered input by a known id and broke the label/htmlFor association whenever a custom id was expected. Destructure id from props and fall back to useId() only when the caller does not provide one, reusing the resolved value for both the checkbox input and the label's htmlFor attribute. --- src/components/Checkbox/component.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Checkbox/component.tsx b/src/components/Checkbox/component.tsx index 808740e..5356e8e 100644 --- a/src/components/Checkbox/component.tsx +++ b/src/components/Checkbox/component.tsx @@ -17,13 +17,15 @@ function Checkbox({ layout = DEFAULT_LAYOUT, round = false, ariaLabel, + id, ...props }: CheckboxProps): JSX.Element { - const id = useId(); + const generatedId = useId(); + const checkboxId = id ?? generatedId; const checkbox = ( : undefined} checkedIcon={round ? : undefined} @@ -36,9 +38,9 @@ function Checkbox({ return ( - {layout === LAYOUTS.LEFT && {label}} + {layout === LAYOUTS.LEFT && {label}} {checkbox} - {layout === LAYOUTS.RIGHT && {label}} + {layout === LAYOUTS.RIGHT && {label}} ); } From 78cf86be50fd031fbf3b2bbb03c835d3fcab0fe9 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 2 Sep 2026 22:11:12 -0300 Subject: [PATCH 5/7] feat(BBBCheckbox): add dataTest prop for the underlying input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BBBCheckbox has no way to tag its rendered for test selectors. Spreading an arbitrary data-test through {...props} does not work: MUI's SwitchBase only forwards explicitly recognized props (id, checked, name, inputProps) to the actual , so anything else spread through falls into the outer wrapper instead — the same class of issue fixed for id in the previous commit. Add an explicit dataTest prop, matching the convention already used by BBButton and BBBInput, and route it through MUI's inputProps so it lands on the real , merging with any inputProps the caller already passes directly. --- src/components/Checkbox/README.md | 1 + src/components/Checkbox/component.stories.tsx | 4 ++++ src/components/Checkbox/component.tsx | 4 ++++ src/components/Checkbox/types.ts | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/components/Checkbox/README.md b/src/components/Checkbox/README.md index 5f219a0..4464c4a 100644 --- a/src/components/Checkbox/README.md +++ b/src/components/Checkbox/README.md @@ -28,4 +28,5 @@ import { BBBCheckbox } from 'bbb-ui-components'; | `layout` | `keyof typeof LAYOUTS` | `'right'` | The position of the label relative to the checkbox. | | `round` | `boolean` | `false` | If `true`, the checkbox will be rendered as a round radio-style button. | | `ariaLabel`| `string` | | The accessible name for the checkbox. | +| `dataTest` | `string` | | The `data-test` attribute on the underlying `` element. | | `...props` | `any` | | Any other props will be passed down to the underlying Material-UI Checkbox component. | diff --git a/src/components/Checkbox/component.stories.tsx b/src/components/Checkbox/component.stories.tsx index d5d14ac..c6f7d22 100644 --- a/src/components/Checkbox/component.stories.tsx +++ b/src/components/Checkbox/component.stories.tsx @@ -25,6 +25,10 @@ const meta = { control: 'text', description: 'Accessible name for the checkbox.', }, + dataTest: { + control: 'text', + description: '`data-test` attribute on the underlying `` element.', + }, }, } satisfies Meta; diff --git a/src/components/Checkbox/component.tsx b/src/components/Checkbox/component.tsx index 5356e8e..4e52e96 100644 --- a/src/components/Checkbox/component.tsx +++ b/src/components/Checkbox/component.tsx @@ -18,10 +18,13 @@ function Checkbox({ round = false, ariaLabel, id, + dataTest, + inputProps, ...props }: CheckboxProps): JSX.Element { const generatedId = useId(); const checkboxId = id ?? generatedId; + const mergedInputProps = { 'data-test': dataTest, ...inputProps }; const checkbox = ( : undefined} checkedIcon={round ? : undefined} + inputProps={mergedInputProps} /> ); diff --git a/src/components/Checkbox/types.ts b/src/components/Checkbox/types.ts index bd7298c..c61e065 100644 --- a/src/components/Checkbox/types.ts +++ b/src/components/Checkbox/types.ts @@ -15,4 +15,7 @@ export interface CheckboxProps extends MuiCheckboxProps { /** Accessible name for the checkbox. */ ariaLabel?: string; + + /** `data-test` attribute on the underlying `` element. */ + dataTest?: string; } From c796cb4c675d5d865c51ba72ef0f3c446aa06f06 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Thu, 3 Sep 2026 14:27:14 -0300 Subject: [PATCH 6/7] feat(BBBAvatar): add avatar component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library has no reusable component for rendering a user avatar. Every BBB surface that needs one — e.g. bbb-plugin-pick-random-user's user-avatar — reimplements the same image/initials/color logic locally, causing duplicated code and visual inconsistency across plugins and apps. Add BBBAvatar (src/components/Avatar), using bbb-plugin-pick-random-user's user-avatar as the reference implementation, extended to mirror BBB's own conventions rather than inventing new ones: - Renders the avatar image, falling back to initials on a colored background when no image is given or it fails to load. - The fallback color is hashed deterministically from the user's name into the same 16-color palette akka-bbb-apps' ColorPicker assigns server-side, so the fallback lands on colors real BBB users get. - small/medium/large size variants (medium default). - isModerator renders the rounded-square shape BBB uses for moderators, independent of color. - isYou applies BBB's own "you" color (colorUserYou, added to the shared palette) in place of the fallback, overridden by an explicit color prop. - isTalking shows a pulsing ring in the avatar's own color, mirroring BBB's talking indicator (ported to use CSS's native color-to-transparent interpolation instead of manual alpha-string concatenation, so it works with both hex colors and CSS custom properties). - A Tippy tooltip shows the full name on hover; disableTooltip opts out. --- README.md | 4 +- package.json | 10 ++ src/components/Avatar/README.md | 83 +++++++++++++++ src/components/Avatar/assets/example.png | Bin 0 -> 29282 bytes src/components/Avatar/component.stories.tsx | 112 ++++++++++++++++++++ src/components/Avatar/component.tsx | 72 +++++++++++++ src/components/Avatar/constants.ts | 15 +++ src/components/Avatar/index.ts | 1 + src/components/Avatar/styles.ts | 48 +++++++++ src/components/Avatar/types.ts | 36 +++++++ src/components/index.ts | 1 + src/stylesheets/colors.ts | 4 + src/stylesheets/palette.ts | 3 + webpack.config.babel.js | 1 + 14 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 src/components/Avatar/README.md create mode 100644 src/components/Avatar/assets/example.png create mode 100644 src/components/Avatar/component.stories.tsx create mode 100644 src/components/Avatar/component.tsx create mode 100644 src/components/Avatar/constants.ts create mode 100644 src/components/Avatar/index.ts create mode 100644 src/components/Avatar/styles.ts create mode 100644 src/components/Avatar/types.ts diff --git a/README.md b/README.md index 223b759..dbf0055 100644 --- a/README.md +++ b/README.md @@ -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) @@ -101,6 +102,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 @@ -132,7 +134,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 diff --git a/package.json b/package.json index bea56d7..b56e4f4 100644 --- a/package.json +++ b/package.json @@ -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", @@ -181,6 +188,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" ], diff --git a/src/components/Avatar/README.md b/src/components/Avatar/README.md new file mode 100644 index 0000000..1d98e3f --- /dev/null +++ b/src/components/Avatar/README.md @@ -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'; + + +``` + +### Avatar with initials fallback +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Avatar with a custom color +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Medium avatar +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Large avatar +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Moderator avatar +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Current user's avatar +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Talking indicator +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +### Avatar without the hover tooltip +```jsx +import { BBBAvatar } from 'bbb-ui-components-react'; + + +``` + +## 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. | diff --git a/src/components/Avatar/assets/example.png b/src/components/Avatar/assets/example.png new file mode 100644 index 0000000000000000000000000000000000000000..7a9463aaa2e68c1a5cbebf842f749eeaa3967ea3 GIT binary patch literal 29282 zcmd3t<98)d7v_`h*tTukys_<$ZM$Q;V|DDLW7{@vY&#uwH20mEcg>o=VCKX9QdR5J zseP)>-TU|KCrVjS3K0$u4h#$oQAS!q6$}hK4fLCW`35@Toq($h28IGABO$8pm22B4 zESK->bMbKWPfD2-F7A)OX=umA?8D;j*T-sKre%j#axgFwDmKvF27+O}gQ4I^;DG(? zBN+e#gMdPX0wZCGWB~&U5)B0hgFzre02BFvVhjfME#&{BOUq%vc6pBw&nK6oCz~fG zIVWuD0Trwj?iS743~IP{p5I>)h+;-e7a{b0^l@6Xs())$qkLs!jEna%Qgm*`gMnS6 zP5!l%EiQ}6zU@7!<2Z}cJmljFS?{%++Z7^x;i90V(C(Ay;{`oJgX39d zGS#7Vh!6A*cge}-JS+ngWRM3aO1oL_EIJFbG?|@C-2k1B5-BJKml5bz% zKq0g=$CecWs^6ZR`7E_lA;$ZB*8Z(-QigB^UEe)wP5*LR?C_W=Y<(@&W1yT|H$4^sFKTK0?$>wXvP zVx%c~St<7ug8p|rROe5i1#OFhFPVO>;n4+_i}uiw2EPs37QH>gh;8A6mi+6dFv{24 z)xyS3eyg_sN0Der;)|B^8mzk>=+WO5$hZaG=kLT%@KK#(@loy`W5~t7cbwU2 zgCI6(`ZQ)I=A&n@T(;Qz`hno^hGamb)%DtC5q~F7a;tB}R0IDhmAn_q-}tw0zrhdk zyfqc_mBQtD;@2f8GU{*shl4UOLQb1#|=2*UY-Rt-J z7$jM4+clb%^fx88jB4upVP45(-TKl3oz=O+wt}>WM8Z}Q4|yC&z`nX5Y_X@t!&@;S=`oEwx{UL~ysh2+ks{ie_|4V^1DFS*NAeFe&`PsfD zi;^dd!4T);^7^U^#B(jt#-GG=SOyfN<$ujW(_Lp~guWwzLKwk8A4hCA_2#gO**JVW zGU#k@Igjg~mzTW&iyfHzzMs!ytl}9)Z(`Y-c@`K$E*1*Ty%(@Q+k#R1ucrJUU2Uky zLTN_P-$oHGK|y>wsOpEB(RxHj>&`Y|;D$(GCm5Opj?-wW?$MSbaZii=nsc`aWcD^u zI!eE|;oNl9kDBnZh^tpCw0mY2GFW|&7qNpiOAlYJ-anI^vhPj6usmcueaMh^F~+jy z{HM&Sac?-#c+;MpOtU^8KXokuh9qjZW!(*5C*O(kls{8?XO%s-*A28113@nnQ{UFp ztbg9c^0LC+pFZ+0!PndqA?vm(hmAWM2;Fz*hV||s?0iU6sGL++nfv{7{UzF4c<)Nd zcIq-s%gfWBAv=8`EP;8o;sFq32s|Zv499z}k1GTICY(8C(k2YPVbvlzy=}$j+i>=G;Qepw^~KK9yCAlm zOMp(Tf4)3X7TUrLl2^IsuK0#ymr<8LX^iv31FjFl8bjJy(jpFr%ax<2=Qt2`8?5mG zwxUb)`S$p-NQ?mS;eK@$1ojYBp1MOF6U4 z>X}KD+FIlZU{%C)9>+`#tYe7naf8gLSxL}CsU>huI*Zjx8#`S2N8-Zc7s=a9Y zWht`XQy|da7XtATQ=lz@9J~Zh+#v~q80zSeE^IwgKgjf1KFZAuz;h3;3{Q^Z_q+Fg zIrF3pYG^f;5o*;K}G3Wf@#0r`cd!3;SKF8)R9+)QoC>SVZPX~K6u1pYbpIHb-7cx z;wUxDpZ`9hzf8mk$9QaMu^PIhFHx@uq}p|Rv|`fVq^~@Y>viu+s8_J(Xy=lZ!7`C8&YAh> z&D1NJsqIB~d(KT1QrDF+uS3Y~u{uqnuRS-+NEV|A;qJhkOXYYwe#0w6i1MaVQ*S1> zEITjLSeu*uQH!Vc>(8@6Q|?}N!*)yOt(DGIX2Lk(#F}SjgUvdN$@dsr%kHn!-%t6t z*n?&s?Myo4PixRbMJnSjal3IX@HTeBchALND{c?fpjeL|0pG>)KKI;6|8wKfCFO|j zFSQHufvymsR#Y*ppl~1F;|PY*n>uENX+||vX4h)$qmmtU*z3(UOOK(!)ay+ zR46`Z2VpEtpC4ja*Dp>*rW++KBZPyd&eW(5dgdiz;m{2Z?ZVeuK0XfTgmu?iRykSN zKBa3%kXos_N`^2VGZ_#|XruPeIkeU`F^y@kCW46(`sqeQymEVdCf*Sv-hL6SWOh3R z2l%b;_1Eb&2|~SYqxliHg65dZ2?DJc;98dgNA5~b77 zcQK-mJvYWWi46II%?{Tc1NO2_{=ld6gmip_c%Y05s`0FFJp{NGhWe)8+Uc#OS~35% zwZVb0_hWd= zVRJH>cU2yKi_>e;hy`}3ta>M^ab8u}G;x09g-)4V@hazOz5o>nfv47JLWj5Xa^PR7tWQ6W zbWZ4Js<&__)4DPEO{cf64Kdwl=~CC5qL$kaBd^M8$}SVBX+ZsyPtXW|rPXZBfY!?8$`HzQ0c!Xj$2bR==8gs8d+q3>fZV0r#U;$~P<9X3DGtL}$ zDxgHM*S|}%?92Qj9y{O1Y8d~k)pp0Yd;4}HUiij{1$zeyX-s5mLqSgo-cim6lzF7i z2>7aRHa|n(H(JgG~PbH%b3D@6|5?EI;_9Jw*%XX3S0@7h2g(tV)kjo$H?E$`Xm zUMUrXv@ve|`Q7R$x`d~!=k;z?Y1J?As&(Il0GmX+(c>W5_qte>hSMFfZ6%z4=9ZC^ z2~f0L@^pPSety&PUe8U&0pac#o;^)*ib+33n|v*x!A?Pm5!kVQ9e8$)@DBaH?E9}^ zCQP`g6)}4K@9M3-t>b)Cbq4-(EaDNv$G^+VRb)^GB3Js9!^VM!+P^O&cW#N2TP zt0#O#@2D3-BQci4N`}u2F`u8=nor6TrB#KuXF z^*nm?#e)Aly0LaAiq1f9qXTQEc@X=Ph%WPH1)~1O$W=c5ZU1?{b6dde%Xf5r<#A-b{oku`+d{N} zLf_pXMk3Q;DvT|PjLWQ{-4+*4oZq%ElDXe(xWWdJUIUmTF<)9P3U=J{Xln~$!>oRv zXV2sPqDvU{$s@{q9ID6jOR?0K@BMP!%x3F8j)PuvK!>jHuSdY-LzT-d%h!VM+p`=} z*L6?BzkVld&jT(tEXjR%9qB~bq!CoPS^95E5>1kGNpW?=$r$@XTT&kJnF`%nT2sV=7xTaLa`TmCpIr*X7#76U6i9zXSQ zSJ2b{7N{k_WU2i17gx=J-=C?jxJkc2eb0WW8}S_79p$=)jlU{H8lcUH-@0-SKAI zgQo8{QTTcJ_U)ni^@ks(t1~=rAKLDkHseL?KCJ;jYQ>jc%Lby)u@CfNheK{usEe@I zrp4g}*x4)5Jg5B_>)O(_NuxZS#+tSd|Nj()ch|A_@R@JRgfAi7w*^=BDK4!sga2g# z!r301u02iO`wxLPQ4gM96SROv&Wm~BTONxy=D%xGLd2Wr0>u}xz825Tfnb|IzjwGD z7!-il8dz^;Vh%a98{%A3u04;N3AAi@%z~D`ck8TPL)_`=e$G-2<*vW7M#$bNKe3>q zz@b{jnNVNBwFoO*SlD)b3p^oxnke_Du-#4<$A0Fli*8**(1rw$tu8k0<{!7IAlFA= zIz^j_SW&Xtei;4^!NXQ9J@hOuU&5`_M>isN`j#+Q!jL_$zW0Gd;ssoYKE}DvGtk|M zn|OwKGy1amhwEER-4-X;-bT7qi$iNIiARNLnTEFg)9?)u@7?fU67ZtOJWxidu1z{f z&O3(A_c?fnsto_&KKdZ}pK62eI8Cl)2ZP8{DW`QDyA~LBP-nB)wNlYF|a&r{Y-hW5ET+CMd z)lIqi5its#)Tz6gG2%{7y=t|cYOq-S-r0*lO>&?3rb*EG9B_m6+mk`$;CIQ6^2 zy=_`nUlZe?g7$93|Klzw17mz}l(lCsj`f3l+neNJU|q*60+i_rx{JbXR(!p8xh+W= zX5`P@^i{Bsvu|FC+jVbWJ)ti%#jO$Z?a*`_yZz-g-A_94YdKnIhL3!XJPv$bJiY7lg)UpN*VQ%r?LF^XYJ4` zz=R@j^Cyg-l1ggfA5}y874;a-jd-oEjc=TwYN_ZgY&6{Yj@_&zym|U?z)8t3PFsy~ zA%`}JsGDX$>CI8=p%%mXykuQB3hQL4S0Q`1(OOC<-{;B(>m{fy(fqMeSW$B|GFMk0 z;mf{ilY=>NR3rc#RNG2c&Lsrcv_5O@cB-heyuDOf_Y*n{DhAxmf|!EklfIYULKb%T z*M{e)P~+QX@6@Bw=Z|9rrJCNAdV%?-^k-gn(VH1=(m-%ft?gc4?Y+s~xq+2-NB54p^)-IjGfsrVx$&p&{Z2`d>^6OQ z7P;5b1nnMs4jU?GnVI*rXf)!tVvwyk(Kz!`gL?eN*7hfK!tE5^Q6{Xd5Qwd{?FLl{ zY6)=`ljo|nhUULNST-KV#!C?mn5*#f#7yMB82uIx1z#0lXkJ!ITr5840eg{CtH0TwRvW|Bar%_Fu4R+T0K0>1 z)AxH5X%yqW6a=b^C_iV#q+-o#YpWGb4qk_sfE=}WJrsTrEa0GYlK^O^=UDiG3Q@gW z?f1#Azj8){L%JaPge|-uDCkh8&`_08sNZx}O zV{}KR)b;6A-D}uyET|6r6rji3*VWZ-Y8(ti>&|Qdd&=bf#sm6)&SCS#bv-Ss5M{#e zzumqZa5Kl<9=wZz2pxVj@=3tG8FlU@u-VXXZuRT9!4R4Z!A;6_l!75+_r#KL&v-`* z%Yq~j8id@k7w{Oo+!}ghBHmGn+IarBOf<(Z|D!Jt3v!4aEdFe;%h}O~JkSmD8#z8Y z=oAkCRhDzPj|xcdd&?>TTze_9q#ysD+HA4Qo*Y5<*2_-DT%0jkl=ur|*AegfvmP-E z0^wU*?{`sOzEqRQ=O$)sXE7(?j9D469q6Ys(0#^A4qKq=Gh^zyx^)l!l;eJcdsLC& za9}w3<2wkYY7tJNGy0wUQMCm~3|cPU&JDqMGH1zH{Rg7fR2#a_-jhpez)28*$Zc;I zClhOOLvV zfQJQ`Uj5-YO3;{_`nl?F|F|s!g*D^+W~Y*zZv@$BDjTu79?d~#n@&z8=NI#HEA5s@ zcIL>lzJQB5Ta725rT+oKkd4D2ZnbQg4d2<4-K&&yTftlLFhc3|s5%t-9TnM!w~{#!uly%RD+^VZ+AA?>8rbCs?whEGG2a~vO9+& z5TBq=2+V=cfMLt0$;MOBK6NZb(EM(16PJA7A}*SEZqX6D>zM>J2w$acH88OBv#&ke z6dQjlJEGrQMd~pIN0`KAf497ih3T6jNNeaznZRF4xBZoFi}7MaU@sddQ^Q{MXpuP) zsjGRvUvDMArO;P$P=1a{e~L+dib*YhUh8_nkMuwF3Kl;2|7R*jIm`=ry?ifwXG^gz zY1)M(Teo&8`Xs{3=Pik*6&St1obv@bbq*H;m&BNa0Y$5JmH`b@c_OwJEgb#PSoUqU zf*it_G~ARdBH<=A1b85L`oJSSXzvDL5Rbe{UbTh8LvfG{kjcmJ6u(y=mFp#H9X9g! zy>gRLicUo>V9$19-%VcW_g~W zqvO0`5fU2;k(eANwv+`zH}}J&+gvG6!M5ukLJE`=qrqmcqYKG>!xpc*Qmw#pF-NG2 zLJB(5C}fIQ8v;@)lcOs4IEGoVtaL0)wEO~zjk<{=ICpVrbxVEeio*(i4PW+fY)Dys zJt_5#9pG4`faTZZ(YjlfMXI#T9Eppzsk^&@zd4}paGV@aIfWG^i%yj8M0eumpr~kL zLu}}@@Y&D?fTY##(|;IndQU?o;jxlN+b)rP-Pkc&^X))#EE{3LN`M`UG_SWC39qy% zsg4>ifN#&){he64KOmV>%Hq{E)@H?)T54l+d{&z%{viL?%24{>r`#`LrPYIttB|5} zM%+X+W{K^fN!~ypX%}mK1W!mW=TFRF1OYa|v}t%dmgU8yw818_kcnz_R_&nZbtrf# z>SDD=w+7WB_I>uKKW>c630E6pTU6HPK)`i9$l99~9sy zzMcG>u=ivtk8+l4owkvp&f#tV6GMm2t!j6i2o@yLcW~cEk*!gtvPff=-CvwK-VEpM z>Mw3{vji3Oa+yhf{_h>k_Vc$yIFw`#vsOUs1zVUrUK$pFjC2;Hc{CdC)N$k7-1yVY zbdi$vDK@f_2buz7YtcPSCPCDIzSC^~f!Bld#fx9)z z`%vAer}kObHIB*TA>q_}ysBIYODUN4J)6{UKr4B6(5UfVH0HR2%EU5ZATw@-HNamj zSE9Bx2^XVNRCkNSMudVse=?1O3&`+y3C23F+A<*Qe($UsD1WL#N*Bqp-~Zj4-H8A9 zyq<-*Hj(U@){+)Ey~29&%`*R>T}ybVl(rpKx1vIeUY10&Zf;>g{kBD8u8@CiVFQHv-uJ0$=MyEO1mjf5FlwwNVDFB9i4=!0 zr4X3=T2Le6hLSq-ggTo|0;S}Tje2y(X*n1dcKV89f$b6iJR|}?JkOt*(%ah$rOzr6 zl7teHle;3;IO*;c8-AJ3C%4I4dnZ&5t^cCa(Go6++avyf6sm_sPe zW3gK1V2hfC|Gm(D7uD_61(sRdZ!@P#V^$(YYwu87W8bE)siLY{$&U93V9&IRfx$Xg zhl`UP#)M$iCLoFn&1WQ|OBO5BZ}aL<5=f9`yX19|{vpIpTlLk>w^Dn#KL0^hD9&4M z3Zf%|dJ7z!^@aNju=K-3I8$=5QPg^;ToS1h7zk8@W^=v=-2^=D$zrU3Vt<>W!%In0VNeR-C`}L-(P||pypF6J30XK?ZGA?ac1?vFz}#M4UPJF)Hnq= zE+E>JDJ3-p1gy6oR2VG+75gaxl>!y^s)pAu2X3>t?##mL3_Krag-i>>c>jezwf3BliZr8WogbOPDc> zl01oe>JgU&t-W9w6cUC=mH}mJ1$M)&y19(fQoM4^e7rC_Cdxz^%P30D_n%f+A+@X4 zzR&X2G|XGB@9G|(sk%|gvnSlPc5Uoq4)s>mbOBBf*SLIcEfjPdERha~$Mx1ww3HN^ zvc}54CXB_Qi21z_O^l~Q@7f$AnH{*)ojDLL;scVN4eVgD9{&Xm<}2(qSdAH+F!5V# zP(f`xl9}>QCmJ`)O1j%X)s)^vvqbK|-cXY9Y3q`k%*lX*>!Yzn+H#Dea;b+g{o*1Q z(RPZbU|q9&0uiJdE9EkP_P?TLH1CvUJJi*v>~nKBzOdoKKR}Ea9lb=)r^Df3gLZ7W zY7E~5imbR#f zV`1qt9W+?eb(gAC9k7faM&Fo(mV!i3eK385B~}|XacvyxpW?R3F^62@sp?C`s%|sB zd#`Ea*A zJRj*7GbnlH>l^va{^F6TTDX8O%|Mz7xLJBeD@q<)rhu=i&>6ANZJ&aHfI$LLkZR41 z2>ge;k*e0z#cU>P%=J^0T1`hgs}*WWD$LYX0NmOR39QY4}U|8>KNI42gK z40gYTB2m_ItCER{A4aMy<%YC}nW<{|nV>JDL2Ip8A!iPA`sq0OXas5Wbh*|q4J`zs zU$_!7t=fspFek=e^lrgLTNmn(BjBWHepqk$pJ*En3l-6d30yu zlCk_1mq4*;*Zn$1e2ctodH<_EQ zBt1%gmNDg56ekpZbOJ&Si+~1Q`dUqCvB=&bs`#?5U~XZ;z6#nvXc+Kf0HTGj;t5ne zlz+dMr9+@d4hzo8zUVyfUWz%V<KB@>sne+)*1v zsdWmeU$i~(hYg7NxF9Vmtt_Ih(OvkvdOMUxlP*f41tNLwbXyQhkpL5oNkpG3g5Hcx zTOBQoc!(;c+@c$fkjh!*1d@89TNp&^Nou$ixx?O>nso~n@XW?6IM>It=xn7%W1Nhi za%-io_p@lb2XT$XY*u^HD}7*xO|aRFOK1l|!i(!RQq*F;;u_7XP$d_N%jS90ZBy0F zG;n~CQ&SVCjvQz7=e9FOw5#iD6lO{{^Hqh#X02UwVuiR2nlsnv;wB-MJ6r@>#uM~a z$j5oSYV?DtsUldWem)5>ia#i{*aNIOWa(40`3n{4%2W7WcnIN`I{$qVx+6;YQuY2210Z*l<2E}#hCqr z396~TluunlBN%{3D7>JtLljiDEn=aiiJayt$K3zdG2|s+C{?0*Ss??SL@?NgbGhF1hfqm@jFxJRgoBLSafO1Lkr%TMzD6Y% zsfZJbvC!4*W}2oMpmCtjZZ6rx2Lf2(Qv?K9z%gLiim@o^YWdUFMTScY;Dsm@{<&)L zLb7zr&#M<>YRH;0b5$ncW+meg8AC>dZ_ZZuMwCwfV~iqBjE^#S@B$MP9~W!rge3&x z8&S6J{;6|wN0b~DY@7a2vGS=|yQ+7)tVIhBP@OMK-d^=gYG{H0l3y)i6K!m-VoCrs zMcHmi22b|LpqroJ#Y&TPPKS~NMJASE()P`+l5wi-VM^wesj?xZyh0p#lN2$(ItHQv zgmLzbdbDQvYGYX}_6R8w*sB~95^w<`A|lcvZof>)G*))|yT3zFd9z31i=qGV{j1=h zTwXOa(MF-Cpn#fAPXVmPO3|QRfr2-c&1S}Zd|FyMrPmrVgA->$1>yun0+38)P|z?m z+q|2Rx;3+m3Z9al2QZxyA;HBSWJ5L7R|7rVj8l|X=inWjfg!@*9$Ar9f@ zOEyYfb`Jvo2HyL?RCN%Nr4bKfi?Q4Yq|yFa?US5qvYd_JzdtC2oVi3Vbo9K_a_X*& zrWg66)2Ez(r=?WnfjQ z*Hm@tqoEQL6JRq}wTl%j)L>At;WJqhBS*zpLc`hv29NmaGbK{TFfsT9SvYS0whIVx zXQJY?3%3?3(qjL~6f_NaWeAY5M$aVbvsh?GsMKc?$2jz0to}WLfdhC>dNyGZXHUNE zA~OD1_@_K}xN79}%~AM%bK$kIOsUuZ{rrY&*D9OO`I1nL3IYxi8$P<_dNYFja_-Kq zYva-y{lK)S<@yEP(Q%_&n7rh9$eFzLhWC%{*WMfUl;oC-Eu*}>IxQzxcJ5H=yc-~T zeoDZ#mM3<`Qh99}4l`xuPvugZ)TYg79CJgt%*v8-4J&?hOw|iUrB03h*Yv-!NEE}L ze;Pz{=IK?aZOt?dWE7(vSacI|Y3DK+%9 zbZB3Y;8Vm(_P(kg5g%zrK6Ep3hi%j|{13a!-E|>gYSlU)fDEO>`!AiNCalwpfnZ8U zc`5u@hrc%L;vtm~pvdz_hw%lFq^g$p;uh}d>Luvx(`bLu>D!8LA;gJs>Dg&E$dqu; zTTMa50cR*!$9@HcL?Gg`Tdxv#o-$~vHEuc-%~`o+_s3s8b=|MWpk9JSAN>f>xhbYd z7EP{M9h2n%@HgQ6J;0%v3#Xt=9sC=O7DihqN{fI{S&3GGF|qcN=ZuPKMyFP1mBVIY zWoi(WX~J$}uaacUmEBNXA0=*@R6#*E>HO%}S|?;fiLap>lhB2Y2I(^ZQ%5gF!JMv< z!{;9WYNJTk(r)m6bMRC0k*za$Lpq3~KS~u$-oFb6+7Yq3&CLaUD7RwZ^;M%;i;)#t zA=su!Se8Ko$Cmao#ibAlCXD|S78Ww68>*a7!F?YN_DzuX*yOWS##6%3dWJcQM9`E> zrT7^v4M(3pLal;}*V$EGohjc8L=2~3dy}xvg{$}C`h`J^g@!?4>yWmjMg3P6eOY~o zatP8X6Mz>rX~teIUr{|jW2SA7#g{5uQ19G7@GWzU(T*sDxbH6Md10wq^yh8|+rVMl zUf;`Tet$+e%qC$t|JXU6L;{n}&l{Q8glR#~dVDDyc&Uu-kt5e~I=s{gxWuRuQUTIw zi)nt>^Qp7TpMO|V)>Sk<=3ehIRdLhQ}VhD!(@{IF6S9bKsl>Zz8(ltil-HxtSa zgl$DUlGT_2#^^JZRTpMW8Y2_4_5I*r`k1B0t>~syn2s~CE|PX>uSR>`JfLRvHxIZk zkaWFi^FPXJuIu_xk?jptV*Sb>wFB59bAx`Gf@Z)8z5N1 z=hL%bR(25YjR&k9`tHS7lUc}ATM7CP=GLpWfam2Cx_Sv)^npi{`6Z8~!_0~s*Oo9b zasW-9O0Jir7fhbmHm|h}pDXL^s{ZW-!BL^o>#HVRx7GR7?$cP~;tAQvZ>{4lzl6ET zVOP`aVg%HUwHs)AfuHYt`H3nHy?*)R82J8pI=<*Rlkq-NHCu2^SAB#RpZVciKQtO^ zVJcBLM-h|DWic{B%#FhtzW2dad0wfZ9xhSWv`1vapQ)hs9FJUyY28cN{^P$az?L%H zHC1B~U~VHHef$q9Mb&im+^aw={HMRps$W;7iuI_!C|BonHTRlq)x`nXK-Gg>@L-YL zc}PN^~^gmd*HUiZ9M{l%4;re6Q1)9j_;$rISkK!CDCoj#M)LVPbedW?`B;jQvRiF8UsL~FfG$2h(L$?{ zFkDR8(#G8%m`t%|JsOM{2NC3THCNwQS;S?%>Jz&28N*uTjZ;gTY%YB|p}A^166a!z zk>|i;^@%t$vuna>xoE6pP%F0y9E4x;aN<&F`f~ zxomUPfa@< zBdihbMjB~T32bJKvRHgpi}$%xF=IDrFA??Ad~CSM^Cquy_E-mWr)qV!8W~A@O{;Q> ziNwzj{EKsYuH=Tx%wU-iYQftqZYS zvfBEA?u?v*tR&LA$C1V5E4oFdnsOBepN4J#e51Z(QV~0+tk$|gOjZKp;nKf<`UooG za&qKRq0ZJ0ZBqKpik!Xu)Z)|!*%B-MmDV@z|2+nnp*GaC6c`iK4=0nWzGoWgYHJdq z(J131QlhmZJo|1)N4eeqF5mS@Z3meCSVzrOJb;8+Sxtk6hOOWd44I~*?WEu?XHajD zE{=r3VyP(p0WL@q>9|+2N2Wk<`T1K`G^%90b~g6dRv z@61V3y--@Muo$`V_sit+sJr{HWcX13U9?Yg9zf;v*A&iBe6jL0swytOKvq9H#|{xO zm5t`|S*$90ST)KoN3eSS3AZSF(FOV=ok*`#5cK_Dm!DJl$titm7|;C>SbjHivN(2 zsZ~Tdc!elzERBMVh;1W}TkPZ+i*oKfcI|^?@`wlPX$t_xphHn&C_(3Ga4!KzI?fCey4$%trNN?y7pzU(v5a8 zMGbeIAIdfwk|o<_O*(e!*#@Pes_n}^SZ>>zw#Bh93@rA-ujZDN51HsRHkf}dmp8MQ z|6=lER)=FGVHvX!CF~$TzXeAz?unPD>V6P8-sDEQt~uxC*;kIL|Hlzl$p&AI@Yn%g zc!oP(B)(Q&-_FgRQ30oD0s)1!Us| z3N{@HmIR3cIB$KXRE#0x4I1^T5s|Zgyg?Ye->ED*oA;+Z*;O&k6jeb0D%>%s?qQo@ z138Si8L=|u??sk2w&fs^bp-Kr4)c#Vu6jz41_PE*LkMjcwgXDnmlJhQ5ZQQ&NBuI{WE=uyB5QkT;_yBw1h z<;zm2qphVfFVzMzbOS4gbGI?@MQfs-KpqSJQ3?{z?p9LdFZ`{p zM#Z{T5}|Cd=%xh#?#qLUbXhkx{xq4{f;bHVc*tV0GSJ@4e~v=GQtRd{2E^Y(QAbV# zP)F}2h9ATK-2TO8!iFrLQm=_OS<)E?kYmol(z`-w@)#Eez)4p4Gd8MUN zG~3XK8F)!{&=j_oeNQpF2Y; z&+RDCmKCY?%35_~uo|b$N^z^DE68NBp;NvuHRgYIClM1403YWmu!_XDkoc9Br4Pd? z_X;cc?oA|@l-ULQq{RS&8jg}Lxn5u0^0BhVDl$+ZP|vZrv~4QZu&^;|P7#(AkW88D z=-?(GX%LNojQlOU?yJ!*A}BwRD9O+#&4M-MiUO{I)U0TIOr0Mw&<9Az*x{9FXfRu- zOY0#~zc#K(KOMiumHcxgwxDrf_SZW1CF%<2;^hmwQvaPafGhdHqJHCwB0J8EO2s%CVbtVPyd?XHiwDs&qjcFE(JpRl1UsS#W#%bsddnlgqFJ#AB#C~dY`cGw2= z#_EI|h02s~Wz!`(wIW+5Jo(VO{j%E&nvH9}f#xL^zFK9DmsWrgAxz7|hl;rw*%~?PDf$i=_n%?u8>nyIi znLp-JV^PBT(X$g=9sXdcsU{icVb+imOF_UB_;Errv|OSl@MNelV$) zacO8#BSg1P@gT_*qkB7;LjqncqM$yPNKw4Ta(y{}7kIpaId)|PjUa!KpQvu6$b}=# zi2KtKqm$6nj-RQ(xU7i*GV*IU-xUs*my5;&Z&{J@Ap3ww3acsL_)4t5rviS|m3leD zO0e)XFu<=<>fESiOnVA42aO>g(RWz0QrV;bHsD!qn zXQUC^x1afP>oEfK8%bN2l@n3BONACzD2q+JZI7t(Ljrm>fFW`jp-i*i)FoeqNIbF$smU>!0x5B z^;P;PFrZ5rc-odMi6{BM(C*e$OvCZTz}D1Q&K9+#I$zIzn*4XF$oPzSBPF^f-qjwB z+`EqXoZD0W+cqE33+lhchP|8l%Tk3aFwcG81j5c5x% z2Xvz^mHT;SNR!N-N4e7Fzh-|fxl9!@I%LXi_BrIN$fV4t39xDrku+{YhTjwYmok{m zf}kdr<$i=hHyG-ywW60|U(VF}`yJ8hOj5G#psgs)tx#xSxiTFAlV0n3ST@|j&m}^e zSC*CKNLm0H!`EZX@06dJIGng4atEr&^`<*2JW#~nUt>9$5%L9!eGQ`fnL*hia^tYa zt>55{R29guRq1X)O+0M5SZXmmg-0k%${sCyR!o&DZ}0bhkOJzMSJui9r29-M3x~M> z4ZlMy68cK5zX)_|YtXHpKb^+Q)v^7vyC>%-Ok{S*QlZa<`0Pq4aeEvepTFR)R(M{# zzDZ{7kVJ8kT>M+iq1pRFd8pd}S0<=!ng?V}rsw1!i;Vl(3$g%%p=3B3^uvIG>3{ow z2jmaV)!uGZZF6VSEpnB{=;U%7xr%Ci+QIB5w=>h0zS2M+}O^{Q*xq!qSM#>tYiGR zEWCK0^89b2r!}P9`l8gKc=+7~*Pae?GMKECPbex4lv$AB^`JD9`yPI9>uS*u`1KIv zatI4*41N;}Fm{Q`N-y{?3y3>A`Hl(mLKw*sY;B!>v~Fiu&3|AVB=zZw){Aof=iLSO z9#=>MXO|YmSR(oVxL(b%qg6&R)L)cscDKbL-jO75j6vb|>~&o;xhn3Fdl!RGBkpI8 z-Yj9)o;VleU7r(8jkBp*&h@75O9$mkA+okv&npEM<|otel8o##$ISf2o~`UL)5ZUm zv_WAkP~QFd%zcYb8Lpuh15bBYzIc>AspZfS3U;f3Vhll&cBb`Z7}_zGWiJUA2MfNN z0*cdaib5FOjeLPqxT@{v;i?$KPo-dx)Yb;q;<0MJRMN9)$dG)70@ec!Dv++GYTYf4 zVCz7B>duCX{~i?32=V)wfbsq31?b!3NL_JbP&$17#NWm#TJLdb_`qD7`}kQDvL0xmkB|a|OyW&^7 zd8hlqLFesfL9v|?Fh~~2ueh@LdMx^`E~ya>Y8L3$cXiM6@$RlPtR#H+TdUP0u=tsB zFiPZ3DZjP5eE&UgSq9aDExY%{;{!V1KkO%c?NSFz=Le{v59Hq7{K!wm!Xp~%IQj%R zKtV6b3#FsvT95cBx5KOH7PB6)f52J1F!H>0(;E>vIM#W?)$x%@#gN;{r6swwQEpwG9InA=>2D^LAtSbYg3e zpi(K(%uQa6K7st6tX2CX^nd&NQWxm|M|0;L4cFVgdn82Eh#EDBXwivo5`;tsnE!MVX*51#) zpZj{Q>vN~-@XX8Gd2U^pWpbJf`0gkw*&T#6+3S`<7)l8~<}dxHx;nlxLhy^1o&2>s zGuhK(rA7HF@)El_U5AhF{Km4?Io6zg{C#lFJUFnO9^QB>%3^Od>z@dp5f63ixK=g5 zVL;V>eteP;p$L9n{qGbXRyZmxGsM$`5)}>>0PSmA6~2G;uZ-TwxSvYk=31TJdX{rj zcBj3_=O84StGVX9O&|a2%TwGhUaUwKh*DNcxOl6Q?6}~mU;@Z}=)B)bsL>*j8}Y~X zVWLi1Fd$S8fiOgaDq|ONh%)J2lEqA~<1Q_(c-paxr%Ss_uRKvH`<7}H=(#e%h*tL)xV)S??0PG-w^EH zB@GH9!t342c53q42o=Ti<0}JNsE>up1ypewU{ug@-|bR`0(_fTKpuDDB%6zGkgWw( zYaCnZ@x*!dymuXB5|SnrVxZx)2ju6N$!qG`o=i#!LYFh8p8CCK(;dv09dq zEs>IkcY2kBDtu;NNFM~urSIlHE1W7x4;FIwXS5dFo0k%lP&o;{Qo;lneRxOm_8=PImzy7$Omiw{z{s5{$P9Q3%=7X62 zj%Dl{7)DF&w=#V<6;k$U8TZn#<}o$Mzl_*N#)2wq@~#ol(*5U!9h17w*k|cr6mJ*N zk^-PD4Qkp$z|MZ|H{O(y(5-Lryqo%)KLR^i5bX^(lYZg-TR5VoLsY|b#vbkCmXfN@ zZxaOr3<}~U@C6#3mn(YEf%jI4h$q^Aa>M55{{E3w=WkX00=6{PXlqvDrD7oJ^j3qf z+*(0FAuLn@2*U)S*zESTd5MA<*)zlFcwV{&J5wdAs%E}>Qh={?v2aJ8Ed{C{CJwOx z+!12L6*`SO7av1a0)xpn8Jw+H7G&jb_~-=MYG${G{!R-1F5YGce8TmA@+v=z;}r3$ z*`Rb82XCd#A5_*_)Y(E*`wV~|GvI|&`8*exYAN8=C-Hl4YOuLaunP-U0y;tr1FyP= z*hJI#+-qfY8yr@pJtQ(ozk5TNF1&g>~va7%Y22I z>|Y=6nTO4sS~fTYo1>G%UnGxv@;CC-L&yhW?}?fN97*DN#d_d0Wg$-HE5xu>wU`|u zY|nT;t?YV83#u~6`Cus#iF7y)uy;#?o8>=OOA-YFh^v;~1Fmyn&HESkzP~#KqA?(S z{&6=&tB`$*&ZywTk>IU+cEdfLqn+Ij8q03yJuhe{o86pt%7n{dUdGOUnuWQ^<*Koq zw}OX$-noirR{xBU_zy5jg!2Pj_#RSzt)8^PuA^SfP*w9~*zy_kriDT-ANu3bvmBPi z!4&D`uRMBoQr|-jRVxkDBJ$n?;m}xwdRzHF7^B8R;{RuGBMX7CMV8oMtjzU?3+T<@@J@f4-j8-3iCatR{iR=K`-8UfOtOAJ}Jj5LB_)oBQ^`{B_O#CXqUwtZ?(BJN5!C7y={8~2rO$HJ4ouxdOr;bvKk zva-_pseT1FJL@Ke2y$qd3#NiP!5p@=m?o&mH5bQo{oV3A={mQ{W5#D(G6%yH!aH+7 zl`p4wX{k`2isc$#M$IQKEPg-_`C2X~U?;Okdwu5lQrZL4HzpI~muo{vq(}#w078U993_XdY$M3aVA4ybN4dJ`Fvwcho&r?{H4^f?l z&CSY6M80pPSU)WY%NP+J6bCW=Ar}Q5Z=E$@c8XK z6#86-vN;5A5%aggTqR#a#yMp9yTy!5jdV}y-lAGBojuI`dbKd5Bhhckt=t68C6%f~ zIMs|Nmf0%1V5GCjuGdYAUoFppaJnX91dA=sM-fZ*5L3Hx;f+%5`UgjZj z$=E`B&uHtfLYVFBtMXb47vJg~1490IoP87>HB5ow5nDjguuavKqRKBLMkDfF9A{lIa+le(EZy; znYq?um*XT;`H={>>xj9K1eY;3rQwaTk+iIUoiC~zrb3_t|%D5_WFvn}qhE&k{)&S=p>7U4<5L(C|JPLiO2|eh+i~>)RV3`deBZ*6+R`(J`w?x}Os9{9Uj<&ya{#!#oxCX9WdId|GA)VB1%Gcd_OG2DL!iOXki z6qZ7HWrT7~^54j*z5WqAToFB0B$nWJj6ropv#5VA?}Vw!;)9ud3Tw7Jf8Lbwq%3>q zI_+`Gr!X5P{PBL%G<8_<3+uIsT+w4hQcawYtQ0vHXbYwi+BoL7RXMK$b-VtzYn-=U z`PD~k^*-n)LFwtcIZvE_9NfM_VaTzPQ(ix2u5!m9siH~)a~aoeJ8QMNp86mN%Y5FA zF^p}#0$jdKuBOtqW6Q3h9~b;)wrdLdB>L~qHoEZOH`r)mZO$`kB_^m)LFGk=3KX`! zpL1Peq~o__p@#E{$E|xzrM2J+FTG{#mY#K;o?5Qc_NtL9Ke7coSe+BPLP}ykxKT6- z-}xwfSu@KB5Openhk!%8UdcgJPVq+f7sK_15#cI`T!J>*-7%-#WscmaSU0;wUUVv0 z@!wj2VQ^l+bbsfyCaQu$aaEP?534z6$D_zgQntsOzSgdaP;qjj* z*^4!Qu1TgI?V1}h|LSk{%|7T`?;*bw9!h`&Z5Km!Eza!*E9ln}Kuvj>_zrgouzl0M zjiV1w!9s7tr;t(AI@iz20jetn)>$Ec8d@kh7bI*>${{VV4%-^OpV#+k`c>o_uwQbC z^jbW>5I%FwxGA6q8IfIbAq#7!q)lS7id7U-q2|%%c7O8b^GvNXs6)`WUp}Ykt?>k! zsJ;--ry5tW-ScPAwDjYyTXU*&;vgTVy)WjiS4F#X4$LDqd}tIr@YU_7D)uiWovxQ` zwbnRTk9@s0nLbeaQFtXH3!f*0_z_*S4piS&Q|Fl>vDIA+DpoZ92j%G_tkQw1YX~oi ztl{(3L5r*+r|BTVe1dFuyp~(jsGBBiTj*Z%SiS%9m)PpZtBr@uTOVRX6_uH{ci@^I zo_y1^##QB8`RsIhn6S3Y7bP8D9FaZ}t?N2qoy(w1F)wa}7>+0-ETUJWYDEUWe0UZ&nhay}Z~ZgCV1e+&{Svr7c&4f}-B?idU#y{)jxsZ;3<9^3zh+94EHK-LiU~*~mv{zd4 z33@KFl}i%#dR7BaV(e5+oT4x=g=Cao;}X^e80s{ zJ3Y@ZG4O(Q9AGzeqw>z&1_R1Bn2Q8$|e>I!vc5?JCO*z7SysS?r{1`o4&%F1mW+rfD z-nXJ1lW%)f);d1GKZA&1Dd^$0VIoU`!k4@J4qGr9ACRf>3@(N|rTN1aH~Vo?cfFDq z*?2#PdoCMRKj~7@s4(8&5_+1_Y(QCD<{TMC!Jk+LAz0rN5GpQ#9$k_SDFs^FV3OZ` zMK~xq)ilFYzNkk1=g|Mz)paQlM~aULaO| z)1|Mvnx}j)z1?oQUPK-OCTyLwAN$`8J=9G z3ESqD`QG90?qaYObv`#Xa&39gmOIK3A{&LN;L9^HzeYu-EGHl?FVR}vyxG4etMyHi zsn}Wc8S0z~gfo10U|w1Ww<4fU3y|y!hhb0Mlb}o%1ckt6mxG@wNp4$l!9<*!sxZ=N zk(#*2m(An+_DVGgvz`^&^PmV)zq!fDYTNB0%HNmGJZu%~F7S*8P!arV4%tQrXZoA7 zl{-A1)qI9-b5aUgxWV%vCec&-}_UzkTbv z^qoZ$^XdyPqq`~z=K}@8UwJzh&%FlA_3jQ^T0 zSIwUL_$l68kHV)0_^Rj;eI!+k*S)5|`nfBi9&QZXz1Fwf9+jPK`oBD2bFHfZtuGZ# zXI7mNX4OtO?wxK-R@Eb@3RpbBnl$Uaz`YS*=!=%MYD+x^m|ai+u~sDp12$YDiOYa7 z+VJDe73dUhwr)!{??|iL+NwvagM$FzmzKy-SFm%(182GjmwCaD(gRrv6-D_p5v-Rf z)7x^L&c`#Om+r^AYb-qogS)479G7+L&jVh08FZAQjT$Xk_=Q!b z9a#5We@|WArG|&@q_h4OD{bk@&E*Y%Ae2Y~%ETu(UxZyZ9Xlt}c-Bvye#rN|b50;u ziqV1H$W9ZBdv)00yf;_hi{aZvLC|j@txaquefc_CsZyhb;NGWu>YwWxyFK*qlzK|; zsEe50w16ErP-X51AA4+_09JO~#EA$^#04siQ0vCe_fs_J^w!u#*wv;}^F27-@luGI zhtXF0p4X)-SJm{{CYvJE5+>#u&-VU0%}d1wv^iJC^NSJJVwH^gBM@?vwd_RReX3gC z@5l)T-QMTcH$$h4dbWzYb_BTHzvE%DAGX>C@BX>G@DbRKvnhFetq1-Qb?dQprblkj zGoyZ?qO|gss_*LF4bMKN+)T`1FffK53bqHfoJOJVLl%#QqhF1BflZb@bjFrD>WK#v zk6x!<L5L*~i_k>SAK*%h3lcJ5S2%EQEZ$ zIjXaGqhzy!T!wkEC(c1H07%T_Vx|J=ygmyvb<|pJQr$x7@v3GsX898*B-Rfvx_{$k z=5P0E3l)bD6;3ag`JGU~UawI#O!sUZ-$F9o34TymGLzEzu^a{JEAfThaN`M6fAHG- zS$iGbPE8&Gfg35xhl!hEd{>Iz~{z@LD+s1t}~ZexrMPSbMjVZrB2(-#-`-Pk=AQ#!0H zhNqi2>!9o}Z8tW%oSycR`o!ZsWXmQRjs)`BZ`R`Lr)Q3{=~&<23GrIh<#(vQ!Uft0 zUBo7$(7#hn8aOyP8|X6DcSdna>pxOhjp3bL?kF6}VUChX3 zp=xG#P#196?L&{mssgP9Ih*pFu=4G)v%1t5oMXtC?N;i-jgOr6a3**uXycJC?0 z5`wk)Lzp{A-Gc%(9RVSJ0W12Ja;f{<597q(?sQZW0I#E5I+=3p$4D{x2wD({4LCmn z6qWtqk0ndD9ncSg*g3NU{hu&xzIpGLf0fL8`n3(Q>bkOD-WM8w-XjolfYcc#+y&!8ceq`3JLrt+po}Q`W^5v-<>-d#x@^PvQ-g;*U zk@Gda5t435%x{c#E=?!mC&YMQZxzKanSt;*O{4njn?|%0``>OOM^yFm6P%2PnsK* z@#y;CTXeE`^s&C$wMMVxBwzLYTiontyC3aWL$=U)Iym@R(fmXpp8d}^l*8`#<=&pZ zjdVUF@Y!Q=>{QgjR?4%(+ZKm}rTGmnhk#di)t&a1zC18KNhRA$<}#5^Y5gGM`q4xM zbS3w6+2X9#Aayc&PY(X4-?WAKm4c5cuSpFjNV6pvAB@}O*ts``G6jOq+?mSOUVy`c zkz&YNpHDjMd}s4C^`b?QOQ!tk!7t})mS#VOpR~U%Nm%57C$7v39v2iPOHHgbcz>)- z>Im?jBgjZkG-Vn)3{K8r_sJUk;l%;A__E?2W`F(Bd?18c0{=_bh1=O}`@VBXx2G=W{&A2wk} zOO4ogud47?%k}IG8H2dAT3JX_@?Z5>&J@HB>Y)#n?)#%}HY^B=-RD$s(7Cx&H)D3Q2AliA*c147S_c-PX-8w%jYqBDifkWOZCCPle_N$E| zi}&V-b`Lv^yNePsB0O_zlz|%d|JDL(j+DT6MmXind^Wu=8k8z}qqI9$5GPk8JHn~s z)1>cnOMjCxi{Al++d78FCzh{O%nHR761%8`0-n8dw;gH=2M*5Oteu5ZvAzfMjlO=#n*7WD>AR~sfb!o??k!pWj_yw&?)I~ddvXnFMjDyz z)53ODsbjZQt~lJv*5yl7*yEXMD|it0R`hPlhA`W>MwKG`!0mmp9YFCOCb~;9PYWEN zv{P5mpRQ!^JSlxXI$YxH=sCB4k=X8n6p7@Z@8(FmsldZq($8kAtEFa?t8qT$1n+eb z`O6>@{^qG%_Gy`IfqIea&Wo#usr56AH`_Mp`E~TDzKmY#Lur!IZuC+fycMhq!%+1k`DCRf(ylMz||OoJh$!{`}HMk*#gMuK~_forD5|ufc#K+UG>lQ-d*5L zsgqk5T$|@U4tZo&(9}kthCd4N^xZV{GVpg{J-FL2^ZZ%fkLMhknG25dfhC>*A)ejo zyWMlDzF%}0Db4O>6)2uBSyd~FnQZ@y{ssY%@He$e4|eNl;S+^`gdff55*Wn3%c{Ac zFFax_{oTB7>&}edr1f|B<_Gl8FwceNI+*|PxUYu+gBWnI_{KVvocCKNGwYryRJ~M+ zSC4X43JUeoF*aj4sAjFmuhbuH45_FN=n9XiX9`!Mp5%dx?!s?!4WGSIy()V@Hs%ch zFwAD{`C3AkV@B&W*#oZa3R)k#Ow7}N{D20C==MN&t) zTu6#Y5QPg~uw=YY7q6ea1nfyIaBCJ$CjV6gKZcT1%)qz9hgbnRNWr_#aU z3IJhOn4K;vf!E=dyEXT|>Hq2-idcSdujGTd`Pp7f(W>=@eyjp1lpF8q-|19k?MZdJ z9^}J>lLTNO-;F{5_+X0lXQhs?Y#&ALj!m-B3;1ol-40p~9 z40pD9b?kH%4`3T5033M$gu}_0RiD2@Ocp>jwTxUCf1Y?j*MIb#Pd%l)>C5jxC_aY+ zx3+MBQFVT^V(XYAzmWjkC!6t!mA^!O?|+E++Ia9vPjg=LYn-A-W5FCY0^JQnw zN(?xFd`HqI6FH7x&JnG&H&0ZxQ!VRE_*lq3uF)`4f1xh3$pgzMaZ{%M^5peXUpJW> z-Xy>kxR-`GT9dD`mGKn>z^1g?*oP>3VIKc%Bi3>vvU3M10yFwK6MYp=_E)wx`ccy) z3aHX+C$@>dYF;1J9~R5_dN8`^xruBVD1wdaCaxu$(d+!!mjceZ-5h*fH79-_kcaj* z3O5brMO2r42xX^#_-!{6w)^mIj7Z(%na26FS8Wi$-_)2{;Pipky@q7~#`g%Ec{rxw$;XDYfkh!UdQ}E65e$&H4trX^VbuVkNVlR(P>+L zJ`tZJ}=WR~A20b^om3#dNP;x3dT!sdilWb-5u}H@9PvrFfW3{%vMs ztgZBi#}yu!^7D!fcfL!{O={N)XTC=V#>%oT%Llx11EM;~?m*`mXI(K$H+OvvBCG>X z_&`E?(3j`#pquGGugqIT#;K4M$Chw6DVK(D#EV_P38W94{H~J9)-5Mge4lh4@Y=N~W(TV|`*H7+|^FwDk%j$DiE8V!mfS}aLRd!vX1MkXTiN-y8o8vTWZMg{)fBztnI=tLOWuWg1FLCadE(O=aelJ(54ns? z!mGazVV_H9nphUWWm)li?88!WD)_TksPeHw&zQXWw!GtOWpGI>#h zfMXIz;=xkfoILPkP=%6BIvjFkGT7X$GKGjQEnK9-(!{ws(ubdKmgnvYp)R z7g5s=6D1c>{mU<@Ip5&T9N!ogzC|7wy9uo>UfRv>6oa(f$MZ9f_W**&f10o!#ycxo zDVMp6%U8kBpAe{F;<;UYP{){G>fc7||4RL)e52(pdSu+sy4oPCz_aCv zxF&mWJ0bsgL*8rCwG>-&>L0MxeXZZhZT?@$Us-oe=vH?Ut%y@lQMqv|8z<{K`u#XY zVSr>FgmvE}zsut=qZwQbOx^3iFPDXw$K zZdBtq?s4R5Md}zqN&3bj@6zvUCltnxI;GCvSHOJ51NUFMA645_Nvxbh<1L-%~Ss;Vw0MI(W(h?I#Jtq!M&;HH;P6u&e4 z)`P^qlKOt<=4<~zfvzqI!=4j7^6n9XkzAy;x|uZ<%y_e!#L(?e?k^615?{SC6HRP#a`LyT{@X#eukjYf z*R}NN#p;#td@D<>w$p%@Z*JV~2)0zeaSjZa(T4*n3skO>arccwopZ9TzDYL#ng1*yIngU$w(#bh9+=tU zJP(dv=U%b~_Mt>D^6YGQd}9yg;pi#59|<|q*xBo*-{ug<=&OQv^YZs|YE&Ytp0{mO z-XXOjq0^wE)!UP~C&J*<2!Ppfd5KLodCMmPfy}|TZc6e%I8MwlA3;plEzLlA9sqynSn*#Y?MH+ z^Tdj)hLyxY;&KcWUW4hAz^A}SkR=*y`!Z+6L^5}nMK`b`P;dn^WolV9)+4^;(b6#A z7Q7kQjt7W)i2yqI><@_#AH8+@AI|%6N}Ta8|Bd3m?YSx%S6Tqrbn&*p3@$p3@;bpc!ZKV_pcF+ z%ElK?3j)EgI%w)mv7e+5+Vtr5*q^*vV|ub1xb;D{EP{opZ3egg`C*9ahl24)nPiwH z5TP}z2%aZ}SWt<)(S*6`BNi%5B4}>%)K}OH$LtOLHQ<<)nBoR);G)t2+5lPGoFJv5 z^dYV}w{dNgvvk(B0orvI!$|G4Di7dm>)8X4uGTRLRgcRNQf2`tdtN5MYY0K0i>IrM2>m9#K*{mlDfd$0-x)>nS>70KP~ zo#$ZIw}7>S3ddx@(P^45Wd>Zv=_zCCqEVIm4hTU1c_zsz-qrHLg^ZM+*&>qsFN>Hc zr~SId|7XJu|4}La+c*z_mWYTzz$Rae1Ct`j{@%_jL^r{^3gQul01$$R=*c6ndYS5r GcmD;m&PmV! literal 0 HcmV?d00001 diff --git a/src/components/Avatar/component.stories.tsx b/src/components/Avatar/component.stories.tsx new file mode 100644 index 0000000..94bd44a --- /dev/null +++ b/src/components/Avatar/component.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +/** Falls back to initials when no image is given, next to an avatar rendering a provided image. */ +export const Default: Story = { + render: (args) => ( +
+ + +
+ ), +}; + +/** All size variants rendered side by side. */ +export const Sizes: Story = { + render: (args) => ( +
+ {AVATAR_SIZE_VALUES.map((size) => ( + + ))} +
+ ), +}; + +/** The default circular shape next to the rounded-square shape used for moderators. */ +export const Types: Story = { + render: (args) => ( +
+ + +
+ ), +}; + +/** Different names deriving different colors from the deterministic fallback, next to an explicit custom color override. */ +export const Colors: Story = { + render: (args) => ( +
+ + + + + + + + + + +
+ ), +}; + +/** Not talking next to the pulsing ring shown while the user is speaking. */ +export const Talking: Story = { + render: (args) => ( +
+ + +
+ ), +}; diff --git a/src/components/Avatar/component.tsx b/src/components/Avatar/component.tsx new file mode 100644 index 0000000..fe2e161 --- /dev/null +++ b/src/components/Avatar/component.tsx @@ -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 ? ( + setHasImageError(true)} + /> + ) : ( + + {getInitials(name)} + + ); + + if (disableTooltip) { + return avatarElement; + } + + return ( + + {avatarElement} + + ); +} + +export default Avatar; diff --git a/src/components/Avatar/constants.ts b/src/components/Avatar/constants.ts new file mode 100644 index 0000000..7e9e260 --- /dev/null +++ b/src/components/Avatar/constants.ts @@ -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', +]; diff --git a/src/components/Avatar/index.ts b/src/components/Avatar/index.ts new file mode 100644 index 0000000..3d4170d --- /dev/null +++ b/src/components/Avatar/index.ts @@ -0,0 +1 @@ +export { default as BBBAvatar } from './component'; diff --git a/src/components/Avatar/styles.ts b/src/components/Avatar/styles.ts new file mode 100644 index 0000000..71ab5f7 --- /dev/null +++ b/src/components/Avatar/styles.ts @@ -0,0 +1,48 @@ +import styled, { css, keyframes } from 'styled-components'; +import { colorWhite } from '../../stylesheets/palette'; +import { AVATAR_SIZES } from './constants'; +import { StyledAvatarProps } from './types'; + +const DIMENSIONS = { + [AVATAR_SIZES.SMALL]: { dimension: '1.625rem', fontSize: '0.625rem', fontWeight: '600' }, + [AVATAR_SIZES.MEDIUM]: { dimension: '3rem', fontSize: '1.125rem', fontWeight: '500' }, + [AVATAR_SIZES.LARGE]: { dimension: '6rem', fontSize: '2.75rem', fontWeight: '400' }, +}; + +// Mirrors BBB's talking indicator (a ring, in the user's own color, that spreads and fades out). +const talkingPulse = (color: string) => keyframes` + 0% { box-shadow: 0 0 0 0 ${color}; } + 100% { box-shadow: 0 0 0 4px transparent; } +`; + +const talkingStyles = css` + ${({ $isTalking, $color }) => $isTalking && css` + animation: ${talkingPulse($color)} 1s infinite ease-in; + `} +`; + +export const AvatarInitials = styled.div` + width: ${({ $size }) => DIMENSIONS[$size].dimension}; + height: ${({ $size }) => DIMENSIONS[$size].dimension}; + border-radius: ${({ $isModerator }) => ($isModerator ? '20%' : '50%')}; + flex-shrink: 0; + background: ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-size: ${({ $size }) => DIMENSIONS[$size].fontSize}; + font-weight: ${({ $size }) => DIMENSIONS[$size].fontWeight}; + color: ${colorWhite}; + text-transform: uppercase; + ${talkingStyles} +`; + +export const AvatarImage = styled.img` + width: ${({ $size }) => DIMENSIONS[$size].dimension}; + height: ${({ $size }) => DIMENSIONS[$size].dimension}; + border-radius: ${({ $isModerator }) => ($isModerator ? '20%' : '50%')}; + flex-shrink: 0; + object-fit: cover; + border: 2px solid ${({ $color }) => $color}; + ${talkingStyles} +`; diff --git a/src/components/Avatar/types.ts b/src/components/Avatar/types.ts new file mode 100644 index 0000000..978b74f --- /dev/null +++ b/src/components/Avatar/types.ts @@ -0,0 +1,36 @@ +import { AVATAR_SIZE_VALUES } from './constants'; + +export type AvatarSize = typeof AVATAR_SIZE_VALUES[number]; + +export interface StyledAvatarProps { + $size: AvatarSize; + $color: string; + $isModerator: boolean; + $isTalking: boolean; +} + +export interface AvatarProps { + /** Full name of the user; used to render initials, to derive a deterministic fallback color, and as the tooltip content. */ + name: string; + + /** URL of the user's avatar image. Falls back to initials when omitted or if the image fails to load. */ + avatarUrl?: string; + + /** Background color behind the initials (and border color on the image). @default a color deterministically derived from `name`, or BBB's "you" color when `isYou` is set */ + color?: string; + + /** Size variant of the avatar. @default 'medium' */ + size?: AvatarSize; + + /** Renders a rounded-square shape instead of a circle, matching BBB's moderator avatar treatment. @default false */ + isModerator?: boolean; + + /** 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. @default false */ + isYou?: boolean; + + /** Shows a pulsing ring around the avatar, in its own color, matching BBB's talking indicator. @default false */ + isTalking?: boolean; + + /** Disables the tooltip that shows the full `name` on hover. @default false */ + disableTooltip?: boolean; +} diff --git a/src/components/index.ts b/src/components/index.ts index 230d706..ab4ba37 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,5 @@ export { BBBAccordion } from './Accordion'; +export { BBBAvatar } from './Avatar'; export { BBButton } from './Button'; export { BBBCheckbox } from './Checkbox'; export { BBBDivider } from './Divider'; diff --git a/src/stylesheets/colors.ts b/src/stylesheets/colors.ts index 84251c7..fc5341a 100644 --- a/src/stylesheets/colors.ts +++ b/src/stylesheets/colors.ts @@ -9,6 +9,7 @@ import { colorHoverDark, colorHoverLight, colorHoverNeutral, colorOverlay, colorShadowDefault, + colorUserYou, } from './palette'; export const colors = { @@ -66,6 +67,9 @@ export const colors = { shadow: { default: colorShadowDefault, }, + user: { + you: colorUserYou, + }, } as const; export type Colors = typeof colors; diff --git a/src/stylesheets/palette.ts b/src/stylesheets/palette.ts index 1867278..6b43f8e 100644 --- a/src/stylesheets/palette.ts +++ b/src/stylesheets/palette.ts @@ -66,3 +66,6 @@ export const colorOverlay = 'var(--color-overlay, rgba(0, 0, 0, 0.75))'; //Shadow colors export const colorShadowDefault = 'var(--color-shadow-default, rgb(0 35 11 / 20%))'; + +// User colors +export const colorUserYou = 'var(--color-user-you, #19237C)'; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 461ae7f..90d5502 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -3,6 +3,7 @@ import path from 'path'; export default { entry: { Accordion: './src/components/Accordion/index.ts', + Avatar: './src/components/Avatar/index.ts', Button: './src/components/Button/index.ts', Checkbox: './src/components/Checkbox/index.ts', Divider: './src/components/Divider/index.ts', From f85e02cda95ee5a02726a1fc1164868c2a05e297 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Mon, 21 Sep 2026 08:22:41 -0300 Subject: [PATCH 7/7] feat(BBBSlider): add slider component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library has no slider, so every range control in BigBlueButton ships its own: the camera modal drives brightness with a raw `input[type=range]` plus hand-positioned markers, while the profile panel uses a bare MUI `Slider` with local `styled()` overrides. The two disagree on range, on styling and on how the value is announced. Add `BBBSlider`, a horizontal single-thumb wrapper around MUI's `Slider`, so those call sites collapse onto one control. Its appearance matches what the profile panel renders today — MUI's defaults carrying the brand colour — so adopting it changes no pixels there. Three things are handled by the wrapper rather than left to callers, because MUI gets them wrong by default in this context: - `id` and `aria-describedby` are routed into the hidden range input through `slotProps.input`. MUI puts unrecognized props on the root ``, which makes `