Add new surfaces to Card

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-06 06:52:29 +00:00
parent be7c1d73b2
commit 03f1bbcc10
7 changed files with 312 additions and 68 deletions
@@ -21,16 +21,42 @@
display: flex;
flex-direction: column;
gap: var(--bui-space-3);
background-color: var(--bui-bg-surface-1);
border-radius: var(--bui-radius-3);
padding-block: var(--bui-space-3);
color: var(--bui-fg-primary);
border: 1px solid var(--bui-border);
overflow: hidden;
min-height: 0;
width: 100%;
}
.bui-Card[data-surface='0'] {
background-color: var(--bui-bg-surface-0);
}
.bui-Card[data-surface='1'] {
background-color: var(--bui-bg-surface-1);
}
.bui-Card[data-surface='2'] {
background-color: var(--bui-bg-surface-2);
}
.bui-Card[data-surface='3'] {
background-color: var(--bui-bg-surface-3);
}
.bui-Card[data-surface='danger'] {
background-color: var(--bui-bg-danger);
}
.bui-Card[data-surface='warning'] {
background-color: var(--bui-bg-warning);
}
.bui-Card[data-surface='success'] {
background-color: var(--bui-bg-success);
}
.bui-CardBody {
flex: 1;
min-height: 0;
@@ -16,6 +16,9 @@
import preview from '../../../../../.storybook/preview';
import { Card, CardHeader, CardBody, CardFooter } from './Card';
import { Text } from '../..';
import { Flex } from '../Flex';
import { Box } from '../Box';
import { Button } from '../Button';
const meta = preview.meta({
title: 'Backstage UI/Card',
@@ -124,3 +127,121 @@ export const WithListRow = meta.story({
</Card>
),
});
export const Surfaces = meta.story({
render: args => (
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
<Card {...args} style={{ width: '200px' }}>
<CardHeader>Default</CardHeader>
<CardBody>No surface prop</CardBody>
</Card>
<Card {...args} surface="0" style={{ width: '200px' }}>
<CardHeader>Surface 0</CardHeader>
<CardBody>Explicit surface 0</CardBody>
</Card>
<Card {...args} surface="1" style={{ width: '200px' }}>
<CardHeader>Surface 1</CardHeader>
<CardBody>Explicit surface 1</CardBody>
</Card>
<Card {...args} surface="2" style={{ width: '200px' }}>
<CardHeader>Surface 2</CardHeader>
<CardBody>Explicit surface 2</CardBody>
</Card>
<Card {...args} surface="3" style={{ width: '200px' }}>
<CardHeader>Surface 3</CardHeader>
<CardBody>Explicit surface 3</CardBody>
</Card>
<Card
{...args}
surface={{ initial: '0', sm: '1' }}
style={{ width: '200px' }}
>
<CardHeader>Responsive</CardHeader>
<CardBody>Surface 0 → 1</CardBody>
</Card>
<Card {...args} surface="danger" style={{ width: '200px' }}>
<CardHeader>Danger</CardHeader>
<CardBody>Surface danger</CardBody>
</Card>
<Card {...args} surface="warning" style={{ width: '200px' }}>
<CardHeader>Warning</CardHeader>
<CardBody>Surface warning</CardBody>
</Card>
<Card {...args} surface="success" style={{ width: '200px' }}>
<CardHeader>Success</CardHeader>
<CardBody>Surface success</CardBody>
</Card>
</Flex>
),
});
export const SurfacesNested = meta.story({
render: args => (
<Flex direction="column">
<Box style={{ maxWidth: '600px' }} mb="4">
In this test, we are nesting cards on different surfaces to ensure that
the correct surface is applied to each element. If a Button is placed on
a surface that doesn't have the surface prop set, it will inherit the
surface from the parent.
</Box>
<Card {...args} surface="1" style={{ width: '500px' }}>
<CardHeader>Surface 1</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
<Card {...args} surface="2" style={{ marginTop: '16px' }}>
<CardHeader>Surface 2</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
<Card {...args} style={{ marginTop: '16px' }}>
<CardHeader>Inherited</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
</CardBody>
</Card>
</CardBody>
</Card>
</CardBody>
</Card>
</Flex>
),
});
export const SurfacesAutoIncrement = meta.story({
render: args => (
<Flex direction="column">
<Box style={{ maxWidth: '600px' }} mb="4">
Using surface="auto" automatically increments from the parent surface
level. This makes components more reusable as they don't need to know
their absolute surface level. Notice how each nested Card with
surface="auto" automatically increments: 0 → 1 → 2 → 3 (capped at 3).
</Box>
<Card {...args} surface="0" style={{ width: '500px' }}>
<CardHeader>Surface 0 (explicit)</CardHeader>
<CardBody>
<Card {...args} surface="auto">
<CardHeader>Surface auto (becomes 1)</CardHeader>
<CardBody>
<Button variant="secondary" onSurface="auto">
Button auto
</Button>
<Card {...args} surface="auto" style={{ marginTop: '16px' }}>
<CardHeader>Surface auto (becomes 2)</CardHeader>
<CardBody>
<Card {...args} surface="auto">
<CardHeader>Surface auto (becomes 3)</CardHeader>
<CardBody>
<Card {...args} surface="auto">
<CardHeader>Surface auto (stays 3 - capped)</CardHeader>
<CardBody>Capped at max surface level</CardBody>
</Card>
</CardBody>
</Card>
</CardBody>
</Card>
</CardBody>
</Card>
</CardBody>
</Card>
</Flex>
),
});
+38 -40
View File
@@ -15,16 +15,19 @@
*/
import { forwardRef } from 'react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { CardDefinition } from './definition';
import { useDefinition } from '../../hooks/useDefinition';
import {
CardDefinition,
CardHeaderDefinition,
CardBodyDefinition,
CardFooterDefinition,
} from './definition';
import type {
CardProps,
CardHeaderProps,
CardBodyProps,
CardFooterProps,
} from './types';
import styles from './Card.module.css';
/**
* Card component.
@@ -32,18 +35,21 @@ import styles from './Card.module.css';
* @public
*/
export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
const { classNames, cleanedProps } = useStyles(CardDefinition, props);
const { className, ...rest } = cleanedProps;
const { ownProps, restProps, dataAttributes } = useDefinition(
CardDefinition,
props,
);
const { classes, surfaceChildren } = ownProps;
return (
<div
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
<div ref={ref} className={classes.root} {...dataAttributes} {...restProps}>
{surfaceChildren}
</div>
);
});
Card.displayName = 'Card';
/**
* CardHeader component.
*
@@ -51,23 +57,19 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
*/
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
(props, ref) => {
const { classNames, cleanedProps } = useStyles(CardDefinition, props);
const { className, ...rest } = cleanedProps;
const { ownProps, restProps } = useDefinition(CardHeaderDefinition, props);
const { classes, children } = ownProps;
return (
<div
ref={ref}
className={clsx(
classNames.header,
styles[classNames.header],
className,
)}
{...rest}
/>
<div ref={ref} className={classes.root} {...restProps}>
{children}
</div>
);
},
);
CardHeader.displayName = 'CardHeader';
/**
* CardBody component.
*
@@ -75,19 +77,19 @@ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
*/
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
(props, ref) => {
const { classNames, cleanedProps } = useStyles(CardDefinition, props);
const { className, ...rest } = cleanedProps;
const { ownProps, restProps } = useDefinition(CardBodyDefinition, props);
const { classes, children } = ownProps;
return (
<div
ref={ref}
className={clsx(classNames.body, styles[classNames.body], className)}
{...rest}
/>
<div ref={ref} className={classes.root} {...restProps}>
{children}
</div>
);
},
);
CardBody.displayName = 'CardBody';
/**
* CardFooter component.
*
@@ -95,19 +97,15 @@ export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
*/
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
(props, ref) => {
const { classNames, cleanedProps } = useStyles(CardDefinition, props);
const { className, ...rest } = cleanedProps;
const { ownProps, restProps } = useDefinition(CardFooterDefinition, props);
const { classes, children } = ownProps;
return (
<div
ref={ref}
className={clsx(
classNames.footer,
styles[classNames.footer],
className,
)}
{...rest}
/>
<div ref={ref} className={classes.root} {...restProps}>
{children}
</div>
);
},
);
CardFooter.displayName = 'CardFooter';
+62 -6
View File
@@ -14,17 +14,73 @@
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
import { defineComponent } from '../../hooks/useDefinition';
import type {
CardOwnProps,
CardHeaderOwnProps,
CardBodyOwnProps,
CardFooterOwnProps,
} from './types';
import styles from './Card.module.css';
/**
* Component definition for Card
* @public
*/
export const CardDefinition = {
export const CardDefinition = defineComponent<CardOwnProps>()({
styles,
classNames: {
root: 'bui-Card',
header: 'bui-CardHeader',
body: 'bui-CardBody',
footer: 'bui-CardFooter',
},
} as const satisfies ComponentDefinition;
surface: 'container',
propDefs: {
surface: { dataAttribute: true, default: '1' },
children: {},
className: {},
},
});
/**
* Component definition for CardHeader
* @public
*/
export const CardHeaderDefinition = defineComponent<CardHeaderOwnProps>()({
styles,
classNames: {
root: 'bui-CardHeader',
},
propDefs: {
children: {},
className: {},
},
});
/**
* Component definition for CardBody
* @public
*/
export const CardBodyDefinition = defineComponent<CardBodyOwnProps>()({
styles,
classNames: {
root: 'bui-CardBody',
},
propDefs: {
children: {},
className: {},
},
});
/**
* Component definition for CardFooter
* @public
*/
export const CardFooterDefinition = defineComponent<CardFooterOwnProps>()({
styles,
classNames: {
root: 'bui-CardFooter',
},
propDefs: {
children: {},
className: {},
},
});
+7 -8
View File
@@ -15,11 +15,10 @@
*/
export { Card, CardHeader, CardBody, CardFooter } from './Card';
export { CardDefinition } from './definition';
export type {
CardProps,
CardHeaderProps,
CardBodyProps,
CardFooterProps,
} from './types';
export {
CardDefinition,
CardHeaderDefinition,
CardBodyDefinition,
CardFooterDefinition,
} from './definition';
export type * from './types';
+40 -12
View File
@@ -14,38 +14,66 @@
* limitations under the License.
*/
import type { ReactNode } from 'react';
import type { Responsive, Surface } from '../../types';
/** @public */
export type CardOwnProps = {
surface?: Responsive<Surface>;
children?: ReactNode;
className?: string;
};
/**
* Props for the Card component.
*
* @public
*/
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
export interface CardProps
extends CardOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
/** @public */
export type CardHeaderOwnProps = {
children?: ReactNode;
className?: string;
};
/**
* Props for the CardHeader component.
*
* @public
*/
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
export interface CardHeaderProps
extends CardHeaderOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
/** @public */
export type CardBodyOwnProps = {
children?: ReactNode;
className?: string;
};
/**
* Props for the CardBody component.
*
* @public
*/
export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
export interface CardBodyProps
extends CardBodyOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
/** @public */
export type CardFooterOwnProps = {
children?: ReactNode;
className?: string;
};
/**
* Props for the CardFooter component.
*
* @public
*/
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
export interface CardFooterProps
extends CardFooterOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
@@ -77,6 +77,22 @@ export function useDefinition<
}
}
// Override data-surface for container components with the resolved value
// This ensures 'auto' is replaced with the actual computed surface level
if (
definition.surface === 'container' &&
resolvedSurface !== undefined &&
dataAttributes['data-surface'] !== undefined
) {
const surfaceValue =
typeof resolvedSurface === 'object'
? resolveResponsiveValue(resolvedSurface as any, breakpoint)
: resolvedSurface;
if (surfaceValue !== undefined) {
dataAttributes['data-surface'] = String(surfaceValue);
}
}
// Add data-on-surface for leaf components
if (definition.surface === 'leaf' && resolvedSurface !== undefined) {
// Handle responsive surface values - for data attributes, use the resolved string