Add new surfaces to Card
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user