Merge pull request #30466 from backstage/canon-skeleton

Canon - Add new Skeleton component
This commit is contained in:
Charles de Dreuille
2025-07-08 08:39:09 +01:00
committed by GitHub
16 changed files with 361 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Add new Skeleton component in Canon
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { SkeletonSnippet } from '@/snippets/stories-snippets';
import {
skeletonPropDefs,
skeletonUsageSnippet,
skeletonDefaultSnippet,
skeletonDemo1Snippet,
skeletonDemo2Snippet,
} from './skeleton.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# Skeleton
Use to show a placeholder while content is loading.
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo2" />}
code={skeletonDefaultSnippet}
/>
<ComponentInfos
component="skeleton"
classNames={['canon-Skeleton']}
usageCode={skeletonUsageSnippet}
/>
## API reference
<PropsTable data={skeletonPropDefs} />
## Examples
### Demo 1
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo1" />}
code={skeletonDemo1Snippet}
open
/>
### Demo 2
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo2" />}
code={skeletonDemo2Snippet}
open
/>
@@ -0,0 +1,58 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const skeletonPropDefs: Record<string, PropDef> = {
width: {
type: 'number',
default: '80',
responsive: false,
},
height: {
type: 'number',
default: '24',
responsive: false,
},
rounded: {
type: 'boolean',
default: 'false',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const skeletonUsageSnippet = `import { Flex, Skeleton } from '@backstage/canon';
<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
export const skeletonDefaultSnippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
export const skeletonDemo1Snippet = `<Flex gap="4">
<Skeleton rounded width={48} height={48} />
<Flex direction="column" gap="4">
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Flex gap="4">
<Skeleton width="100%" height={8} />
<Skeleton width="100%" height={8} />
</Flex>
</Flex>
</Flex>`;
export const skeletonDemo2Snippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
@@ -23,6 +23,7 @@ import * as TabsStories from '../../../packages/canon/src/components/Tabs/Tabs.s
import * as SwitchStories from '../../../packages/canon/src/components/Switch/Switch.stories';
import * as SearchFieldStories from '../../../packages/canon/src/components/SearchField/SearchField.stories';
import * as TooltipStories from '../../../packages/canon/src/components/Tooltip/Tooltip.stories';
import * as SkeletonStories from '../../../packages/canon/src/components/Skeleton/Skeleton.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
@@ -233,3 +234,14 @@ export const TooltipSnippet = ({
return StoryComponent ? <StoryComponent /> : null;
};
export const SkeletonSnippet = ({
story,
}: {
story: keyof typeof SkeletonStories;
}) => {
const stories = composeStories(SkeletonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
+5
View File
@@ -131,6 +131,11 @@ export const components: Page[] = [
slug: 'select',
status: 'alpha',
},
{
title: 'Skeleton',
slug: 'skeleton',
status: 'alpha',
},
{
title: 'Switch',
slug: 'switch',
+17
View File
@@ -9088,6 +9088,12 @@
text-decoration: none;
}
@keyframes pulse {
50% {
opacity: .5;
}
}
:root {
--canon-font-regular: system-ui;
--canon-font-monospace: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;
@@ -9173,6 +9179,7 @@
--canon-ring: #1f5493;
--canon-scrollbar: #a0a0a03b;
--canon-scrollbar-thumb: #a0a0a0;
--canon-animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
}
[data-theme="dark"] {
@@ -10460,6 +10467,16 @@
height: 1rem;
}
.canon-Skeleton {
animation: var(--canon-animate-pulse);
background-color: var(--canon-bg-surface-2);
border-radius: var(--canon-radius-2);
}
.canon-Skeleton[data-rounded="true"] {
border-radius: var(--canon-radius-full);
}
.canon-Tooltip {
background: var(--canon-bg-surface-2);
forced-color-adjust: none;
+5
View File
@@ -448,6 +448,11 @@ export const componentDefinitions: {
readonly size: readonly ['small', 'medium'];
};
};
readonly Skeleton: {
readonly classNames: {
readonly root: 'canon-Skeleton';
};
};
readonly Switch: {
readonly classNames: {
readonly root: 'canon-Switch';
@@ -0,0 +1,82 @@
/*
* 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 { Skeleton } from './Skeleton';
import { Flex } from '../Flex';
const meta = {
title: 'Components/Skeleton',
component: Skeleton,
argTypes: {
rounded: {
control: 'boolean',
},
width: {
control: 'number',
},
height: {
control: 'number',
},
},
args: {
width: 80,
height: 24,
rounded: false,
},
} satisfies Meta<typeof Skeleton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};
export const Rounded: Story = {
args: {
rounded: true,
width: 48,
height: 48,
},
};
export const Demo1: Story = {
render: () => (
<Flex gap="4">
<Skeleton rounded width={48} height={48} />
<Flex direction="column" gap="4">
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Flex gap="4">
<Skeleton width="100%" height={8} />
<Skeleton width="100%" height={8} />
</Flex>
</Flex>
</Flex>
),
};
export const Demo2: Story = {
render: () => (
<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>
),
};
@@ -0,0 +1,25 @@
/*
* Copyright 2024 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-Skeleton {
animation: var(--canon-animate-pulse);
background-color: var(--canon-bg-surface-2);
border-radius: var(--canon-radius-2);
}
.canon-Skeleton[data-rounded='true'] {
border-radius: var(--canon-radius-full);
}
@@ -0,0 +1,36 @@
/*
* Copyright 2024 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 { useStyles } from '../../hooks/useStyles';
import { SkeletonProps } from './types';
/** @public */
export const Skeleton = (props: SkeletonProps) => {
const { width = 80, height = 24, rounded = false, ...rest } = props;
const { classNames } = useStyles('Skeleton');
return (
<div
className={classNames.root}
data-rounded={rounded}
style={{
width,
height,
}}
{...rest}
/>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2024 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 { Skeleton } from './Skeleton';
export type { SkeletonProps } from './types';
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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 { ComponentProps } from 'react';
/** @public */
export interface SkeletonProps extends ComponentProps<'div'> {
width?: number | string;
height?: number | string;
rounded?: boolean;
}
+1
View File
@@ -43,6 +43,7 @@
@import '../components/Text/styles.css';
@import '../components/TextField/TextField.styles.css';
@import '../components/SearchField/SearchField.styles.css';
@import '../components/Skeleton/Skeleton.styles.css';
@import '../components/Tooltip/Tooltip.styles.css';
@import '../components/ScrollArea/ScrollArea.styles.css';
@import '../components/Select/Select.styles.css';
+8
View File
@@ -24,6 +24,12 @@
text-decoration: none;
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
/* Light theme tokens */
:root {
/* Font families */
@@ -132,6 +138,8 @@
--canon-ring: #1f5493;
--canon-scrollbar: #a0a0a03b;
--canon-scrollbar-thumb: #a0a0a0;
--canon-animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Dark theme tokens */
@@ -178,6 +178,11 @@ export const componentDefinitions = {
size: ['small', 'medium'] as const,
},
},
Skeleton: {
classNames: {
root: 'canon-Skeleton',
},
},
Switch: {
classNames: {
root: 'canon-Switch',