Merge pull request #32711 from backstage/bui-surfaces

BUI - Replace Surface system with Bg (neutral background) system
This commit is contained in:
Charles de Dreuille
2026-02-11 10:49:55 +00:00
committed by GitHub
48 changed files with 997 additions and 866 deletions
+148 -79
View File
@@ -144,7 +144,6 @@ export const AlertDefinition: {
readonly spinner: 'bui-AlertSpinner';
readonly actions: 'bui-AlertActions';
};
readonly surface: 'container';
readonly propDefs: {
readonly status: {
readonly dataAttribute: true;
@@ -157,7 +156,6 @@ export const AlertDefinition: {
readonly customActions: {};
readonly title: {};
readonly description: {};
readonly surface: {};
readonly className: {};
readonly style: {};
};
@@ -165,7 +163,7 @@ export const AlertDefinition: {
};
// @public (undocumented)
export type AlertOwnProps = ContainerSurfaceProps & {
export type AlertOwnProps = {
status?: Responsive<'info' | 'success' | 'warning' | 'danger'>;
icon?: boolean | ReactElement;
loading?: boolean;
@@ -207,6 +205,23 @@ export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
src: string;
}
// @public (undocumented)
export interface BgContextValue {
// (undocumented)
bg: ContainerBg | undefined;
}
// @public
export const BgProvider: ({ bg, children }: BgProviderProps) => JSX_2.Element;
// @public (undocumented)
export interface BgProviderProps {
// (undocumented)
bg: ContainerBg;
// (undocumented)
children: ReactNode;
}
// @public (undocumented)
export type Border = 'none' | 'base' | 'error' | 'warning' | 'selected';
@@ -234,12 +249,12 @@ export const BoxDefinition: {
readonly classNames: {
readonly root: 'bui-Box';
};
readonly surface: 'container';
readonly bg: 'provider';
readonly propDefs: {
readonly as: {
readonly default: 'div';
};
readonly surface: {
readonly bg: {
readonly dataAttribute: true;
};
readonly children: {};
@@ -275,7 +290,7 @@ export const BoxDefinition: {
// @public (undocumented)
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
surface?: Responsive<Surface>;
bg?: Responsive<ProviderBg>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -320,7 +335,7 @@ export const ButtonDefinition: {
readonly content: 'bui-ButtonContent';
readonly spinner: 'bui-ButtonSpinner';
};
readonly surface: 'leaf';
readonly bg: 'consumer';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -338,7 +353,6 @@ export const ButtonDefinition: {
};
readonly iconStart: {};
readonly iconEnd: {};
readonly onSurface: {};
readonly children: {};
readonly className: {};
readonly style: {};
@@ -360,7 +374,7 @@ export const ButtonIconDefinition: {
readonly content: 'bui-ButtonIconContent';
readonly spinner: 'bui-ButtonIconSpinner';
};
readonly surface: 'leaf';
readonly bg: 'consumer';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -374,14 +388,13 @@ export const ButtonIconDefinition: {
readonly dataAttribute: true;
};
readonly icon: {};
readonly onSurface: {};
readonly className: {};
readonly style: {};
};
};
// @public (undocumented)
export type ButtonIconOwnProps = LeafSurfaceProps & {
export type ButtonIconOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
icon?: ReactElement;
@@ -409,7 +422,7 @@ export const ButtonLinkDefinition: {
readonly root: 'bui-ButtonLink';
readonly content: 'bui-ButtonLinkContent';
};
readonly surface: 'leaf';
readonly bg: 'consumer';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -421,7 +434,6 @@ export const ButtonLinkDefinition: {
};
readonly iconStart: {};
readonly iconEnd: {};
readonly onSurface: {};
readonly children: {};
readonly className: {};
readonly style: {};
@@ -429,7 +441,7 @@ export const ButtonLinkDefinition: {
};
// @public (undocumented)
export type ButtonLinkOwnProps = LeafSurfaceProps & {
export type ButtonLinkOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
iconStart?: ReactElement;
@@ -445,7 +457,7 @@ export interface ButtonLinkProps
ButtonLinkOwnProps {}
// @public (undocumented)
export type ButtonOwnProps = LeafSurfaceProps & {
export type ButtonOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
destructive?: boolean;
@@ -473,18 +485,41 @@ export const CardBody: ForwardRefExoticComponent<
>;
// @public
export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
children?: React.ReactNode;
}
export const CardBodyDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-CardBody';
};
readonly propDefs: {
readonly children: {};
readonly className: {};
};
};
// @public (undocumented)
export type CardBodyOwnProps = {
children?: ReactNode;
className?: string;
};
// @public
export interface CardBodyProps
extends CardBodyOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
// @public
export const CardDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Card';
readonly header: 'bui-CardHeader';
readonly body: 'bui-CardBody';
readonly footer: 'bui-CardFooter';
};
readonly propDefs: {
readonly children: {};
readonly className: {};
};
};
@@ -494,10 +529,29 @@ export const CardFooter: ForwardRefExoticComponent<
>;
// @public
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
children?: React.ReactNode;
}
export const CardFooterDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-CardFooter';
};
readonly propDefs: {
readonly children: {};
readonly className: {};
};
};
// @public (undocumented)
export type CardFooterOwnProps = {
children?: ReactNode;
className?: string;
};
// @public
export interface CardFooterProps
extends CardFooterOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
// @public
export const CardHeader: ForwardRefExoticComponent<
@@ -505,16 +559,40 @@ export const CardHeader: ForwardRefExoticComponent<
>;
// @public
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
children?: React.ReactNode;
}
export const CardHeaderDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-CardHeader';
};
readonly propDefs: {
readonly children: {};
readonly className: {};
};
};
// @public (undocumented)
export type CardHeaderOwnProps = {
children?: ReactNode;
className?: string;
};
// @public
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
// (undocumented)
children?: React.ReactNode;
}
export interface CardHeaderProps
extends CardHeaderOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
// @public (undocumented)
export type CardOwnProps = {
children?: ReactNode;
className?: string;
};
// @public
export interface CardProps
extends CardOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
// @public (undocumented)
export const Cell: {
@@ -654,6 +732,15 @@ export const Container: ForwardRefExoticComponent<
ContainerProps & RefAttributes<HTMLDivElement>
>;
// @public
export type ContainerBg =
| 'neutral-1'
| 'neutral-2'
| 'neutral-3'
| 'danger'
| 'warning'
| 'success';
// @public
export const ContainerDefinition: {
readonly classNames: {
@@ -684,12 +771,6 @@ export interface ContainerProps {
style?: React.CSSProperties;
}
// @public (undocumented)
export interface ContainerSurfaceProps {
// (undocumented)
surface?: Responsive<Surface>;
}
// @public (undocumented)
export interface CursorParams<TFilter> {
// (undocumented)
@@ -861,11 +942,10 @@ export const FlexDefinition: {
'direction',
];
readonly dataAttributes: {
readonly surface: readonly [
'0',
'1',
'2',
'3',
readonly bg: readonly [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
@@ -881,6 +961,8 @@ export interface FlexProps extends SpaceProps {
// (undocumented)
align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>;
// (undocumented)
bg?: Responsive<ProviderBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
className?: string;
@@ -892,8 +974,6 @@ export interface FlexProps extends SpaceProps {
justify?: Responsive<'start' | 'center' | 'end' | 'between'>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
surface?: Responsive<Surface>;
}
// @public (undocumented)
@@ -931,11 +1011,10 @@ export const GridDefinition: {
'py',
];
readonly dataAttributes: {
readonly surface: readonly [
'0',
'1',
'2',
'3',
readonly bg: readonly [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
@@ -950,11 +1029,10 @@ export const GridItemDefinition: {
};
readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan'];
readonly dataAttributes: {
readonly surface: readonly [
'0',
'1',
'2',
'3',
readonly bg: readonly [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
@@ -964,6 +1042,8 @@ export const GridItemDefinition: {
// @public (undocumented)
export interface GridItemProps {
// (undocumented)
bg?: Responsive<ProviderBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -978,12 +1058,12 @@ export interface GridItemProps {
rowSpan?: Responsive<Columns>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
surface?: Responsive<Surface>;
}
// @public (undocumented)
export interface GridProps extends SpaceProps {
// (undocumented)
bg?: Responsive<ProviderBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -994,8 +1074,6 @@ export interface GridProps extends SpaceProps {
gap?: Responsive<Space>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
surface?: Responsive<Surface>;
}
// @public
@@ -1088,12 +1166,6 @@ export type JustifyContent =
| 'around'
| 'between';
// @public (undocumented)
export interface LeafSurfaceProps {
// (undocumented)
onSurface?: Responsive<Surface>;
}
// @public (undocumented)
export const Link: ForwardRefExoticComponent<
LinkProps & RefAttributes<HTMLAnchorElement>
@@ -1444,6 +1516,9 @@ export interface PopoverProps extends Omit<PopoverProps_2, 'children'> {
hideArrow?: boolean;
}
// @public
export type ProviderBg = ContainerBg | 'neutral-auto';
// @public (undocumented)
export interface QueryOptions<TFilter> {
// (undocumented)
@@ -1655,17 +1730,6 @@ export const SubmenuTrigger: (props: SubmenuTriggerProps) => JSX_2.Element;
// @public (undocumented)
export interface SubmenuTriggerProps extends SubmenuTriggerProps_2 {}
// @public
export type Surface =
| '0'
| '1'
| '2'
| '3'
| 'danger'
| 'warning'
| 'success'
| 'auto';
// @public (undocumented)
export const Switch: ForwardRefExoticComponent<
SwitchProps & RefAttributes<HTMLLabelElement>
@@ -2081,7 +2145,6 @@ export interface ToggleButtonProps extends ToggleButtonProps_2 {
iconEnd?: ReactElement;
// (undocumented)
iconStart?: ReactElement;
onSurface?: Responsive<Surface>;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -2110,6 +2173,12 @@ export const TooltipTrigger: (
props: TooltipTriggerComponentProps,
) => JSX_2.Element;
// @public
export function useBgConsumer(): BgContextValue;
// @public
export function useBgProvider(bg?: Responsive<ProviderBg>): BgContextValue;
// @public (undocumented)
export const useBreakpoint: () => {
breakpoint: Breakpoint;
@@ -284,50 +284,38 @@ export const LongContent = meta.story({
),
});
export const OnDifferentSurfaces = meta.story({
export const OnDifferentBackgrounds = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default Surface</Text>
<Text>Default</Text>
<Flex direction="column" gap="2" p="4">
<Alert status="info" icon={true} title="Alert on default surface" />
<Alert
status="success"
icon={true}
title="Alert on default surface"
/>
<Alert status="info" icon={true} title="Alert on default bg" />
<Alert status="success" icon={true} title="Alert on default bg" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex direction="column" gap="2" surface="0" p="4">
<Alert status="info" icon={true} title="Alert on surface 0" />
<Alert status="success" icon={true} title="Alert on surface 0" />
<Text>On Neutral 1</Text>
<Flex direction="column" gap="2" bg="neutral-1" p="4">
<Alert status="info" icon={true} title="Alert on neutral-1" />
<Alert status="success" icon={true} title="Alert on neutral-1" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex direction="column" gap="2" surface="1" p="4">
<Alert status="info" icon={true} title="Alert on surface 1" />
<Alert status="success" icon={true} title="Alert on surface 1" />
<Text>On Neutral 2</Text>
<Flex direction="column" gap="2" bg="neutral-2" p="4">
<Alert status="info" icon={true} title="Alert on neutral-2" />
<Alert status="success" icon={true} title="Alert on neutral-2" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex direction="column" gap="2" surface="2" p="4">
<Alert status="info" icon={true} title="Alert on surface 2" />
<Alert status="success" icon={true} title="Alert on surface 2" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex direction="column" gap="2" surface="3" p="4">
<Alert status="info" icon={true} title="Alert on surface 3" />
<Alert status="success" icon={true} title="Alert on surface 3" />
<Text>On Neutral 3</Text>
<Flex direction="column" gap="2" bg="neutral-3" p="4">
<Alert status="info" icon={true} title="Alert on neutral-3" />
<Alert status="success" icon={true} title="Alert on neutral-3" />
</Flex>
</Flex>
</Flex>
@@ -349,7 +337,7 @@ export const Responsive = meta.story({
export const WithUtilityProps = meta.story({
render: () => (
<Box surface="1" py="4">
<Box bg="neutral-1" py="4">
<Alert
status="success"
icon={true}
@@ -34,7 +34,6 @@ export const AlertDefinition = defineComponent<AlertOwnProps>()({
spinner: 'bui-AlertSpinner',
actions: 'bui-AlertActions',
},
surface: 'container',
propDefs: {
status: { dataAttribute: true, default: 'info' },
loading: { dataAttribute: true },
@@ -42,7 +41,6 @@ export const AlertDefinition = defineComponent<AlertOwnProps>()({
customActions: {},
title: {},
description: {},
surface: {},
className: {},
style: {},
},
+2 -6
View File
@@ -15,14 +15,10 @@
*/
import type { ReactElement, ReactNode, CSSProperties } from 'react';
import type {
ContainerSurfaceProps,
Responsive,
MarginProps,
} from '../../types';
import type { Responsive, MarginProps } from '../../types';
/** @public */
export type AlertOwnProps = ContainerSurfaceProps & {
export type AlertOwnProps = {
status?: Responsive<'info' | 'success' | 'warning' | 'danger'>;
icon?: boolean | ReactElement;
loading?: boolean;
+6 -10
View File
@@ -23,31 +23,27 @@
color: var(--bui-fg-primary);
}
.bui-Box[data-surface='0'] {
background-color: var(--bui-bg-neutral-0);
}
.bui-Box[data-surface='1'] {
.bui-Box[data-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-1);
}
.bui-Box[data-surface='2'] {
.bui-Box[data-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-2);
}
.bui-Box[data-surface='3'] {
.bui-Box[data-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-3);
}
.bui-Box[data-surface='danger'] {
.bui-Box[data-bg='danger'] {
background-color: var(--bui-bg-danger);
}
.bui-Box[data-surface='warning'] {
.bui-Box[data-bg='warning'] {
background-color: var(--bui-bg-warning);
}
.bui-Box[data-surface='success'] {
.bui-Box[data-bg='success'] {
background-color: var(--bui-bg-success);
}
}
+23 -66
View File
@@ -346,90 +346,47 @@ export const Display = meta.story({
),
});
export const Surfaces = meta.story({
export const BackgroundColors = meta.story({
args: { px: '6', py: '4' },
render: args => (
<Flex align="center" style={{ flexWrap: 'wrap' }}>
<Box {...args}>Default</Box>
<Box surface="0" {...args}>
Surface 0
<Box bg="neutral-1" {...args}>
Neutral 1
</Box>
<Box surface="1" {...args}>
Surface 1
<Box bg="neutral-2" {...args}>
Neutral 2
</Box>
<Box surface="2" {...args}>
Surface 2
<Box bg="neutral-3" {...args}>
Neutral 3
</Box>
<Box surface="3" {...args}>
Surface 3
<Box bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Neutral
</Box>
<Box surface={{ initial: '0', sm: '1' }} {...args}>
Responsive Surface
<Box bg="danger" {...args}>
Danger
</Box>
<Box surface="danger" {...args}>
Surface Danger
<Box bg="warning" {...args}>
Warning
</Box>
<Box surface="warning" {...args}>
Surface Warning
</Box>
<Box surface="success" {...args}>
Surface Success
<Box bg="success" {...args}>
Success
</Box>
</Flex>
),
});
export const SurfacesNested = meta.story({
export const NestedNeutralColors = meta.story({
args: { px: '6', py: '4' },
render: args => (
<Flex direction="column">
<Box style={{ maxWidth: '600px' }} mb="4">
In this test, we are nesting boxes and buttons 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>
<Box {...args} surface="1">
<Button variant="secondary">Button</Button>
<Box {...args} surface="2" mt="4">
<Button variant="secondary">Button</Button>
<Box {...args} mt="4">
<Button variant="secondary">Button</Button>
</Box>
<Box {...args} bg="neutral-1">
<Button variant="secondary">Button (on neutral-1)</Button>
<Box {...args} bg="neutral-2" mt="4">
<Button variant="secondary">Button (on neutral-2)</Button>
<Box {...args} bg="neutral-3" mt="4">
<Button variant="secondary">Button (on neutral-3)</Button>
</Box>
</Box>
</Flex>
),
});
export const SurfacesAutoIncrement = meta.story({
args: { px: '6', py: '4' },
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 Box with
surface="auto" automatically increments: 0 1 2 3 (capped at 3).
</Box>
<Box {...args} surface="0">
Surface 0 (explicit)
<Box {...args} surface="auto" mt="4">
<Box mb="3">Surface auto (becomes 1)</Box>
<Button variant="secondary" onSurface="auto">
Button auto
</Button>
<Box {...args} surface="auto" mt="4">
Surface auto (becomes 2)
<Box {...args} surface="auto" mt="4">
Surface auto (becomes 3)
<Box {...args} surface="auto" mt="4">
Surface auto (stays 3 - capped)
</Box>
</Box>
</Box>
</Box>
</Box>
</Flex>
</Box>
),
});
+2 -2
View File
@@ -25,7 +25,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
BoxDefinition,
props,
);
const { classes, as, surfaceChildren } = ownProps;
const { classes, as, childrenWithBgProvider } = ownProps;
return createElement(
as,
@@ -36,7 +36,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
...dataAttributes,
...restProps,
},
surfaceChildren,
childrenWithBgProvider,
);
});
+2 -2
View File
@@ -27,10 +27,10 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
classNames: {
root: 'bui-Box',
},
surface: 'container',
bg: 'provider',
propDefs: {
as: { default: 'div' },
surface: { dataAttribute: true },
bg: { dataAttribute: true },
children: {},
className: {},
style: {},
+2 -2
View File
@@ -15,12 +15,12 @@
*/
import type { ReactNode, CSSProperties } from 'react';
import type { Responsive, Surface, SpaceProps } from '../../types';
import type { Responsive, ProviderBg, SpaceProps } from '../../types';
/** @public */
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
surface?: Responsive<Surface>;
bg?: Responsive<ProviderBg>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -118,19 +118,19 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg: var(--bui-bg-neutral-2);
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg: var(--bui-bg-neutral-3);
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg: var(--bui-bg-neutral-4);
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
@@ -182,17 +182,17 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
}
@@ -81,32 +81,9 @@ export const Variants = meta.story({
</Button>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>Neutral 0</Text>
<Flex align="center" surface="0" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
Button
</Button>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>Neutral 1</Text>
<Flex align="center" surface="1" p="4">
<Flex align="center" bg="neutral-1" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
@@ -129,63 +106,49 @@ export const Variants = meta.story({
</Flex>
<Flex direction="column" gap="4">
<Text>Neutral 2</Text>
<Box surface="1">
<Flex align="center" surface="2" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
Button
</Button>
</Flex>
</Box>
<Flex align="center" bg="neutral-2" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
Button
</Button>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>Neutral 3</Text>
<Box surface="1">
<Box surface="2">
<Flex align="center" surface="3" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button
iconStart={<RiCloudLine />}
variant="secondary"
destructive
>
Button
</Button>
<Button
iconStart={<RiCloudLine />}
variant="tertiary"
destructive
>
Button
</Button>
</Flex>
</Box>
</Box>
<Flex align="center" bg="neutral-3" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
Button
</Button>
</Flex>
</Flex>
</Flex>
),
@@ -246,8 +209,8 @@ export const Destructive = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4" gap="4">
<Text>On Neutral 1</Text>
<Flex align="center" bg="neutral-1" p="4" gap="4">
<Button variant="primary" destructive>
Primary
</Button>
@@ -462,49 +425,33 @@ export const LoadingVariants = meta.story({
),
});
export const OnSurfaceAuto = meta.story({
export const AutoBg = meta.story({
render: () => (
<Flex direction="column" gap="4">
<div style={{ maxWidth: '600px' }}>
Using onSurface="auto" on buttons inherits their container's surface
level, making them reusable. This is equivalent to not specifying
onSurface. To override, use explicit surface values like onSurface="0"
or onSurface="2".
Buttons automatically detect their parent bg context and increment the
neutral level by 1. No prop is needed on the button -- it's fully
automatic.
</div>
<Box surface="0" p="4">
<Text>Surface 0 container</Text>
<Box bg="neutral-1" p="4">
<Text>Neutral 1 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Default (inherits 0)</Button>
<Button variant="secondary" onSurface="auto">
Auto (inherits 0)
</Button>
<Button variant="secondary" onSurface="1">
Explicit 1
</Button>
<Button variant="secondary">Auto (neutral-2)</Button>
<Button variant="tertiary">Auto (neutral-2)</Button>
</Flex>
</Box>
<Box surface="1" p="4">
<Text>Surface 1 container</Text>
<Box bg="neutral-2" p="4">
<Text>Neutral 2 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Default (inherits 1)</Button>
<Button variant="secondary" onSurface="auto">
Auto (inherits 1)
</Button>
<Button variant="secondary" onSurface="2">
Explicit 2
</Button>
<Button variant="secondary">Auto (neutral-3)</Button>
<Button variant="tertiary">Auto (neutral-3)</Button>
</Flex>
</Box>
<Box surface="2" p="4">
<Text>Surface 2 container</Text>
<Box bg="neutral-3" p="4">
<Text>Neutral 3 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Default (inherits 2)</Button>
<Button variant="secondary" onSurface="auto">
Auto (inherits 2)
</Button>
<Button variant="secondary" onSurface="3">
Explicit 3
</Button>
<Button variant="secondary">Auto (neutral-4)</Button>
<Button variant="tertiary">Auto (neutral-4)</Button>
</Flex>
</Box>
</Flex>
@@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent<ButtonOwnProps>()({
content: 'bui-ButtonContent',
spinner: 'bui-ButtonSpinner',
},
surface: 'leaf',
bg: 'consumer',
propDefs: {
size: { dataAttribute: true, default: 'small' },
variant: { dataAttribute: true, default: 'primary' },
@@ -37,7 +37,6 @@ export const ButtonDefinition = defineComponent<ButtonOwnProps>()({
loading: { dataAttribute: true },
iconStart: {},
iconEnd: {},
onSurface: {},
children: {},
className: {},
style: {},
+2 -2
View File
@@ -16,10 +16,10 @@
import type { ReactElement, ReactNode, CSSProperties } from 'react';
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
import type { LeafSurfaceProps, Responsive } from '../../types';
import type { Responsive } from '../../types';
/** @public */
export type ButtonOwnProps = LeafSurfaceProps & {
export type ButtonOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
destructive?: boolean;
@@ -85,19 +85,19 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg: var(--bui-bg-neutral-2);
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg: var(--bui-bg-neutral-3);
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg: var(--bui-bg-neutral-4);
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
@@ -122,17 +122,17 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
}
@@ -29,13 +29,12 @@ export const ButtonIconDefinition = defineComponent<ButtonIconOwnProps>()({
content: 'bui-ButtonIconContent',
spinner: 'bui-ButtonIconSpinner',
},
surface: 'leaf',
bg: 'consumer',
propDefs: {
size: { dataAttribute: true, default: 'small' },
variant: { dataAttribute: true, default: 'primary' },
loading: { dataAttribute: true },
icon: {},
onSurface: {},
className: {},
style: {},
},
@@ -16,10 +16,10 @@
import type { ReactElement, CSSProperties } from 'react';
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
import type { LeafSurfaceProps, Responsive } from '../../types';
import type { Responsive } from '../../types';
/** @public */
export type ButtonIconOwnProps = LeafSurfaceProps & {
export type ButtonIconOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
icon?: ReactElement;
@@ -79,19 +79,19 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg: var(--bui-bg-neutral-2);
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg: var(--bui-bg-neutral-3);
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg: var(--bui-bg-neutral-4);
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
@@ -115,17 +115,17 @@
--bg-active: var(--bui-bg-neutral-1-pressed);
--fg: var(--bui-fg-primary);
&[data-on-surface='1'] {
&[data-on-bg='neutral-1'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-on-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-on-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-4-hover);
--bg-active: var(--bui-bg-neutral-4-pressed);
}
@@ -28,13 +28,12 @@ export const ButtonLinkDefinition = defineComponent<ButtonLinkOwnProps>()({
root: 'bui-ButtonLink',
content: 'bui-ButtonLinkContent',
},
surface: 'leaf',
bg: 'consumer',
propDefs: {
size: { dataAttribute: true, default: 'small' },
variant: { dataAttribute: true, default: 'primary' },
iconStart: {},
iconEnd: {},
onSurface: {},
children: {},
className: {},
style: {},
@@ -16,10 +16,10 @@
import type { ReactElement, ReactNode, CSSProperties } from 'react';
import type { LinkProps as RALinkProps } from 'react-aria-components';
import type { LeafSurfaceProps, Responsive } from '../../types';
import type { Responsive } from '../../types';
/** @public */
export type ButtonLinkOwnProps = LeafSurfaceProps & {
export type ButtonLinkOwnProps = {
size?: Responsive<'small' | 'medium'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
iconStart?: ReactElement;
@@ -21,11 +21,9 @@
display: flex;
flex-direction: column;
gap: var(--bui-space-3);
background-color: var(--bui-bg-neutral-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%;
@@ -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,119 @@ export const WithListRow = meta.story({
</Card>
),
});
export const Backgrounds = meta.story({
render: args => (
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
<Card {...args} style={{ width: '200px' }}>
<CardHeader>No parent</CardHeader>
<CardBody>Defaults to neutral-1</CardBody>
</Card>
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-1</CardHeader>
<CardBody>Auto-increments to neutral-2</CardBody>
</Card>
</Box>
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Auto-increments to neutral-3</CardBody>
</Card>
</Box>
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Steps up to neutral-4</CardBody>
</Card>
</Box>
</Flex>
),
});
export const BgNested = meta.story({
render: args => (
<Flex direction="column">
<Box style={{ maxWidth: '600px' }} mb="4">
Nested cards auto-increment their neutral level. Buttons inherit the
parent card's bg via data-on-bg.
</Box>
<Card {...args} style={{ width: '500px' }}>
<CardHeader>Card (visual: neutral-1, provides: neutral-1)</CardHeader>
<CardBody>
<Button variant="secondary">Button (on neutral-1)</Button>
<Card {...args} style={{ marginTop: '16px' }}>
<CardHeader>
Card (visual: neutral-2, provides: neutral-2)
</CardHeader>
<CardBody>
<Button variant="secondary">Button (on neutral-2)</Button>
<Card {...args} style={{ marginTop: '16px' }}>
<CardHeader>
Card (visual: neutral-4, provides: neutral-3)
</CardHeader>
<CardBody>
<Button variant="secondary">Button (on neutral-3)</Button>
</CardBody>
</Card>
</CardBody>
</Card>
</CardBody>
</Card>
</Flex>
),
});
export const BgOnProviders = meta.story({
render: args => (
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
<Card {...args} style={{ width: '200px' }}>
<CardHeader>No provider</CardHeader>
<CardBody>Card defaults to neutral-1</CardBody>
</Card>
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-1</CardHeader>
<CardBody>Card auto-increments to neutral-2</CardBody>
</Card>
</Box>
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Card auto-increments to neutral-3</CardBody>
</Card>
</Box>
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Card visually at neutral-4</CardBody>
</Card>
</Box>
</Flex>
),
});
export const CustomCardWithBox = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Box style={{ maxWidth: '600px' }}>
A custom card built with Box. Use Box with an explicit bg prop to create
a card-like container that participates in the bg system as a provider.
</Box>
<Box
bg="neutral-auto"
p="4"
style={{ borderRadius: '8px', width: '300px' }}
>
<Button variant="secondary" style={{ marginTop: '8px' }}>
Button (on neutral-1)
</Button>
</Box>
<Card style={{ width: '300px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
</Flex>
),
});
+44 -39
View File
@@ -15,16 +15,20 @@
*/
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';
import { Box } from '../Box/Box';
/**
* Card component.
@@ -32,18 +36,27 @@ 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, children } = ownProps;
return (
<div
<Box
bg="neutral-auto"
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
className={classes.root}
{...dataAttributes}
{...restProps}
>
{children}
</Box>
);
});
Card.displayName = 'Card';
/**
* CardHeader component.
*
@@ -51,23 +64,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 +84,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 +104,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';
+60 -6
View File
@@ -14,17 +14,71 @@
* 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;
propDefs: {
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';
+38 -12
View File
@@ -14,38 +14,64 @@
* limitations under the License.
*/
import type { ReactNode } from 'react';
/** @public */
export type CardOwnProps = {
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> {}
@@ -24,31 +24,27 @@
min-width: 0;
}
.bui-Flex[data-surface='0'] {
background-color: var(--bui-bg-neutral-0);
}
.bui-Flex[data-surface='1'] {
.bui-Flex[data-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-1);
}
.bui-Flex[data-surface='2'] {
.bui-Flex[data-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-2);
}
.bui-Flex[data-surface='3'] {
.bui-Flex[data-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-3);
}
.bui-Flex[data-surface='danger'] {
.bui-Flex[data-bg='danger'] {
background-color: var(--bui-bg-danger);
}
.bui-Flex[data-surface='warning'] {
.bui-Flex[data-bg='warning'] {
background-color: var(--bui-bg-warning);
}
.bui-Flex[data-surface='success'] {
.bui-Flex[data-bg='success'] {
background-color: var(--bui-bg-success);
}
}
@@ -244,7 +244,7 @@ export const WithTextTruncate = meta.story({
),
});
export const Surfaces = meta.story({
export const Backgrounds = meta.story({
args: {
px: '6',
py: '4',
@@ -252,54 +252,46 @@ export const Surfaces = meta.story({
render: args => (
<Flex align="center" style={{ flexWrap: 'wrap' }}>
<Flex {...args}>Default</Flex>
<Flex surface="0" {...args}>
Surface 0
<Flex bg="neutral-1" {...args}>
Neutral 1
</Flex>
<Flex surface="1" {...args}>
Surface 1
<Flex bg="neutral-2" {...args}>
Neutral 2
</Flex>
<Flex surface="2" {...args}>
Surface 2
<Flex bg="neutral-3" {...args}>
Neutral 3
</Flex>
<Flex surface="3" {...args}>
Surface 3
<Flex bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Bg
</Flex>
<Flex surface={{ initial: '0', sm: '1' }} {...args}>
Responsive Surface
<Flex bg="danger" {...args}>
Danger
</Flex>
<Flex surface="danger" {...args}>
Surface Danger
<Flex bg="warning" {...args}>
Warning
</Flex>
<Flex surface="warning" {...args}>
Surface Warning
</Flex>
<Flex surface="success" {...args}>
Surface Success
<Flex bg="success" {...args}>
Success
</Flex>
</Flex>
),
});
export const SurfacesAutoIncrement = meta.story({
export const BgNeutralAuto = meta.story({
args: { px: '6', py: '4', gap: '4' },
render: args => (
<Flex direction="column">
<div style={{ maxWidth: '600px', marginBottom: '16px' }}>
Using surface="auto" automatically increments from the parent surface.
This allows components to be reusable without hardcoding surface levels.
Using bg="neutral-auto" on Flex auto-increments from the parent context.
The first Flex defaults to neutral-1 (no parent), then each nested Flex
increments by one, capping at neutral-3.
</div>
<Flex {...args} surface="0" direction="column">
<div>Surface 0 (explicit)</div>
<Flex {...args} surface="auto" direction="column">
<div>Surface auto (becomes 1)</div>
<Flex {...args} surface="auto" direction="column">
<div>Surface auto (becomes 2)</div>
<Flex {...args} surface="auto" direction="column">
<div>Surface auto (becomes 3)</div>
<Flex {...args} surface="auto" direction="column">
<div>Surface auto (stays 3 - capped)</div>
</Flex>
</Flex>
<Flex {...args} bg="neutral-auto" direction="column">
<div>Neutral 1 (auto, no parent)</div>
<Flex {...args} bg="neutral-auto" direction="column">
<div>Neutral 2 (auto-incremented)</div>
<Flex {...args} bg="neutral-auto" direction="column">
<div>Neutral 3 (auto-incremented, capped)</div>
</Flex>
</Flex>
</Flex>
+6 -10
View File
@@ -20,24 +20,20 @@ import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { FlexDefinition } from './definition';
import styles from './Flex.module.css';
import { SurfaceProvider, useSurface } from '../../hooks/useSurface';
import { BgProvider, useBgProvider } from '../../hooks/useBg';
/** @public */
export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
// Resolve the surface this Flex creates for its children
// Using 'surface' parameter = container behavior (auto increments)
const { surface: resolvedSurface } = useSurface({
surface: props.surface,
});
const { bg: resolvedBg } = useBgProvider(props.bg);
const { classNames, dataAttributes, utilityClasses, style, cleanedProps } =
useStyles(FlexDefinition, {
gap: '4',
...props,
surface: resolvedSurface, // Use resolved surface for data attribute
bg: resolvedBg, // Use resolved bg for data attribute
});
const { className, surface, ...rest } = cleanedProps;
const { className, bg, ...rest } = cleanedProps;
const content = (
<div
@@ -54,8 +50,8 @@ export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
/>
);
return resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>{content}</SurfaceProvider>
return resolvedBg ? (
<BgProvider bg={resolvedBg}>{content}</BgProvider>
) : (
content
);
@@ -45,6 +45,13 @@ export const FlexDefinition = {
'direction',
],
dataAttributes: {
surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const,
bg: [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
] as const,
},
} as const satisfies ComponentDefinition;
+2 -2
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Responsive, Space, SpaceProps, Surface } from '../../types';
import type { Responsive, Space, SpaceProps, ProviderBg } from '../../types';
/** @public */
export interface FlexProps extends SpaceProps {
@@ -25,5 +25,5 @@ export interface FlexProps extends SpaceProps {
direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>;
className?: string;
style?: React.CSSProperties;
surface?: Responsive<Surface>;
bg?: Responsive<ProviderBg>;
}
+12 -17
View File
@@ -21,38 +21,33 @@
display: grid;
}
.bui-Grid[data-surface='0'],
.bui-GridItem[data-surface='0'] {
background-color: var(--bui-bg-neutral-0);
}
.bui-Grid[data-surface='1'],
.bui-GridItem[data-surface='1'] {
.bui-Grid[data-bg='neutral-1'],
.bui-GridItem[data-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-1);
}
.bui-Grid[data-surface='2'],
.bui-GridItem[data-surface='2'] {
.bui-Grid[data-bg='neutral-2'],
.bui-GridItem[data-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-2);
}
.bui-Grid[data-surface='3'],
.bui-GridItem[data-surface='3'] {
.bui-Grid[data-bg='neutral-3'],
.bui-GridItem[data-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-3);
}
.bui-Grid[data-surface='danger'],
.bui-GridItem[data-surface='danger'] {
.bui-Grid[data-bg='danger'],
.bui-GridItem[data-bg='danger'] {
background-color: var(--bui-bg-danger);
}
.bui-Grid[data-surface='warning'],
.bui-GridItem[data-surface='warning'] {
.bui-Grid[data-bg='warning'],
.bui-GridItem[data-bg='warning'] {
background-color: var(--bui-bg-warning);
}
.bui-Grid[data-surface='success'],
.bui-GridItem[data-surface='success'] {
.bui-Grid[data-bg='success'],
.bui-GridItem[data-bg='success'] {
background-color: var(--bui-bg-success);
}
}
@@ -106,78 +106,70 @@ export const RowAndColumns = meta.story({
),
});
export const Surfaces = meta.story({
export const Backgrounds = meta.story({
args: { px: '6', py: '4' },
render: args => (
<Flex direction="column">
<Flex style={{ flexWrap: 'wrap' }}>
<Grid.Root {...args} surface="0">
Surface 0
<Grid.Root {...args} bg="neutral-1">
Neutral 1
</Grid.Root>
<Grid.Root {...args} surface="1">
Surface 1
<Grid.Root {...args} bg="neutral-2">
Neutral 2
</Grid.Root>
<Grid.Root {...args} surface="2">
Surface 2
<Grid.Root {...args} bg="neutral-3">
Neutral 3
</Grid.Root>
<Grid.Root {...args} surface="3">
Surface 3
<Grid.Root {...args} bg={{ initial: 'neutral-1', sm: 'neutral-2' }}>
Responsive Bg
</Grid.Root>
<Grid.Root {...args} surface={{ initial: '0', sm: '1' }}>
Responsive Surface
<Grid.Root {...args} bg="danger">
Danger
</Grid.Root>
<Grid.Root {...args} surface="danger">
Surface Danger
<Grid.Root {...args} bg="warning">
Warning
</Grid.Root>
<Grid.Root {...args} surface="warning">
Surface Warning
</Grid.Root>
<Grid.Root {...args} surface="success">
Surface Success
<Grid.Root {...args} bg="success">
Success
</Grid.Root>
</Flex>
<Flex style={{ flexWrap: 'wrap' }}>
<Grid.Root {...args}>
<Grid.Item surface="0" style={{ padding: '4px' }}>
Surface 0
<Grid.Item bg="neutral-1" style={{ padding: '4px' }}>
Neutral 1
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="1" style={{ padding: '4px' }}>
Surface 1
<Grid.Item bg="neutral-2" style={{ padding: '4px' }}>
Neutral 2
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="2" style={{ padding: '4px' }}>
Surface 2
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="3" style={{ padding: '4px' }}>
Surface 3
<Grid.Item bg="neutral-3" style={{ padding: '4px' }}>
Neutral 3
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item
surface={{ initial: '0', sm: '1' }}
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
style={{ padding: '4px' }}
>
Responsive Surface
Responsive Bg
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="danger" style={{ padding: '4px' }}>
Surface Danger
<Grid.Item bg="danger" style={{ padding: '4px' }}>
Danger
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="warning" style={{ padding: '4px' }}>
Surface Warning
<Grid.Item bg="warning" style={{ padding: '4px' }}>
Warning
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="success" style={{ padding: '4px' }}>
Surface Success
<Grid.Item bg="success" style={{ padding: '4px' }}>
Success
</Grid.Item>
</Grid.Root>
</Flex>
@@ -185,23 +177,21 @@ export const Surfaces = meta.story({
),
});
export const SurfacesAutoIncrement = meta.story({
export const BgNeutralAuto = meta.story({
args: { px: '6', py: '4', columns: '2', gap: '4' },
render: args => (
<Flex direction="column">
<div style={{ maxWidth: '600px', marginBottom: '16px' }}>
Using surface="auto" automatically increments from the parent surface.
Each Grid.Item with auto will be one level above its Grid.Root parent.
Grid is a layout primitive and is transparent to the bg system by
default. Only an explicit bg prop establishes a new bg level. Nested
grids without a bg prop inherit the parent context unchanged.
</div>
<Grid.Root {...args} surface="0">
<Grid.Item>Surface 0 (Grid.Root)</Grid.Item>
<Grid.Item surface="auto">Surface auto (becomes 1)</Grid.Item>
<Grid.Root {...args} bg="neutral-1">
<Grid.Item>Neutral 1 (Grid.Root)</Grid.Item>
<Grid.Item>
<Grid.Root {...args} surface="auto">
<Grid.Item>Nested: Surface auto (becomes 1)</Grid.Item>
<Grid.Item surface="auto">
Nested: Surface auto (becomes 2)
</Grid.Item>
<Grid.Root {...args} bg="neutral-2">
<Grid.Item>Nested: neutral-2 (explicit)</Grid.Item>
<Grid.Item>Nested: neutral-2 (explicit)</Grid.Item>
</Grid.Root>
</Grid.Item>
</Grid.Root>
+11 -19
View File
@@ -20,24 +20,20 @@ import type { GridItemProps, GridProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { GridDefinition, GridItemDefinition } from './definition';
import styles from './Grid.module.css';
import { SurfaceProvider, useSurface } from '../../hooks/useSurface';
import { BgProvider, useBgProvider } from '../../hooks/useBg';
const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
// Resolve the surface this Grid creates for its children
// Using 'surface' parameter = container behavior (auto increments)
const { surface: resolvedSurface } = useSurface({
surface: props.surface,
});
const { bg: resolvedBg } = useBgProvider(props.bg);
const { classNames, dataAttributes, utilityClasses, style, cleanedProps } =
useStyles(GridDefinition, {
columns: 'auto',
gap: '4',
...props,
surface: resolvedSurface, // Use resolved surface for data attribute
bg: resolvedBg, // Use resolved bg for data attribute
});
const { className, surface, ...rest } = cleanedProps;
const { className, bg, ...rest } = cleanedProps;
const content = (
<div
@@ -54,27 +50,23 @@ const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
/>
);
return resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>{content}</SurfaceProvider>
return resolvedBg ? (
<BgProvider bg={resolvedBg}>{content}</BgProvider>
) : (
content
);
});
const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
// Resolve the surface this GridItem creates for its children
// Using 'surface' parameter = container behavior (auto increments)
const { surface: resolvedSurface } = useSurface({
surface: props.surface,
});
const { bg: resolvedBg } = useBgProvider(props.bg);
const { classNames, dataAttributes, utilityClasses, style, cleanedProps } =
useStyles(GridItemDefinition, {
...props,
surface: resolvedSurface, // Use resolved surface for data attribute
bg: resolvedBg, // Use resolved bg for data attribute
});
const { className, surface, ...rest } = cleanedProps;
const { className, bg, ...rest } = cleanedProps;
const content = (
<div
@@ -91,8 +83,8 @@ const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
/>
);
return resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>{content}</SurfaceProvider>
return resolvedBg ? (
<BgProvider bg={resolvedBg}>{content}</BgProvider>
) : (
content
);
+16 -2
View File
@@ -43,7 +43,14 @@ export const GridDefinition = {
'py',
],
dataAttributes: {
surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const,
bg: [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
] as const,
},
} as const satisfies ComponentDefinition;
@@ -57,6 +64,13 @@ export const GridItemDefinition = {
},
utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan'],
dataAttributes: {
surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const,
bg: [
'neutral-1',
'neutral-2',
'neutral-3',
'danger',
'warning',
'success',
] as const,
},
} as const satisfies ComponentDefinition;
+3 -3
View File
@@ -19,7 +19,7 @@ import type {
SpaceProps,
Responsive,
Columns,
Surface,
ProviderBg,
} from '../../types';
/** @public */
@@ -29,7 +29,7 @@ export interface GridProps extends SpaceProps {
columns?: Responsive<Columns>;
gap?: Responsive<Space>;
style?: React.CSSProperties;
surface?: Responsive<Surface>;
bg?: Responsive<ProviderBg>;
}
/** @public */
@@ -41,5 +41,5 @@ export interface GridItemProps {
colStart?: Responsive<Columns>;
rowSpan?: Responsive<Columns>;
style?: React.CSSProperties;
surface?: Responsive<Surface>;
bg?: Responsive<ProviderBg>;
}
@@ -44,7 +44,7 @@ export const Default = meta.story({
},
});
export const Surfaces = meta.story({
export const Backgrounds = meta.story({
args: {
children: 'Toggle',
},
@@ -64,26 +64,20 @@ export const Surfaces = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4">
<Text>On Neutral 1</Text>
<Flex align="center" bg="neutral-1" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4">
<Text>On Neutral 2</Text>
<Flex align="center" bg="neutral-2" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
@@ -21,7 +21,6 @@ import type { ToggleButtonProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { ToggleButtonDefinition } from './definition';
import styles from './ToggleButton.module.css';
import { useSurface } from '../../hooks/useSurface';
/** @public */
export const ToggleButton = forwardRef(
@@ -34,17 +33,13 @@ export const ToggleButton = forwardRef(
},
);
const { children, className, iconStart, iconEnd, onSurface, ...rest } =
cleanedProps;
const { surface } = useSurface({ onSurface });
const { children, className, iconStart, iconEnd, ...rest } = cleanedProps;
return (
<AriaToggleButton
className={clsx(classNames.root, styles[classNames.root], className)}
ref={ref}
{...dataAttributes}
{...(typeof surface === 'string' ? { 'data-on-surface': surface } : {})}
{...rest}
>
{renderProps => {
@@ -17,7 +17,6 @@
import type { Breakpoint } from '../..';
import type { ReactElement } from 'react';
import type { ToggleButtonProps as AriaToggleButtonProps } from 'react-aria-components';
import type { Responsive, Surface } from '../../types';
/**
* Properties for {@link ToggleButton}
@@ -28,6 +27,4 @@ export interface ToggleButtonProps extends AriaToggleButtonProps {
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
/** Surface the toggle button is placed on. Defaults to context surface if available */
onSurface?: Responsive<Surface>;
}
@@ -71,7 +71,7 @@ export const MultipleSelection = meta.story({
),
});
export const Surfaces = meta.story({
export const Backgrounds = meta.story({
args: {
selectionMode: 'single',
defaultSelectedKeys: ['option1'],
@@ -99,8 +99,8 @@ export const Surfaces = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4" gap="4">
<Text>On Neutral 1</Text>
<Flex align="center" bg="neutral-1" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
@@ -112,8 +112,8 @@ export const Surfaces = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4" gap="4">
<Text>On Neutral 2</Text>
<Flex align="center" bg="neutral-2" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
@@ -125,21 +125,8 @@ export const Surfaces = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4" gap="4">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
+124
View File
@@ -0,0 +1,124 @@
/*
* 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.
*/
import { useContext, ReactNode } from 'react';
import {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
import { ContainerBg, ProviderBg, Responsive } from '../types';
import { useBreakpoint } from './useBreakpoint';
import { resolveResponsiveValue } from './useDefinition/helpers';
/** @public */
export interface BgContextValue {
bg: ContainerBg | undefined;
}
/** @public */
export interface BgProviderProps {
bg: ContainerBg;
children: ReactNode;
}
const BgContext = createVersionedContext<{
1: BgContextValue;
}>('bg-context');
/**
* Increments a neutral bg level by one, capping at 'neutral-3'.
* Intent backgrounds (danger, warning, success) pass through unchanged.
*
* The 'neutral-4' level is reserved for consumer component CSS and is
* never set on providers.
*
* @internal
*/
function incrementNeutralBg(bg: ContainerBg | undefined): ContainerBg {
if (!bg) return 'neutral-1';
if (bg === 'neutral-1') return 'neutral-2';
if (bg === 'neutral-2') return 'neutral-3';
if (bg === 'neutral-3') return 'neutral-3'; // capped at neutral-3
// Intent values pass through unchanged
return bg;
}
/**
* Provider component that establishes the bg context for child components.
*
* @public
*/
export const BgProvider = ({ bg, children }: BgProviderProps) => {
return (
<BgContext.Provider value={createVersionedValueMap({ 1: { bg } })}>
{children}
</BgContext.Provider>
);
};
/**
* Hook for consumer components (e.g. Button) to read the parent bg context.
*
* Returns the parent container's bg unchanged. The consumer component's CSS
* handles the visual step-up (e.g. on a neutral-1 surface, the consumer
* uses neutral-2 tokens via `data-on-bg`).
*
* @public
*/
export function useBgConsumer(): BgContextValue {
const value = useContext(BgContext)?.atVersion(1);
return value ?? { bg: undefined };
}
/**
* Hook for provider components (e.g. Box, Card) to resolve and provide bg context.
*
* **Resolution rules:**
*
* - `bg` is `undefined` -- transparent, no context change, returns `{ bg: undefined }`.
* This is the default for Box, Flex, and Grid (they do **not** auto-increment).
* - `bg` is a `ContainerBg` value -- uses that value directly (e.g. `'neutral-1'`).
* - `bg` is `'neutral-auto'` -- increments the neutral level from the parent context,
* capping at `neutral-3`. Only components that explicitly pass `'neutral-auto'`
* (e.g. Card) will auto-increment; it is never implicit.
*
* **Capping:**
*
* Provider components cap at `neutral-3`. The `neutral-4` level is **not** a valid
* prop value -- it exists only in consumer component CSS (e.g. a Button on a
* `neutral-3` surface renders with `neutral-4` tokens via `data-on-bg`).
*
* The caller is responsible for wrapping children with `BgProvider` when the
* resolved bg is defined.
*
* @public
*/
export function useBgProvider(bg?: Responsive<ProviderBg>): BgContextValue {
const { breakpoint } = useBreakpoint();
const context = useBgConsumer();
if (bg === undefined) {
return { bg: undefined };
}
const resolved = resolveResponsiveValue(bg, breakpoint);
if (resolved === 'neutral-auto') {
return { bg: incrementNeutralBg(context.bg) };
}
return { bg: resolved };
}
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import type { ComponentConfig, SurfacePropsConstraint } from './types';
import type { ComponentConfig, BgPropsConstraint } from './types';
export function defineComponent<P extends Record<string, any>>() {
return <
const S extends Record<string, string>,
const C extends ComponentConfig<P, S>,
>(
config: C & SurfacePropsConstraint<P, C['surface']>,
config: C & BgPropsConstraint<P, C['bg']>,
): C => config;
}
+19 -21
View File
@@ -36,25 +36,25 @@ export interface ComponentConfig<
propDefs: { [K in keyof P]: PropDefConfig<P[K]> };
// readonly for compatibility with const inference from factory
utilityProps?: readonly UtilityPropKey[];
surface?: 'container' | 'leaf';
/**
* How this component participates in the bg system.
*
* - `'provider'` calls `useBgProvider`, sets `data-bg`, wraps children in `BgProvider`
* - `'consumer'` calls `useBgConsumer`, sets `data-on-bg`
*/
bg?: 'provider' | 'consumer';
}
/**
* Type constraint that validates surface props are present in the props type.
* - If surface is 'leaf', P must include 'onSurface'
* - If surface is 'container', P must include 'surface'
* Type constraint that validates bg props are present in the props type.
* - Provider components must include 'bg' in their props
* - Consumer components don't need a bg prop
*/
export type SurfacePropsConstraint<P, Surface> = Surface extends 'leaf'
? 'onSurface' extends keyof P
export type BgPropsConstraint<P, Bg> = Bg extends 'provider'
? 'bg' extends keyof P
? {}
: {
__error: 'Leaf components must include onSurface in props type. Extend LeafProps.';
}
: Surface extends 'container'
? 'surface' extends keyof P
? {}
: {
__error: 'Container components must include surface in props type. Extend ContainerProps.';
__error: 'Bg provider components must include bg in props type.';
}
: {};
@@ -81,12 +81,10 @@ type ResolvedOwnProps<
[K in keyof PropDefs & keyof P]: ResolvePropType<P[K], PropDefs[K]>;
};
type ChildrenProps<Surface extends 'container' | 'leaf' | undefined> =
Surface extends 'container'
? { surfaceChildren: ReactNode; children?: never }
: Surface extends 'leaf'
? { children: ReactNode; surfaceChildren?: never }
: { children: ReactNode };
type ChildrenProps<Bg extends 'provider' | 'consumer' | undefined> =
Bg extends 'provider'
? { childrenWithBgProvider: ReactNode; children?: never }
: { children: ReactNode; childrenWithBgProvider?: never };
type DataAttributeKeys<PropDefs> = {
[K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true }
@@ -98,7 +96,7 @@ type DataAttributes<PropDefs> = {
[K in DataAttributeKeys<PropDefs> as `data-${Lowercase<
string & K
>}`]?: string;
} & { 'data-on-surface'?: string };
} & { 'data-bg'?: string; 'data-on-bg'?: string };
export type UtilityKeys<D extends ComponentConfig<any, any>> =
D['utilityProps'] extends ReadonlyArray<infer K extends string> ? K : never;
@@ -127,7 +125,7 @@ export interface UseDefinitionResult<
ownProps: {
classes: Record<keyof D['classNames'], string>;
} & ResolvedOwnProps<P, D['propDefs']> &
ChildrenProps<D['surface']>;
ChildrenProps<D['bg']>;
// Rest props excludes both propDefs keys AND utility prop keys
restProps: keyof Omit<P, keyof D['propDefs'] | UtilityKeys<D>> extends never
@@ -17,7 +17,7 @@
import { ReactNode } from 'react';
import clsx from 'clsx';
import { useBreakpoint } from '../useBreakpoint';
import { useSurface, SurfaceProvider, UseSurfaceOptions } from '../useSurface';
import { useBgProvider, useBgConsumer, BgProvider } from '../useBg';
import { resolveResponsiveValue, processUtilityProps } from './helpers';
import type {
ComponentConfig,
@@ -36,14 +36,13 @@ export function useDefinition<
): UseDefinitionResult<D, P> {
const { breakpoint } = useBreakpoint();
const surfaceOptions: UseSurfaceOptions | undefined =
definition.surface === 'container'
? { surface: props.surface }
: definition.surface === 'leaf'
? { onSurface: props.onSurface }
: undefined;
// Provider: resolve bg and provide context for children
const providerBg = useBgProvider(
definition.bg === 'provider' ? props.bg : undefined,
);
const { surface: resolvedSurface } = useSurface(surfaceOptions);
// Consumer: read parent context bg
const consumerBg = useBgConsumer();
const ownPropKeys = new Set(Object.keys(definition.propDefs));
const utilityPropKeys = new Set(definition.utilityProps ?? []);
@@ -70,6 +69,9 @@ export function useDefinition<
if (finalValue !== undefined) {
ownPropsResolved[key] = finalValue;
// Skip data-bg for bg prop when the provider path handles it
if (key === 'bg' && definition.bg === 'provider') continue;
if ((config as any).dataAttribute) {
// eslint-disable-next-line no-restricted-syntax
dataAttributes[`data-${key.toLowerCase()}`] = String(finalValue);
@@ -77,16 +79,14 @@ export function useDefinition<
}
}
// Add data-on-surface for leaf components
if (definition.surface === 'leaf' && resolvedSurface !== undefined) {
// Handle responsive surface values - for data attributes, use the resolved string
const surfaceValue =
typeof resolvedSurface === 'object'
? resolveResponsiveValue(resolvedSurface as any, breakpoint)
: resolvedSurface;
if (surfaceValue !== undefined) {
dataAttributes['data-on-surface'] = String(surfaceValue);
}
// Provider: set data-bg from the resolved provider bg
if (definition.bg === 'provider' && providerBg.bg !== undefined) {
dataAttributes['data-bg'] = String(providerBg.bg);
}
// Consumer: set data-on-bg from the parent context
if (definition.bg === 'consumer' && consumerBg.bg !== undefined) {
dataAttributes['data-on-bg'] = String(consumerBg.bg);
}
const { utilityClasses, utilityStyle } = processUtilityProps<UtilityKeys<D>>(
@@ -109,13 +109,11 @@ export function useDefinition<
}
let children: ReactNode | undefined;
let surfaceChildren: ReactNode | undefined;
let childrenWithBgProvider: ReactNode | undefined;
if (definition.surface === 'container') {
surfaceChildren = resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>
{props.children}
</SurfaceProvider>
if (definition.bg === 'provider') {
childrenWithBgProvider = providerBg.bg ? (
<BgProvider bg={providerBg.bg}>{props.children}</BgProvider>
) : (
props.children
);
@@ -127,8 +125,8 @@ export function useDefinition<
ownProps: {
classes,
...ownPropsResolved,
...(definition.surface === 'container'
? { surfaceChildren }
...(definition.bg === 'provider'
? { childrenWithBgProvider }
: { children }),
},
restProps,
-204
View File
@@ -1,204 +0,0 @@
/*
* 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.
*/
import { useContext, ReactNode } from 'react';
import {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
import { Surface, Responsive } from '../types';
/** @public */
export interface SurfaceContextValue {
surface: Responsive<Surface> | undefined;
}
/** @public */
export interface SurfaceProviderProps {
surface: Responsive<Surface>;
children: ReactNode;
}
/** @public */
export interface UseSurfaceOptions {
/**
* The surface value this component CREATES for its children (container behavior).
* When 'auto', increments from parent surface.
*
* Use this for components like Box, Flex, Grid that establish surface context.
*/
surface?: Responsive<Surface>;
/**
* The surface value this component is ON for styling (leaf behavior).
* When 'auto', inherits from current surface.
*
* Use this for leaf components like Button that consume surface for styling.
*/
onSurface?: Responsive<Surface>;
}
const SurfaceContext = createVersionedContext<{
1: SurfaceContextValue;
}>('surface-context');
/**
* Increments a surface level by one, capping at '3'.
* Intent surfaces (danger, warning, success) remain unchanged.
*
* @internal
*/
function incrementSurface(surface: Surface | undefined): Surface {
if (!surface) return '0'; // no context = root level
if (surface === '0') return '1';
if (surface === '1') return '2';
if (surface === '2' || surface === '3') return '3'; // cap at max
// Intent surfaces remain unchanged
if (surface === 'danger') return 'danger';
if (surface === 'warning') return 'warning';
if (surface === 'success') return 'success';
// 'auto' should not appear here, but handle it defensively
if (surface === 'auto') return '1';
return surface;
}
/**
* Resolves a surface value for containers (SurfaceProvider).
* When 'auto' is used, increments from the parent surface.
* For responsive surfaces (objects), returns them as-is without resolution.
*
* @param contextSurface - The surface from context
* @param requestedSurface - The requested surface value (may be 'auto')
* @returns The resolved surface value
* @internal
*/
export function resolveSurfaceForProvider(
contextSurface: Responsive<Surface> | undefined,
requestedSurface: Responsive<Surface> | undefined,
): Responsive<Surface> | undefined {
if (!requestedSurface) {
return contextSurface;
}
// If requestedSurface is a responsive object (breakpoint-based), return as-is
if (typeof requestedSurface === 'object') {
return requestedSurface;
}
// If contextSurface is a responsive object, we can't auto-increment from it
// Return the requested surface as-is or default to '0' for auto
if (typeof contextSurface === 'object') {
if (requestedSurface === 'auto') {
return '0'; // fallback to root when context is responsive
}
return requestedSurface;
}
// For containers, 'auto' means increment to create a new elevated context
if (requestedSurface === 'auto') {
return incrementSurface(contextSurface);
}
return requestedSurface;
}
/**
* Resolves a surface value for leaf components (useSurface hook).
* When 'auto' is used, inherits the current surface (doesn't increment).
* For responsive surfaces (objects), returns them as-is without resolution.
*
* @param contextSurface - The surface from context
* @param requestedSurface - The requested surface value (may be 'auto')
* @returns The resolved surface value
* @internal
*/
function resolveSurfaceForConsumer(
contextSurface: Responsive<Surface> | undefined,
requestedSurface: Responsive<Surface> | undefined,
): Responsive<Surface> | undefined {
if (!requestedSurface) {
return contextSurface;
}
// If requestedSurface is a responsive object (breakpoint-based), return as-is
if (typeof requestedSurface === 'object') {
return requestedSurface;
}
// For leaf components, 'auto' means inherit the current surface
if (requestedSurface === 'auto') {
// If context is responsive, fallback to '0'
if (typeof contextSurface === 'object') {
return '0';
}
return contextSurface;
}
return requestedSurface;
}
/**
* Provider component that establishes the surface context for child components.
* This allows components to adapt their styling based on their background surface.
*
* Note: The surface value should already be resolved before passing to this provider.
* Container components should use useSurface with the surface parameter.
*
* @internal
*/
export const SurfaceProvider = ({
surface,
children,
}: SurfaceProviderProps) => {
return (
<SurfaceContext.Provider
value={createVersionedValueMap({ 1: { surface } })}
>
{children}
</SurfaceContext.Provider>
);
};
/**
* Hook to access the current surface context.
* Returns the current surface level, or undefined if no provider is present.
*
* The parameter name determines the behavior:
* - `surface`: Container behavior - 'auto' increments from parent
* - `onSurface`: Leaf behavior - 'auto' inherits from parent
*
* @param options - Optional configuration for surface resolution
* @internal
*/
export const useSurface = (
options?: UseSurfaceOptions,
): SurfaceContextValue => {
const value = useContext(SurfaceContext)?.atVersion(1);
const context = value ?? { surface: undefined };
// Infer behavior from which parameter is provided
// 'surface' = provider behavior (increment)
// 'onSurface' = consumer behavior (inherit)
const isProvider = options?.surface !== undefined;
const requestedSurface = options?.surface ?? options?.onSurface;
const resolvedSurface = isProvider
? resolveSurfaceForProvider(context.surface, requestedSurface)
: resolveSurfaceForConsumer(context.surface, requestedSurface);
return {
surface: resolvedSurface,
};
};
+2
View File
@@ -64,3 +64,5 @@ export * from './types';
// Hooks
export { useBreakpoint } from './hooks/useBreakpoint';
export { useBgProvider, useBgConsumer, BgProvider } from './hooks/useBg';
export type { BgContextValue, BgProviderProps } from './hooks/useBg';
+20 -19
View File
@@ -182,29 +182,30 @@ export interface ComponentDefinition {
}
/**
* Surface type
* Background type for the neutral bg system.
*
* Supports absolute levels ('0'-'3'), intent surfaces ('danger', 'warning', 'success'),
* and 'auto' which increments from the parent surface context.
* Supports neutral levels ('neutral-1' through 'neutral-3') and
* intent backgrounds ('danger', 'warning', 'success').
*
* The 'neutral-4' level is not exposed as a prop value -- it is reserved
* for leaf component CSS (e.g. Button on a 'neutral-3' surface).
*
* @public
*/
export type Surface =
| '0'
| '1'
| '2'
| '3'
export type ContainerBg =
| 'neutral-1'
| 'neutral-2'
| 'neutral-3'
| 'danger'
| 'warning'
| 'success'
| 'auto';
| 'success';
/** @public */
export interface LeafSurfaceProps {
onSurface?: Responsive<Surface>;
}
/** @public */
export interface ContainerSurfaceProps {
surface?: Responsive<Surface>;
}
/**
* Background values accepted by provider components.
*
* Includes all `ContainerBg` values plus `'neutral-auto'` which
* automatically increments the neutral level from the parent context.
*
* @public
*/
export type ProviderBg = ContainerBg | 'neutral-auto';