Merge pull request #29594 from backstage/canon-avatar
Canon - Add new Avatar component
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/canon': patch
|
||||
---
|
||||
|
||||
Add new Avatar component to Canon.
|
||||
@@ -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.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<AvatarSnippet story="Default" />}
|
||||
code={`<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" />`}
|
||||
/>
|
||||
|
||||
<Tabs.Root>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab>Usage</Tabs.Tab>
|
||||
<Tabs.Tab>Theming</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel>
|
||||
<CodeBlock
|
||||
code={`import { Avatar } from '@backstage/canon';
|
||||
|
||||
<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" />`}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel>
|
||||
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`
|
||||
</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={avatarPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
Avatar sizes can be set using the `size` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<AvatarSnippet story="Sizes" />}
|
||||
code={`<Flex gap="4" direction="column">
|
||||
<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" size="small" />
|
||||
<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" size="medium" />
|
||||
<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" size="large" />
|
||||
</Flex>`}
|
||||
/>
|
||||
|
||||
### Fallback
|
||||
|
||||
If the image is not available, the avatar will show the initials of the name.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<AvatarSnippet story="Fallback" />}
|
||||
code={`<Avatar src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA" name="Charles de Dreuille" />`}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
|
||||
import type { PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const avatarPropDefs: Record<string, PropDef> = {
|
||||
src: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium', 'large'],
|
||||
default: 'medium',
|
||||
responsive: true,
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -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 ? <StoryComponent /> : null;
|
||||
};
|
||||
|
||||
export const AvatarSnippet = ({
|
||||
story,
|
||||
}: {
|
||||
story: keyof typeof AvatarStories;
|
||||
}) => {
|
||||
const stories = composeStories(AvatarStories);
|
||||
const StoryComponent = stories[story as keyof typeof stories];
|
||||
|
||||
return StoryComponent ? <StoryComponent /> : null;
|
||||
};
|
||||
|
||||
@@ -66,6 +66,11 @@ export const layoutComponents: Page[] = [
|
||||
];
|
||||
|
||||
export const components: Page[] = [
|
||||
{
|
||||
title: 'Avatar',
|
||||
slug: 'avatar',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Button',
|
||||
slug: 'button',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<HTMLSpanElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AvatarProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Avatar_2.Root> {
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
// (undocumented)
|
||||
src: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BooleanPropDef = {
|
||||
type: 'boolean';
|
||||
|
||||
@@ -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<typeof Avatar>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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 => (
|
||||
<Flex>
|
||||
<Avatar {...args} size="small" />
|
||||
<Avatar {...args} size="medium" />
|
||||
<Avatar {...args} size="large" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<typeof AvatarPrimitive.Root>,
|
||||
AvatarProps
|
||||
>(({ className, src, name, size = 'medium', ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={clsx('canon-AvatarRoot', className)}
|
||||
data-size={size}
|
||||
{...props}
|
||||
>
|
||||
<AvatarPrimitive.Image className="canon-AvatarImage" src={src} />
|
||||
<AvatarPrimitive.Fallback className="canon-AvatarFallback">
|
||||
{(name || '')
|
||||
.split(' ')
|
||||
.map(word => word[0])
|
||||
.join('')
|
||||
.toLocaleUpperCase('en-US')
|
||||
.slice(0, 2)}
|
||||
</AvatarPrimitive.Fallback>
|
||||
</AvatarPrimitive.Root>
|
||||
));
|
||||
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
@@ -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';
|
||||
@@ -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<typeof Avatar.Root> {
|
||||
src: string;
|
||||
name: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user