Merge pull request #32984 from backstage/cdedreuille/ui-guidelines-cards-with-table
feat(ui): add Guidelines section with Cards + Table page layout story
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized.
|
||||
|
||||
Affected components: Container
|
||||
@@ -15,4 +15,19 @@ export const allModes = {
|
||||
themeMode: 'dark',
|
||||
themeName: 'spotify',
|
||||
},
|
||||
'light spotify neutral-1': {
|
||||
themeMode: 'light',
|
||||
themeName: 'spotify',
|
||||
background: 'neutral-1',
|
||||
},
|
||||
'light spotify neutral-2': {
|
||||
themeMode: 'light',
|
||||
themeName: 'spotify',
|
||||
background: 'neutral-2',
|
||||
},
|
||||
'light spotify neutral-3': {
|
||||
themeMode: 'light',
|
||||
themeName: 'spotify',
|
||||
background: 'neutral-3',
|
||||
},
|
||||
} as const;
|
||||
|
||||
+44
-6
@@ -3,7 +3,7 @@ import addonDocs from '@storybook/addon-docs';
|
||||
import addonThemes from '@storybook/addon-themes';
|
||||
import addonLinks from '@storybook/addon-links';
|
||||
import { definePreview } from '@storybook/react-vite';
|
||||
import { useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { AlertDisplay } from '@backstage/core-components';
|
||||
import { apis } from './support/apis';
|
||||
@@ -19,6 +19,7 @@ import './storybook.css';
|
||||
|
||||
// Custom themes
|
||||
import './themes/spotify.css';
|
||||
import { Box } from '../packages/ui/src/components/Box';
|
||||
|
||||
export default definePreview({
|
||||
tags: ['manifest'],
|
||||
@@ -30,8 +31,8 @@ export default definePreview({
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
items: [
|
||||
{ value: 'light', icon: 'circlehollow', title: 'Light' },
|
||||
{ value: 'dark', icon: 'circle', title: 'Dark' },
|
||||
{ value: 'light', icon: 'sun', title: 'Light' },
|
||||
{ value: 'dark', icon: 'moon', title: 'Dark' },
|
||||
],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
@@ -49,11 +50,26 @@ export default definePreview({
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
background: {
|
||||
name: 'Background',
|
||||
description: 'Global background for components',
|
||||
defaultValue: 'app',
|
||||
toolbar: {
|
||||
icon: 'contrast',
|
||||
items: [
|
||||
{ value: 'app', title: 'App Background' },
|
||||
{ value: 'neutral-1', title: 'Neutral 1 Background' },
|
||||
{ value: 'neutral-2', title: 'Neutral 2 Background' },
|
||||
{ value: 'neutral-3', title: 'Neutral 3 Background' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initialGlobals: {
|
||||
themeMode: 'light',
|
||||
themeName: 'backstage',
|
||||
background: 'app',
|
||||
},
|
||||
|
||||
parameters: {
|
||||
@@ -70,7 +86,13 @@ export default definePreview({
|
||||
|
||||
options: {
|
||||
storySort: {
|
||||
order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'],
|
||||
order: [
|
||||
'Backstage UI',
|
||||
'Guidelines',
|
||||
'Plugins',
|
||||
'Layout',
|
||||
'Navigation',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -114,12 +136,14 @@ export default definePreview({
|
||||
},
|
||||
|
||||
decorators: [
|
||||
Story => {
|
||||
(Story, context) => {
|
||||
const [globals] = useGlobals();
|
||||
const selectedTheme =
|
||||
globals.themeMode === 'light' ? themes.light : themes.dark;
|
||||
const selectedThemeMode = globals.themeMode || 'light';
|
||||
const selectedThemeName = globals.themeName || 'backstage';
|
||||
const selectedBackground = globals.background || 'app';
|
||||
const isFullscreen = context.parameters.layout === 'fullscreen';
|
||||
|
||||
useEffect(() => {
|
||||
document.body.removeAttribute('data-theme-mode');
|
||||
@@ -133,6 +157,8 @@ export default definePreview({
|
||||
}, [selectedTheme, selectedThemeName]);
|
||||
|
||||
document.body.style.backgroundColor = 'var(--bui-bg-app)';
|
||||
document.body.style.padding =
|
||||
isFullscreen && selectedBackground !== 'app' ? '1rem' : '';
|
||||
const docsStoryElements = document.getElementsByClassName('docs-story');
|
||||
Array.from(docsStoryElements).forEach(element => {
|
||||
(element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)';
|
||||
@@ -143,7 +169,19 @@ export default definePreview({
|
||||
{/* @ts-ignore */}
|
||||
<TestApiProvider apis={apis}>
|
||||
<AlertDisplay />
|
||||
<Story />
|
||||
{Array.from({
|
||||
length:
|
||||
selectedBackground === 'app'
|
||||
? 0
|
||||
: parseInt(selectedBackground.split('-')[1], 10),
|
||||
}).reduce<React.ReactNode>(
|
||||
children => (
|
||||
<Box bg="neutral-auto" p="4">
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
<Story />,
|
||||
)}
|
||||
</TestApiProvider>
|
||||
</UnifiedThemeProvider>
|
||||
);
|
||||
|
||||
@@ -213,6 +213,10 @@
|
||||
.bui-Tag {
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
.bui-Container {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode='light'][data-theme-name='spotify'] {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { allModes } from '../../../../../.storybook/modes';
|
||||
import { Button } from './Button';
|
||||
import { Flex } from '../Flex';
|
||||
import { Box } from '../Box';
|
||||
@@ -55,100 +56,37 @@ export const Variants = meta.story({
|
||||
control: false,
|
||||
},
|
||||
},
|
||||
chromatic: {
|
||||
modes: {
|
||||
'light spotify neutral-1': allModes['light spotify neutral-1'],
|
||||
'light spotify neutral-2': allModes['light spotify neutral-2'],
|
||||
'light spotify neutral-3': allModes['light spotify neutral-3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Default</Text>
|
||||
<Flex align="center" p="4">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Neutral 1</Text>
|
||||
<Flex align="center" bg="neutral-1" p="4">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Neutral 2</Text>
|
||||
<Flex align="center" bg="neutral-2" p="4">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Neutral 3</Text>
|
||||
<Flex align="center" bg="neutral-3" p="4">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
@@ -208,20 +146,6 @@ export const Destructive = meta.story({
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>On Neutral 1</Text>
|
||||
<Flex align="center" bg="neutral-1" p="4" gap="4">
|
||||
<Button variant="primary" destructive>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" destructive>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive>
|
||||
Tertiary
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Sizes</Text>
|
||||
<Flex align="center" p="4" gap="4">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
max-width: 120rem;
|
||||
padding-inline: var(--bui-space-4);
|
||||
margin-inline: auto;
|
||||
transition: padding 0.2s ease-in-out;
|
||||
padding-bottom: var(--bui-space-8);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Container,
|
||||
Grid,
|
||||
Flex,
|
||||
Text,
|
||||
Table,
|
||||
CellText,
|
||||
CellProfile,
|
||||
useTable,
|
||||
type ColumnConfig,
|
||||
PluginHeader,
|
||||
HeaderPage,
|
||||
Button,
|
||||
} from '..';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metric card data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MetricCard {
|
||||
label: string;
|
||||
value: string;
|
||||
trend: string;
|
||||
trendColor: 'primary' | 'secondary' | 'success' | 'danger' | 'warning';
|
||||
}
|
||||
|
||||
const metrics: MetricCard[] = [
|
||||
{
|
||||
label: 'Total Components',
|
||||
value: '142',
|
||||
trend: '+12 this week',
|
||||
trendColor: 'success',
|
||||
},
|
||||
{
|
||||
label: 'Active Services',
|
||||
value: '58',
|
||||
trend: '94% healthy',
|
||||
trendColor: 'success',
|
||||
},
|
||||
{
|
||||
label: 'Recent Deployments',
|
||||
value: '23',
|
||||
trend: 'in the last 24 h',
|
||||
trendColor: 'secondary',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ServiceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
ownerAvatar: string;
|
||||
type: 'service' | 'library' | 'website' | 'documentation';
|
||||
lifecycle: 'production' | 'experimental';
|
||||
description: string;
|
||||
}
|
||||
|
||||
const services: ServiceItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'authentication-service',
|
||||
owner: 'security-team',
|
||||
ownerAvatar: 'https://github.com/identicons/security-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
description: 'Handles user authentication, sessions and token refresh.',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'api-gateway',
|
||||
owner: 'platform-team',
|
||||
ownerAvatar: 'https://github.com/identicons/platform-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
description: 'Routes and validates all inbound API requests.',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'frontend-core',
|
||||
owner: 'design-system',
|
||||
ownerAvatar: 'https://github.com/identicons/design-system.png',
|
||||
type: 'library',
|
||||
lifecycle: 'production',
|
||||
description: 'Shared UI components and design tokens.',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'data-pipeline',
|
||||
owner: 'data-team',
|
||||
ownerAvatar: 'https://github.com/identicons/data-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'experimental',
|
||||
description: 'Streaming data ingestion pipeline for analytics.',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'developer-portal',
|
||||
owner: 'devex-team',
|
||||
ownerAvatar: 'https://github.com/identicons/devex-team.png',
|
||||
type: 'website',
|
||||
lifecycle: 'production',
|
||||
description: 'Internal developer portal built on Backstage.',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'notification-service',
|
||||
owner: 'platform-team',
|
||||
ownerAvatar: 'https://github.com/identicons/platform-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
description: 'Sends emails, Slack messages and push notifications.',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'search-indexer',
|
||||
owner: 'search-team',
|
||||
ownerAvatar: 'https://github.com/identicons/search-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'experimental',
|
||||
description: 'Indexes catalog entities for full-text search.',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'billing-service',
|
||||
owner: 'payments-team',
|
||||
ownerAvatar: 'https://github.com/identicons/payments-team.png',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
description: 'Manages subscriptions, invoices and payment processing.',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stat card component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const StatCard = ({ label, value, trend, trendColor }: MetricCard) => (
|
||||
<Card>
|
||||
<CardHeader>{label}</CardHeader>
|
||||
<CardBody>
|
||||
<Flex direction="column" gap="1">
|
||||
<Text variant="title-large" weight="bold">
|
||||
{value}
|
||||
</Text>
|
||||
<Text variant="body-medium" color={trendColor}>
|
||||
{trend}
|
||||
</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page layout component (the actual story render)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const columns: ColumnConfig<ServiceItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
defaultWidth: '3fr',
|
||||
cell: item => (
|
||||
<CellProfile
|
||||
name={item.name}
|
||||
src={item.ownerAvatar}
|
||||
description={item.description}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
defaultWidth: '2fr',
|
||||
cell: item => <CellText title={item.owner} />,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
label: 'Type',
|
||||
defaultWidth: '1fr',
|
||||
cell: item => <CellText title={item.type} />,
|
||||
},
|
||||
{
|
||||
id: 'lifecycle',
|
||||
label: 'Lifecycle',
|
||||
defaultWidth: '1fr',
|
||||
cell: item => (
|
||||
<CellText
|
||||
title={item.lifecycle}
|
||||
style={{
|
||||
color:
|
||||
item.lifecycle === 'production'
|
||||
? 'var(--bui-fg-success)'
|
||||
: 'var(--bui-fg-warning)',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const CardsWithTableLayout = () => {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => services,
|
||||
paginationOptions: { pageSize: 5 },
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginHeader title="Plugin" />
|
||||
<HeaderPage
|
||||
title="Page title"
|
||||
customActions={<Button>Custom action</Button>}
|
||||
/>
|
||||
<Container>
|
||||
<Flex direction="column" gap="6">
|
||||
<Grid.Root columns="3" gap="4">
|
||||
{metrics.map(metric => (
|
||||
<StatCard key={metric.label} {...metric} />
|
||||
))}
|
||||
</Grid.Root>
|
||||
<Table columnConfig={columns} {...tableProps} />
|
||||
</Flex>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storybook meta
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const meta = {
|
||||
title: 'Guidelines/Cards with Table',
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
decorators: [
|
||||
(Story: () => JSX.Element) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <CardsWithTableLayout />,
|
||||
};
|
||||
Reference in New Issue
Block a user