From 851779d101eccdddedc5f35568283f23f0d3aa2e Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 14 Apr 2025 17:09:37 +0200 Subject: [PATCH 1/2] 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'; From 378fa2908deea71dbc81949105395f2787b10b7f Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 14 Apr 2025 17:18:59 +0200 Subject: [PATCH 2/2] Add Avatar docs Signed-off-by: Charles de Dreuille --- .../src/app/(docs)/components/avatar/page.mdx | 75 +++++++++++++++++++ .../src/app/(docs)/components/avatar/props.ts | 19 +++++ canon-docs/src/snippets/stories-snippets.tsx | 12 +++ canon-docs/src/utils/data.ts | 5 ++ 4 files changed, 111 insertions(+) create mode 100644 canon-docs/src/app/(docs)/components/avatar/page.mdx create mode 100644 canon-docs/src/app/(docs)/components/avatar/props.ts diff --git a/canon-docs/src/app/(docs)/components/avatar/page.mdx b/canon-docs/src/app/(docs)/components/avatar/page.mdx new file mode 100644 index 0000000000..b46caf8d62 --- /dev/null +++ b/canon-docs/src/app/(docs)/components/avatar/page.mdx @@ -0,0 +1,75 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { Tabs } from '@/components/Tabs'; +import { CodeBlock } from '@/components/CodeBlock'; +import { AvatarSnippet } from '@/snippets/stories-snippets'; +import { avatarPropDefs } from './props'; + +# Avatar + +An avatar component with a fallback for initials. + +} + code={``} +/> + + + + Usage + Theming + + + `} + /> + + + We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with + your brand. For additional flexibility, you can use the provided class names for each element listed below. + - `canon-AvatarRoot` + - `canon-AvatarRoot[data-size='small']` + - `canon-AvatarRoot[data-size='medium']` + - `canon-AvatarRoot[data-size='large']` + - `canon-AvatarImage` + - `canon-AvatarFallback` + + + +## API reference + + + +## Examples + +### Sizes + +Avatar sizes can be set using the `size` prop. + +} + code={` + + + + `} +/> + +### Fallback + +If the image is not available, the avatar will show the initials of the name. + +} + code={``} +/> diff --git a/canon-docs/src/app/(docs)/components/avatar/props.ts b/canon-docs/src/app/(docs)/components/avatar/props.ts new file mode 100644 index 0000000000..e86c67a41e --- /dev/null +++ b/canon-docs/src/app/(docs)/components/avatar/props.ts @@ -0,0 +1,19 @@ +import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs'; +import type { PropDef } from '@/utils/propDefs'; + +export const avatarPropDefs: Record = { + src: { + type: 'string', + }, + name: { + type: 'string', + }, + size: { + type: 'enum', + values: ['small', 'medium', 'large'], + default: 'medium', + responsive: true, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx index 6b54f5dc4d..83cc866050 100644 --- a/canon-docs/src/snippets/stories-snippets.tsx +++ b/canon-docs/src/snippets/stories-snippets.tsx @@ -15,6 +15,7 @@ import * as FlexStories from '../../../packages/canon/src/components/Flex/Flex.s import * as SelectStories from '../../../packages/canon/src/components/Select/Select.stories'; import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.stories'; import * as LinkStories from '../../../packages/canon/src/components/Link/Link.stories'; +import * as AvatarStories from '../../../packages/canon/src/components/Avatar/Avatar.stories'; export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => { const stories = composeStories(BoxStories); @@ -141,3 +142,14 @@ export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => { return StoryComponent ? : null; }; + +export const AvatarSnippet = ({ + story, +}: { + story: keyof typeof AvatarStories; +}) => { + const stories = composeStories(AvatarStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index 48274ea1a2..d1e9277e57 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -66,6 +66,11 @@ export const layoutComponents: Page[] = [ ]; export const components: Page[] = [ + { + title: 'Avatar', + slug: 'avatar', + status: 'alpha', + }, { title: 'Button', slug: 'button',