diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css
index 85ae86901b..e6e5758e67 100644
--- a/packages/ui/src/components/Card/Card.module.css
+++ b/packages/ui/src/components/Card/Card.module.css
@@ -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;
diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx
index 438dd0e353..d1d6c43327 100644
--- a/packages/ui/src/components/Card/Card.stories.tsx
+++ b/packages/ui/src/components/Card/Card.stories.tsx
@@ -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({
),
});
+
+export const Surfaces = meta.story({
+ render: args => (
+
+
+ Default
+ No surface prop
+
+
+ Surface 0
+ Explicit surface 0
+
+
+ Surface 1
+ Explicit surface 1
+
+
+ Surface 2
+ Explicit surface 2
+
+
+ Surface 3
+ Explicit surface 3
+
+
+ Responsive
+ Surface 0 → 1
+
+
+ Danger
+ Surface danger
+
+
+ Warning
+ Surface warning
+
+
+ Success
+ Surface success
+
+
+ ),
+});
+
+export const SurfacesNested = meta.story({
+ render: args => (
+
+
+ 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.
+
+
+ Surface 1
+
+
+
+ Surface 2
+
+
+
+ Inherited
+
+
+
+
+
+
+
+
+
+ ),
+});
+
+export const SurfacesAutoIncrement = meta.story({
+ render: args => (
+
+
+ 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).
+
+
+ Surface 0 (explicit)
+
+
+ Surface auto (becomes 1)
+
+
+
+ Surface auto (becomes 2)
+
+
+ Surface auto (becomes 3)
+
+
+ Surface auto (stays 3 - capped)
+ Capped at max surface level
+
+
+
+
+
+
+
+
+
+
+ ),
+});
diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx
index a3f4995588..b9ff308784 100644
--- a/packages/ui/src/components/Card/Card.tsx
+++ b/packages/ui/src/components/Card/Card.tsx
@@ -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((props, ref) => {
- const { classNames, cleanedProps } = useStyles(CardDefinition, props);
- const { className, ...rest } = cleanedProps;
+ const { ownProps, restProps, dataAttributes } = useDefinition(
+ CardDefinition,
+ props,
+ );
+ const { classes, surfaceChildren } = ownProps;
return (
-
+
+ {surfaceChildren}
+
);
});
+Card.displayName = 'Card';
+
/**
* CardHeader component.
*
@@ -51,23 +57,19 @@ export const Card = forwardRef((props, ref) => {
*/
export const CardHeader = forwardRef(
(props, ref) => {
- const { classNames, cleanedProps } = useStyles(CardDefinition, props);
- const { className, ...rest } = cleanedProps;
+ const { ownProps, restProps } = useDefinition(CardHeaderDefinition, props);
+ const { classes, children } = ownProps;
return (
-
+
+ {children}
+
);
},
);
+CardHeader.displayName = 'CardHeader';
+
/**
* CardBody component.
*
@@ -75,19 +77,19 @@ export const CardHeader = forwardRef(
*/
export const CardBody = forwardRef(
(props, ref) => {
- const { classNames, cleanedProps } = useStyles(CardDefinition, props);
- const { className, ...rest } = cleanedProps;
+ const { ownProps, restProps } = useDefinition(CardBodyDefinition, props);
+ const { classes, children } = ownProps;
return (
-
+
+ {children}
+
);
},
);
+CardBody.displayName = 'CardBody';
+
/**
* CardFooter component.
*
@@ -95,19 +97,15 @@ export const CardBody = forwardRef(
*/
export const CardFooter = forwardRef(
(props, ref) => {
- const { classNames, cleanedProps } = useStyles(CardDefinition, props);
- const { className, ...rest } = cleanedProps;
+ const { ownProps, restProps } = useDefinition(CardFooterDefinition, props);
+ const { classes, children } = ownProps;
return (
-
+
+ {children}
+
);
},
);
+
+CardFooter.displayName = 'CardFooter';
diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts
index cd0c5f634b..ec05494391 100644
--- a/packages/ui/src/components/Card/definition.ts
+++ b/packages/ui/src/components/Card/definition.ts
@@ -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()({
+ 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()({
+ styles,
+ classNames: {
+ root: 'bui-CardHeader',
+ },
+ propDefs: {
+ children: {},
+ className: {},
+ },
+});
+
+/**
+ * Component definition for CardBody
+ * @public
+ */
+export const CardBodyDefinition = defineComponent()({
+ styles,
+ classNames: {
+ root: 'bui-CardBody',
+ },
+ propDefs: {
+ children: {},
+ className: {},
+ },
+});
+
+/**
+ * Component definition for CardFooter
+ * @public
+ */
+export const CardFooterDefinition = defineComponent()({
+ styles,
+ classNames: {
+ root: 'bui-CardFooter',
+ },
+ propDefs: {
+ children: {},
+ className: {},
+ },
+});
diff --git a/packages/ui/src/components/Card/index.ts b/packages/ui/src/components/Card/index.ts
index dc7dbd4ead..f5596a717a 100644
--- a/packages/ui/src/components/Card/index.ts
+++ b/packages/ui/src/components/Card/index.ts
@@ -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';
diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts
index 8b3419f6a2..50a94683e2 100644
--- a/packages/ui/src/components/Card/types.ts
+++ b/packages/ui/src/components/Card/types.ts
@@ -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;
+ children?: ReactNode;
+ className?: string;
+};
+
/**
* Props for the Card component.
*
* @public
*/
-export interface CardProps extends React.HTMLAttributes {
- children?: React.ReactNode;
-}
+export interface CardProps
+ extends CardOwnProps,
+ React.HTMLAttributes {}
+
+/** @public */
+export type CardHeaderOwnProps = {
+ children?: ReactNode;
+ className?: string;
+};
/**
* Props for the CardHeader component.
*
* @public
*/
-export interface CardHeaderProps extends React.HTMLAttributes {
- children?: React.ReactNode;
-}
+export interface CardHeaderProps
+ extends CardHeaderOwnProps,
+ React.HTMLAttributes {}
+
+/** @public */
+export type CardBodyOwnProps = {
+ children?: ReactNode;
+ className?: string;
+};
/**
* Props for the CardBody component.
*
* @public
*/
-export interface CardBodyProps extends React.HTMLAttributes {
- children?: React.ReactNode;
-}
+export interface CardBodyProps
+ extends CardBodyOwnProps,
+ React.HTMLAttributes {}
+
+/** @public */
+export type CardFooterOwnProps = {
+ children?: ReactNode;
+ className?: string;
+};
/**
* Props for the CardFooter component.
*
* @public
*/
-export interface CardFooterProps extends React.HTMLAttributes {
- children?: React.ReactNode;
-}
+export interface CardFooterProps
+ extends CardFooterOwnProps,
+ React.HTMLAttributes {}
diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx
index 0de2eb8358..c8bd3853dd 100644
--- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx
+++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx
@@ -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