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