From 851779d101eccdddedc5f35568283f23f0d3aa2e Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 14 Apr 2025 17:09:37 +0200 Subject: [PATCH] Add new Avatar component Signed-off-by: Charles de Dreuille --- .changeset/moody-socks-mate.md | 5 ++ packages/canon/css/avatar.css | 49 ++++++++++++++ packages/canon/css/components.css | 50 ++++++++++++++ packages/canon/css/styles.css | 50 ++++++++++++++ packages/canon/report.api.md | 17 +++++ .../src/components/Avatar/Avatar.stories.tsx | 54 +++++++++++++++ .../src/components/Avatar/Avatar.styles.css | 65 +++++++++++++++++++ .../canon/src/components/Avatar/Avatar.tsx | 45 +++++++++++++ packages/canon/src/components/Avatar/index.ts | 18 +++++ packages/canon/src/components/Avatar/types.ts | 25 +++++++ packages/canon/src/css/components.css | 1 + packages/canon/src/index.ts | 1 + 12 files changed, 380 insertions(+) create mode 100644 .changeset/moody-socks-mate.md create mode 100644 packages/canon/css/avatar.css create mode 100644 packages/canon/src/components/Avatar/Avatar.stories.tsx create mode 100644 packages/canon/src/components/Avatar/Avatar.styles.css create mode 100644 packages/canon/src/components/Avatar/Avatar.tsx create mode 100644 packages/canon/src/components/Avatar/index.ts create mode 100644 packages/canon/src/components/Avatar/types.ts diff --git a/.changeset/moody-socks-mate.md b/.changeset/moody-socks-mate.md new file mode 100644 index 0000000000..9bee9120ad --- /dev/null +++ b/.changeset/moody-socks-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Add new Avatar component to Canon. diff --git a/packages/canon/css/avatar.css b/packages/canon/css/avatar.css new file mode 100644 index 0000000000..c1f4cca59e --- /dev/null +++ b/packages/canon/css/avatar.css @@ -0,0 +1,49 @@ +.canon-AvatarRoot { + vertical-align: middle; + user-select: none; + color: var(--canon-fg-primary); + background-color: var(--canon-bg-surface-2); + border-radius: 100%; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + font-size: 1rem; + font-weight: 500; + line-height: 1; + display: inline-flex; + overflow: hidden; +} + +.canon-AvatarRoot[data-size="small"] { + width: 1.5rem; + height: 1.5rem; +} + +.canon-AvatarRoot[data-size="medium"] { + width: 2rem; + height: 2rem; +} + +.canon-AvatarRoot[data-size="large"] { + width: 3rem; + height: 3rem; +} + +.canon-AvatarImage { + object-fit: cover; + width: 100%; + height: 100%; +} + +.canon-AvatarFallback { + width: 100%; + height: 100%; + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); + box-shadow: inset 0 0 0 1px var(--canon-border); + border-radius: var(--canon-radius-full); + justify-content: center; + align-items: center; + display: flex; +} diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index bb1ba10018..ed294b98f4 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -1,3 +1,53 @@ +.canon-AvatarRoot { + vertical-align: middle; + user-select: none; + color: var(--canon-fg-primary); + background-color: var(--canon-bg-surface-2); + border-radius: 100%; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + font-size: 1rem; + font-weight: 500; + line-height: 1; + display: inline-flex; + overflow: hidden; +} + +.canon-AvatarRoot[data-size="small"] { + width: 1.5rem; + height: 1.5rem; +} + +.canon-AvatarRoot[data-size="medium"] { + width: 2rem; + height: 2rem; +} + +.canon-AvatarRoot[data-size="large"] { + width: 3rem; + height: 3rem; +} + +.canon-AvatarImage { + object-fit: cover; + width: 100%; + height: 100%; +} + +.canon-AvatarFallback { + width: 100%; + height: 100%; + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); + box-shadow: inset 0 0 0 1px var(--canon-border); + border-radius: var(--canon-radius-full); + justify-content: center; + align-items: center; + display: flex; +} + .canon-Box { font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 473b8f5d45..942c33f138 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9222,6 +9222,56 @@ --canon-scrollbar-thumb: #575757; } +.canon-AvatarRoot { + vertical-align: middle; + user-select: none; + color: var(--canon-fg-primary); + background-color: var(--canon-bg-surface-2); + border-radius: 100%; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + font-size: 1rem; + font-weight: 500; + line-height: 1; + display: inline-flex; + overflow: hidden; +} + +.canon-AvatarRoot[data-size="small"] { + width: 1.5rem; + height: 1.5rem; +} + +.canon-AvatarRoot[data-size="medium"] { + width: 2rem; + height: 2rem; +} + +.canon-AvatarRoot[data-size="large"] { + width: 3rem; + height: 3rem; +} + +.canon-AvatarImage { + object-fit: cover; + width: 100%; + height: 100%; +} + +.canon-AvatarFallback { + width: 100%; + height: 100%; + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); + box-shadow: inset 0 0 0 1px var(--canon-border); + border-radius: var(--canon-radius-full); + justify-content: center; + align-items: center; + display: flex; +} + .canon-Box { font-family: var(--canon-font-regular); font-weight: var(--canon-font-weight-regular); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 1e65a51dca..fbc1a68542 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; import { ChangeEvent } from 'react'; import { Context } from 'react'; @@ -52,6 +53,22 @@ export type AsProps = | 'dl' | 'dt'; +// @public (undocumented) +export const Avatar: ForwardRefExoticComponent< + AvatarProps & RefAttributes +>; + +// @public (undocumented) +export interface AvatarProps + extends React.ComponentPropsWithoutRef { + // (undocumented) + name: string; + // (undocumented) + size?: 'small' | 'medium' | 'large'; + // (undocumented) + src: string; +} + // @public (undocumented) export type BooleanPropDef = { type: 'boolean'; diff --git a/packages/canon/src/components/Avatar/Avatar.stories.tsx b/packages/canon/src/components/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000000..ce9e38a5b0 --- /dev/null +++ b/packages/canon/src/components/Avatar/Avatar.stories.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { Avatar } from './index'; +import { Flex } from '@backstage/canon'; + +const meta = { + title: 'Components/Avatar', + component: Avatar, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + src: 'https://avatars.githubusercontent.com/u/1540635?v=4', + name: 'Charles de Dreuille', + }, +}; + +export const Fallback: Story = { + args: { + ...Default.args, + src: 'https://avatars.githubusercontent.com/u/15406AAAAAAAAA', + }, +}; + +export const Sizes: Story = { + args: { + ...Default.args, + }, + render: args => ( + + + + + + ), +}; diff --git a/packages/canon/src/components/Avatar/Avatar.styles.css b/packages/canon/src/components/Avatar/Avatar.styles.css new file mode 100644 index 0000000000..b2cbd140e2 --- /dev/null +++ b/packages/canon/src/components/Avatar/Avatar.styles.css @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.canon-AvatarRoot { + display: inline-flex; + justify-content: center; + align-items: center; + vertical-align: middle; + border-radius: 100%; + user-select: none; + font-weight: 500; + color: var(--canon-fg-primary); + background-color: var(--canon-bg-surface-2); + font-size: 1rem; + line-height: 1; + overflow: hidden; + height: 2rem; + width: 2rem; +} + +.canon-AvatarRoot[data-size='small'] { + height: 1.5rem; + width: 1.5rem; +} + +.canon-AvatarRoot[data-size='medium'] { + height: 2rem; + width: 2rem; +} + +.canon-AvatarRoot[data-size='large'] { + height: 3rem; + width: 3rem; +} + +.canon-AvatarImage { + object-fit: cover; + height: 100%; + width: 100%; +} + +.canon-AvatarFallback { + align-items: center; + display: flex; + justify-content: center; + height: 100%; + width: 100%; + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); + box-shadow: inset 0 0 0 1px var(--canon-border); + border-radius: var(--canon-radius-full); +} diff --git a/packages/canon/src/components/Avatar/Avatar.tsx b/packages/canon/src/components/Avatar/Avatar.tsx new file mode 100644 index 0000000000..472bec39fb --- /dev/null +++ b/packages/canon/src/components/Avatar/Avatar.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef, ElementRef } from 'react'; +import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar'; +import clsx from 'clsx'; +import { AvatarProps } from './types'; + +/** @public */ +export const Avatar = forwardRef< + ElementRef, + AvatarProps +>(({ className, src, name, size = 'medium', ...props }, ref) => ( + + + + {(name || '') + .split(' ') + .map(word => word[0]) + .join('') + .toLocaleUpperCase('en-US') + .slice(0, 2)} + + +)); + +Avatar.displayName = AvatarPrimitive.Root.displayName; diff --git a/packages/canon/src/components/Avatar/index.ts b/packages/canon/src/components/Avatar/index.ts new file mode 100644 index 0000000000..d5ab42beb2 --- /dev/null +++ b/packages/canon/src/components/Avatar/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Avatar } from './Avatar'; +export type { AvatarProps } from './types'; diff --git a/packages/canon/src/components/Avatar/types.ts b/packages/canon/src/components/Avatar/types.ts new file mode 100644 index 0000000000..b2e6cf39cf --- /dev/null +++ b/packages/canon/src/components/Avatar/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Avatar } from '@base-ui-components/react/avatar'; + +/** @public */ +export interface AvatarProps + extends React.ComponentPropsWithoutRef { + src: string; + name: string; + size?: 'small' | 'medium' | 'large'; +} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index 7dee4afb97..8abef12afe 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -14,6 +14,7 @@ * limitations under the License. */ +@import '../components/Avatar/Avatar.styles.css'; @import '../components/Box/styles.css'; @import '../components/Button/styles.css'; @import '../components/DataTable/Root/DataTableRoot.styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 9e9e401655..e5e38e696c 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -32,6 +32,7 @@ export * from './components/Text'; export * from './components/Heading'; // UI components +export * from './components/Avatar'; export * from './components/Button'; export * from './components/DataTable'; export * from './components/Icon';