diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 6a0177cafe..3d9e197bbc 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -10,6 +10,7 @@ import { apis } from './support/apis';
import { useGlobals } from 'storybook/preview-api';
import { UnifiedThemeProvider, themes } from '@backstage/theme';
import { allModes } from './modes';
+import { Box } from '../packages/ui/src/components/Box';
// Default Backstage theme CSS (from packages/ui)
import '../packages/ui/src/css/styles.css';
@@ -70,7 +71,13 @@ export default definePreview({
options: {
storySort: {
- order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'],
+ order: [
+ 'Backstage UI',
+ 'Guidelines',
+ 'Plugins',
+ 'Layout',
+ 'Navigation',
+ ],
},
},
@@ -114,7 +121,7 @@ export default definePreview({
},
decorators: [
- Story => {
+ (Story, context) => {
const [globals] = useGlobals();
const selectedTheme =
globals.themeMode === 'light' ? themes.light : themes.dark;
@@ -138,7 +145,7 @@ export default definePreview({
(element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)';
});
- return (
+ const content = (
{/* @ts-ignore */}
@@ -147,6 +154,33 @@ export default definePreview({
);
+
+ if (selectedThemeName !== 'spotify') {
+ return content;
+ }
+
+ const layout = context.parameters?.layout ?? 'padded';
+ const isFullscreen = context.parameters?.layout === 'fullscreen';
+
+ return (
+
+ {content}
+
+ );
},
],
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/Container/Container.module.css b/packages/ui/src/components/Container/Container.module.css
index a0dec1e581..ef1d85c66c 100644
--- a/packages/ui/src/components/Container/Container.module.css
+++ b/packages/ui/src/components/Container/Container.module.css
@@ -21,7 +21,6 @@
max-width: 120rem;
padding-inline: var(--bui-space-4);
margin-inline: auto;
- transition: padding 0.2s ease-in-out;
}
@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: () => ,
+};