diff --git a/.changeset/chatty-wasps-sink.md b/.changeset/chatty-wasps-sink.md new file mode 100644 index 0000000000..7f12c5372c --- /dev/null +++ b/.changeset/chatty-wasps-sink.md @@ -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 diff --git a/.storybook/modes.ts b/.storybook/modes.ts index 7087fa35eb..ae4ff76a7c 100644 --- a/.storybook/modes.ts +++ b/.storybook/modes.ts @@ -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; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6a0177cafe..c5074f7749 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -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 */} - + {Array.from({ + length: + selectedBackground === 'app' + ? 0 + : parseInt(selectedBackground.split('-')[1], 10), + }).reduce( + children => ( + + {children} + + ), + , + )} ); diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index b72683f53e..89b71c48d3 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -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'] { diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 5e5df2073b..2e2f94b4e1 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -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: () => ( - - Default - - - - - - - - + + + + - - Neutral 1 - - - - - - - - - - - Neutral 2 - - - - - - - - - - - Neutral 3 - - - - - - - - + + + + ), @@ -208,20 +146,6 @@ export const Destructive = meta.story({ - - On Neutral 1 - - - - - - Sizes diff --git a/packages/ui/src/components/Container/Container.module.css b/packages/ui/src/components/Container/Container.module.css index a0dec1e581..b93665525f 100644 --- a/packages/ui/src/components/Container/Container.module.css +++ b/packages/ui/src/components/Container/Container.module.css @@ -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) { diff --git a/packages/ui/src/guidelines/CardsWithTable.stories.tsx b/packages/ui/src/guidelines/CardsWithTable.stories.tsx new file mode 100644 index 0000000000..51cc416423 --- /dev/null +++ b/packages/ui/src/guidelines/CardsWithTable.stories.tsx @@ -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) => ( + + {label} + + + + {value} + + + {trend} + + + + +); + +// --------------------------------------------------------------------------- +// Page layout component (the actual story render) +// --------------------------------------------------------------------------- + +const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + defaultWidth: '3fr', + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + defaultWidth: '2fr', + cell: item => , + }, + { + id: 'type', + label: 'Type', + defaultWidth: '1fr', + cell: item => , + }, + { + id: 'lifecycle', + label: 'Lifecycle', + defaultWidth: '1fr', + cell: item => ( + + ), + }, +]; + +const CardsWithTableLayout = () => { + const { tableProps } = useTable({ + mode: 'complete', + getData: () => services, + paginationOptions: { pageSize: 5 }, + }); + + return ( + <> + + Custom action} + /> + + + + {metrics.map(metric => ( + + ))} + + + + + + ); +}; + +// --------------------------------------------------------------------------- +// Storybook meta +// --------------------------------------------------------------------------- + +const meta = { + title: 'Guidelines/Cards with Table', + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (Story: () => JSX.Element) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +};