Migrate useSurface to useBg

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-06 18:46:26 +00:00
parent 0bcd685427
commit 09db540228
46 changed files with 809 additions and 854 deletions
+160 -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,33 @@ export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
src: string;
}
// @public
export type Bg =
| 'neutral-1'
| 'neutral-2'
| 'neutral-3'
| 'neutral-4'
| 'danger'
| 'warning'
| 'success';
// @public (undocumented)
export interface BgContextValue {
// (undocumented)
bg: Responsive<Bg> | undefined;
}
// @public
export const BgProvider: ({ bg, children }: BgProviderProps) => JSX_2.Element;
// @public (undocumented)
export interface BgProviderProps {
// (undocumented)
bg: Responsive<Bg>;
// (undocumented)
children: ReactNode;
}
// @public (undocumented)
export type Border = 'none' | 'base' | 'error' | 'warning' | 'selected';
@@ -234,12 +259,12 @@ export const BoxDefinition: {
readonly classNames: {
readonly root: 'bui-Box';
};
readonly surface: 'container';
readonly bg: 'container';
readonly propDefs: {
readonly as: {
readonly default: 'div';
};
readonly surface: {
readonly bg: {
readonly dataAttribute: true;
};
readonly children: {};
@@ -275,7 +300,7 @@ export const BoxDefinition: {
// @public (undocumented)
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
surface?: Responsive<Surface>;
bg?: Responsive<Bg>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -320,7 +345,7 @@ export const ButtonDefinition: {
readonly content: 'bui-ButtonContent';
readonly spinner: 'bui-ButtonSpinner';
};
readonly surface: 'leaf';
readonly bg: 'leaf';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -338,7 +363,6 @@ export const ButtonDefinition: {
};
readonly iconStart: {};
readonly iconEnd: {};
readonly onSurface: {};
readonly children: {};
readonly className: {};
readonly style: {};
@@ -360,7 +384,7 @@ export const ButtonIconDefinition: {
readonly content: 'bui-ButtonIconContent';
readonly spinner: 'bui-ButtonIconSpinner';
};
readonly surface: 'leaf';
readonly bg: 'leaf';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -374,14 +398,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 +432,7 @@ export const ButtonLinkDefinition: {
readonly root: 'bui-ButtonLink';
readonly content: 'bui-ButtonLinkContent';
};
readonly surface: 'leaf';
readonly bg: 'leaf';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
@@ -421,7 +444,6 @@ export const ButtonLinkDefinition: {
};
readonly iconStart: {};
readonly iconEnd: {};
readonly onSurface: {};
readonly children: {};
readonly className: {};
readonly style: {};
@@ -429,7 +451,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 +467,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 +495,45 @@ 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 bg: 'container';
readonly propDefs: {
readonly bg: {
readonly dataAttribute: true;
};
readonly children: {};
readonly className: {};
};
};
@@ -494,10 +543,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 +573,41 @@ 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 = {
bg?: Responsive<Bg>;
children?: ReactNode;
className?: string;
};
// @public
export interface CardProps
extends CardOwnProps,
React.HTMLAttributes<HTMLDivElement> {}
// @public (undocumented)
export const Cell: {
@@ -684,12 +777,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 +948,11 @@ export const FlexDefinition: {
'direction',
];
readonly dataAttributes: {
readonly surface: readonly [
'0',
'1',
'2',
'3',
readonly bg: readonly [
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -881,6 +968,8 @@ export interface FlexProps extends SpaceProps {
// (undocumented)
align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>;
// (undocumented)
bg?: Responsive<Bg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
className?: string;
@@ -892,8 +981,6 @@ export interface FlexProps extends SpaceProps {
justify?: Responsive<'start' | 'center' | 'end' | 'between'>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
surface?: Responsive<Surface>;
}
// @public (undocumented)
@@ -931,11 +1018,11 @@ export const GridDefinition: {
'py',
];
readonly dataAttributes: {
readonly surface: readonly [
'0',
'1',
'2',
'3',
readonly bg: readonly [
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -950,11 +1037,11 @@ 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',
'neutral-4',
'danger',
'warning',
'success',
@@ -964,6 +1051,8 @@ export const GridItemDefinition: {
// @public (undocumented)
export interface GridItemProps {
// (undocumented)
bg?: Responsive<Bg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -978,12 +1067,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<Bg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -994,8 +1083,6 @@ export interface GridProps extends SpaceProps {
gap?: Responsive<Space>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
surface?: Responsive<Surface>;
}
// @public
@@ -1088,12 +1175,6 @@ export type JustifyContent =
| 'around'
| 'between';
// @public (undocumented)
export interface LeafSurfaceProps {
// (undocumented)
onSurface?: Responsive<Surface>;
}
// @public (undocumented)
export const Link: ForwardRefExoticComponent<
LinkProps & RefAttributes<HTMLAnchorElement>
@@ -1655,17 +1736,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>
@@ -2079,7 +2149,6 @@ export interface ToggleButtonProps extends ToggleButtonProps_2 {
iconEnd?: ReactElement;
// (undocumented)
iconStart?: ReactElement;
onSurface?: Responsive<Surface>;
// (undocumented)
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -2108,6 +2177,18 @@ export const TooltipTrigger: (
props: TooltipTriggerComponentProps,
) => JSX_2.Element;
// @public
export const useBg: (options?: UseBgOptions) => BgContextValue;
// @public (undocumented)
export type UseBgOptions =
| {
bg: Responsive<Bg> | undefined;
}
| {
leaf: true;
};
// @public (undocumented)
export const useBreakpoint: () => {
breakpoint: Breakpoint;
@@ -284,50 +284,46 @@ 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" />
<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 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 4</Text>
<Flex direction="column" gap="2" bg="neutral-4" p="4">
<Alert status="info" icon={true} title="Alert on neutral-4" />
<Alert status="success" icon={true} title="Alert on neutral-4" />
</Flex>
</Flex>
</Flex>
@@ -349,7 +345,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;
+10 -10
View File
@@ -23,31 +23,31 @@
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='neutral-4'] {
background-color: var(--bui-bg-neutral-4);
}
.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);
}
}
+37 -63
View File
@@ -346,90 +346,64 @@ 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>
<Flex direction="column" gap="6">
<Flex direction="column" gap="2">
<div>With bg="neutral-1" nested boxes and buttons auto-increment</div>
<Box {...args} bg="neutral-1">
<Button variant="secondary">Button (neutral-2)</Button>
<Box {...args} mt="4">
<Button variant="secondary">Button</Button>
</Box>
</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>
<Button variant="secondary">Button (neutral-3)</Button>
<Box {...args} mt="4">
<Button variant="secondary">Button (neutral-4)</Button>
</Box>
</Box>
</Box>
</Box>
</Flex>
<Flex direction="column" gap="2">
<div>Without bg no context, no auto-increment</div>
<Box {...args}>
<Button variant="secondary">Button (default)</Button>
<Box {...args} mt="4">
<Button variant="secondary">Button (default)</Button>
<Box {...args} mt="4">
<Button variant="secondary">Button (default)</Button>
</Box>
</Box>
</Box>
</Flex>
</Flex>
),
});
+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, bgChildren } = ownProps;
return createElement(
as,
@@ -36,7 +36,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
...dataAttributes,
...restProps,
},
surfaceChildren,
bgChildren,
);
});
+2 -2
View File
@@ -27,10 +27,10 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
classNames: {
root: 'bui-Box',
},
surface: 'container',
bg: 'container',
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, Bg, SpaceProps } from '../../types';
/** @public */
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
surface?: Responsive<Surface>;
bg?: Responsive<Bg>;
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-bg='neutral-2'] {
--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-bg='neutral-3'] {
--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-bg='neutral-4'] {
--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-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-bg='neutral-4'] {
--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,72 @@ 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 direction="column" gap="4">
<Text>Neutral 4</Text>
<Flex align="center" bg="neutral-4" 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 +232,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 +448,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: 'leaf',
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-bg='neutral-2'] {
--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-bg='neutral-3'] {
--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-bg='neutral-4'] {
--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-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-bg='neutral-4'] {
--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: 'leaf',
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-bg='neutral-2'] {
--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-bg='neutral-3'] {
--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-bg='neutral-4'] {
--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-bg='neutral-2'] {
--bg-hover: var(--bui-bg-neutral-2-hover);
--bg-active: var(--bui-bg-neutral-2-pressed);
}
&[data-on-surface='2'] {
&[data-bg='neutral-3'] {
--bg-hover: var(--bui-bg-neutral-3-hover);
--bg-active: var(--bui-bg-neutral-3-pressed);
}
&[data-on-surface='3'] {
&[data-bg='neutral-4'] {
--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: 'leaf',
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;
+11 -11
View File
@@ -30,31 +30,31 @@
width: 100%;
}
.bui-Card[data-surface='0'] {
background-color: var(--bui-bg-surface-0);
.bui-Card[data-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-1);
}
.bui-Card[data-surface='1'] {
background-color: var(--bui-bg-surface-1);
.bui-Card[data-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-2);
}
.bui-Card[data-surface='2'] {
background-color: var(--bui-bg-surface-2);
.bui-Card[data-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-3);
}
.bui-Card[data-surface='3'] {
background-color: var(--bui-bg-surface-3);
.bui-Card[data-bg='neutral-4'] {
background-color: var(--bui-bg-neutral-4);
}
.bui-Card[data-surface='danger'] {
.bui-Card[data-bg='danger'] {
background-color: var(--bui-bg-danger);
}
.bui-Card[data-surface='warning'] {
.bui-Card[data-bg='warning'] {
background-color: var(--bui-bg-warning);
}
.bui-Card[data-surface='success'] {
.bui-Card[data-bg='success'] {
background-color: var(--bui-bg-success);
}
@@ -128,72 +128,71 @@ export const WithListRow = meta.story({
),
});
export const Surfaces = meta.story({
export const Backgrounds = 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>
<CardBody>No bg prop</CardBody>
</Card>
<Card {...args} surface="0" style={{ width: '200px' }}>
<CardHeader>Surface 0</CardHeader>
<CardBody>Explicit surface 0</CardBody>
<Card {...args} bg="neutral-1" style={{ width: '200px' }}>
<CardHeader>Neutral 1</CardHeader>
<CardBody>Explicit neutral-1</CardBody>
</Card>
<Card {...args} surface="1" style={{ width: '200px' }}>
<CardHeader>Surface 1</CardHeader>
<CardBody>Explicit surface 1</CardBody>
<Card {...args} bg="neutral-2" style={{ width: '200px' }}>
<CardHeader>Neutral 2</CardHeader>
<CardBody>Explicit neutral-2</CardBody>
</Card>
<Card {...args} surface="2" style={{ width: '200px' }}>
<CardHeader>Surface 2</CardHeader>
<CardBody>Explicit surface 2</CardBody>
<Card {...args} bg="neutral-3" style={{ width: '200px' }}>
<CardHeader>Neutral 3</CardHeader>
<CardBody>Explicit neutral-3</CardBody>
</Card>
<Card {...args} surface="3" style={{ width: '200px' }}>
<CardHeader>Surface 3</CardHeader>
<CardBody>Explicit surface 3</CardBody>
<Card {...args} bg="neutral-4" style={{ width: '200px' }}>
<CardHeader>Neutral 4</CardHeader>
<CardBody>Explicit neutral-4</CardBody>
</Card>
<Card
{...args}
surface={{ initial: '0', sm: '1' }}
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
style={{ width: '200px' }}
>
<CardHeader>Responsive</CardHeader>
<CardBody>Surface 0 1</CardBody>
<CardBody>Neutral 1 2</CardBody>
</Card>
<Card {...args} surface="danger" style={{ width: '200px' }}>
<Card {...args} bg="danger" style={{ width: '200px' }}>
<CardHeader>Danger</CardHeader>
<CardBody>Surface danger</CardBody>
<CardBody>Bg danger</CardBody>
</Card>
<Card {...args} surface="warning" style={{ width: '200px' }}>
<Card {...args} bg="warning" style={{ width: '200px' }}>
<CardHeader>Warning</CardHeader>
<CardBody>Surface warning</CardBody>
<CardBody>Bg warning</CardBody>
</Card>
<Card {...args} surface="success" style={{ width: '200px' }}>
<Card {...args} bg="success" style={{ width: '200px' }}>
<CardHeader>Success</CardHeader>
<CardBody>Surface success</CardBody>
<CardBody>Bg success</CardBody>
</Card>
</Flex>
),
});
export const SurfacesNested = meta.story({
export const BgNested = 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.
In this test, we are nesting cards to ensure that the correct background
is applied to each element. Buttons automatically inherit the bg context
and increment their neutral level.
</Box>
<Card {...args} surface="1" style={{ width: '500px' }}>
<CardHeader>Surface 1</CardHeader>
<Card {...args} bg="neutral-1" style={{ width: '500px' }}>
<CardHeader>Neutral 1</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
<Card {...args} surface="2" style={{ marginTop: '16px' }}>
<CardHeader>Surface 2</CardHeader>
<Card {...args} bg="neutral-2" style={{ marginTop: '16px' }}>
<CardHeader>Neutral 2</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
<Card {...args} style={{ marginTop: '16px' }}>
<CardHeader>Inherited</CardHeader>
<CardHeader>Auto-incremented</CardHeader>
<CardBody>
<Button variant="secondary">Button</Button>
</CardBody>
@@ -206,31 +205,31 @@ export const SurfacesNested = meta.story({
),
});
export const SurfacesAutoIncrement = meta.story({
export const BgAutoIncrement = meta.story({
render: args => (
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
<Box surface="0" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} surface="auto" style={{ width: '200px' }}>
<CardHeader>On surface 0</CardHeader>
<CardBody>Card auto 1</CardBody>
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-1</CardHeader>
<CardBody>Card auto neutral-2</CardBody>
</Card>
</Box>
<Box surface="1" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} surface="auto" style={{ width: '200px' }}>
<CardHeader>On surface 1</CardHeader>
<CardBody>Card auto 2</CardBody>
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Card auto neutral-3</CardBody>
</Card>
</Box>
<Box surface="2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} surface="auto" style={{ width: '200px' }}>
<CardHeader>On surface 2</CardHeader>
<CardBody>Card auto 3</CardBody>
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Card auto neutral-4</CardBody>
</Card>
</Box>
<Box surface="3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} surface="auto" style={{ width: '200px' }}>
<CardHeader>On surface 3</CardHeader>
<CardBody>Card auto 3 (capped)</CardBody>
<Box bg="neutral-4" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-4</CardHeader>
<CardBody>Card auto neutral-4 (capped)</CardBody>
</Card>
</Box>
</Flex>
+2 -2
View File
@@ -39,11 +39,11 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
CardDefinition,
props,
);
const { classes, surfaceChildren } = ownProps;
const { classes, bgChildren } = ownProps;
return (
<div ref={ref} className={classes.root} {...dataAttributes} {...restProps}>
{surfaceChildren}
{bgChildren}
</div>
);
});
@@ -32,9 +32,9 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
classNames: {
root: 'bui-Card',
},
surface: 'container',
bg: 'container',
propDefs: {
surface: { dataAttribute: true, default: '1' },
bg: { dataAttribute: true },
children: {},
className: {},
},
+2 -2
View File
@@ -15,11 +15,11 @@
*/
import type { ReactNode } from 'react';
import type { Responsive, Surface } from '../../types';
import type { Responsive, Bg } from '../../types';
/** @public */
export type CardOwnProps = {
surface?: Responsive<Surface>;
bg?: Responsive<Bg>;
children?: ReactNode;
className?: string;
};
+10 -10
View File
@@ -24,31 +24,31 @@
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='neutral-4'] {
background-color: var(--bui-bg-neutral-4);
}
.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,52 +252,52 @@ 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="neutral-4" {...args}>
Neutral 4
</Flex>
<Flex surface={{ initial: '0', sm: '1' }} {...args}>
Responsive Surface
<Flex bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Bg
</Flex>
<Flex surface="danger" {...args}>
Surface Danger
<Flex bg="danger" {...args}>
Danger
</Flex>
<Flex surface="warning" {...args}>
Surface Warning
<Flex bg="warning" {...args}>
Warning
</Flex>
<Flex surface="success" {...args}>
Surface Success
<Flex bg="success" {...args}>
Success
</Flex>
</Flex>
),
});
export const SurfacesAutoIncrement = meta.story({
export const BgAutoIncrement = 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.
Nested Flex components automatically increment their neutral background
level. No explicit bg prop is needed on inner Flex components.
</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 {...args} bg="neutral-1" direction="column">
<div>Neutral 1 (explicit)</div>
<Flex {...args} direction="column">
<div>Auto (becomes neutral-2)</div>
<Flex {...args} direction="column">
<div>Auto (becomes neutral-3)</div>
<Flex {...args} direction="column">
<div>Auto (becomes neutral-4)</div>
<Flex {...args} direction="column">
<div>Auto (stays neutral-4 - capped)</div>
</Flex>
</Flex>
</Flex>
+7 -10
View File
@@ -20,24 +20,21 @@ 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, useBg } 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,
});
// Resolve the bg this Flex creates for its children
const { bg: resolvedBg } = useBg({ bg: 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 +51,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,14 @@ export const FlexDefinition = {
'direction',
],
dataAttributes: {
surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const,
bg: [
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'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, Bg } 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<Bg>;
}
+17 -17
View File
@@ -21,38 +21,38 @@
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='neutral-4'],
.bui-GridItem[data-bg='neutral-4'] {
background-color: var(--bui-bg-neutral-4);
}
.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,78 @@ 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="neutral-4">
Neutral 4
</Grid.Root>
<Grid.Root {...args} surface={{ initial: '0', sm: '1' }}>
Responsive Surface
<Grid.Root {...args} bg={{ initial: 'neutral-1', sm: 'neutral-2' }}>
Responsive Bg
</Grid.Root>
<Grid.Root {...args} surface="danger">
Surface Danger
<Grid.Root {...args} bg="danger">
Danger
</Grid.Root>
<Grid.Root {...args} surface="warning">
Surface Warning
<Grid.Root {...args} bg="warning">
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 bg="neutral-3" style={{ padding: '4px' }}>
Neutral 3
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item surface="3" style={{ padding: '4px' }}>
Surface 3
<Grid.Item bg="neutral-4" style={{ padding: '4px' }}>
Neutral 4
</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 +185,20 @@ export const Surfaces = meta.story({
),
});
export const SurfacesAutoIncrement = meta.story({
export const BgAutoIncrement = 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.
Nested Grid components automatically increment their neutral background.
Each nested Grid.Item inherits and increments from its parent's bg.
</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}>
<Grid.Item>Nested: Auto (becomes neutral-2)</Grid.Item>
<Grid.Item>Nested: Auto (becomes neutral-2)</Grid.Item>
</Grid.Root>
</Grid.Item>
</Grid.Root>
+13 -19
View File
@@ -20,24 +20,21 @@ 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, useBg } 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,
});
// Resolve the bg this Grid creates for its children
const { bg: resolvedBg } = useBg({ bg: 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 +51,24 @@ 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,
});
// Resolve the bg this GridItem creates for its children
const { bg: resolvedBg } = useBg({ bg: 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 +85,8 @@ const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
/>
);
return resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>{content}</SurfaceProvider>
return resolvedBg ? (
<BgProvider bg={resolvedBg}>{content}</BgProvider>
) : (
content
);
+18 -2
View File
@@ -43,7 +43,15 @@ export const GridDefinition = {
'py',
],
dataAttributes: {
surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const,
bg: [
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
] as const,
},
} as const satisfies ComponentDefinition;
@@ -57,6 +65,14 @@ 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',
'neutral-4',
'danger',
'warning',
'success',
] as const,
},
} as const satisfies ComponentDefinition;
+3 -9
View File
@@ -14,13 +14,7 @@
* limitations under the License.
*/
import type {
Space,
SpaceProps,
Responsive,
Columns,
Surface,
} from '../../types';
import type { Space, SpaceProps, Responsive, Columns, Bg } from '../../types';
/** @public */
export interface GridProps extends SpaceProps {
@@ -29,7 +23,7 @@ export interface GridProps extends SpaceProps {
columns?: Responsive<Columns>;
gap?: Responsive<Space>;
style?: React.CSSProperties;
surface?: Responsive<Surface>;
bg?: Responsive<Bg>;
}
/** @public */
@@ -41,5 +35,5 @@ export interface GridItemProps {
colStart?: Responsive<Columns>;
rowSpan?: Responsive<Columns>;
style?: React.CSSProperties;
surface?: Responsive<Surface>;
bg?: Responsive<Bg>;
}
@@ -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,26 @@ 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">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" 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 4</Text>
<Flex align="center" bg="neutral-4" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
@@ -21,7 +21,7 @@ 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';
import { useBg } from '../../hooks/useBg';
/** @public */
export const ToggleButton = forwardRef(
@@ -34,17 +34,16 @@ export const ToggleButton = forwardRef(
},
);
const { children, className, iconStart, iconEnd, onSurface, ...rest } =
cleanedProps;
const { children, className, iconStart, iconEnd, ...rest } = cleanedProps;
const { surface } = useSurface({ onSurface });
const { bg } = useBg({ leaf: true });
return (
<AriaToggleButton
className={clsx(classNames.root, styles[classNames.root], className)}
ref={ref}
{...dataAttributes}
{...(typeof surface === 'string' ? { 'data-on-surface': surface } : {})}
{...(typeof bg === 'string' ? { 'data-bg': bg } : {})}
{...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,8 +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">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
@@ -138,8 +138,8 @@ export const Surfaces = meta.story({
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4" gap="4">
<Text>On Neutral 4</Text>
<Flex align="center" bg="neutral-4" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
+166
View File
@@ -0,0 +1,166 @@
/*
* 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 { Bg, Responsive } from '../types';
/** @public */
export interface BgContextValue {
bg: Responsive<Bg> | undefined;
}
/** @public */
export interface BgProviderProps {
bg: Responsive<Bg>;
children: ReactNode;
}
/** @public */
export type UseBgOptions =
| {
/**
* Container mode: the explicit bg value from the component's prop.
* If undefined, the container auto-increments from parent context.
*/
bg: Responsive<Bg> | undefined;
}
| {
/**
* Leaf mode: automatically reads bg from context and increments by 1.
* No prop is needed on the component.
*/
leaf: true;
};
const BgContext = createVersionedContext<{
1: BgContextValue;
}>('bg-context');
/**
* Increments a neutral bg level by one, capping at 'neutral-4'.
* Intent backgrounds (danger, warning, success) pass through unchanged.
*
* @param bg - The current bg value
* @returns The incremented bg value
* @internal
*/
function incrementNeutralBg(bg: Bg | undefined): Bg | undefined {
if (!bg) return undefined;
if (bg === 'neutral-1') return 'neutral-2';
if (bg === 'neutral-2') return 'neutral-3';
if (bg === 'neutral-3') return 'neutral-4';
if (bg === 'neutral-4') return 'neutral-4'; // capped
// Intent values pass through unchanged
return bg;
}
/**
* Resolves the bg value for a container component.
*
* - If an explicit bg prop is provided, use that.
* - If no bg prop is provided but there is a parent context, auto-increment from parent.
* - If no bg prop and no context, return undefined (no bg).
*
* @internal
*/
function resolveBgForContainer(
contextBg: Responsive<Bg> | undefined,
propBg: Responsive<Bg> | undefined,
): Responsive<Bg> | undefined {
// Explicit bg prop takes priority
if (propBg !== undefined) {
return propBg;
}
// No explicit bg: auto-increment from context if available
if (contextBg === undefined) {
return undefined;
}
// If context is a responsive object, we can't auto-increment
if (typeof contextBg === 'object') {
return undefined;
}
return incrementNeutralBg(contextBg);
}
/**
* Resolves the bg value for a leaf component.
*
* Always auto-increments from parent context. If no context, returns undefined.
*
* @internal
*/
function resolveBgForLeaf(
contextBg: Responsive<Bg> | undefined,
): Responsive<Bg> | undefined {
if (contextBg === undefined) {
return undefined;
}
// If context is a responsive object, we can't auto-increment
if (typeof contextBg === 'object') {
return undefined;
}
return incrementNeutralBg(contextBg);
}
/**
* 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 to access and resolve the current bg context.
*
* Supports two modes:
* - **Container mode** (`{ bg }`) - for components like Box that establish bg context.
* If bg prop is provided, uses that value. If bg is undefined but parent context exists,
* auto-increments from parent.
* - **Leaf mode** (`{ leaf: true }`) - for components like Button that consume bg context.
* Always auto-increments from parent context. No prop needed.
*
* @param options - Configuration for bg resolution
* @public
*/
export const useBg = (options?: UseBgOptions): BgContextValue => {
const value = useContext(BgContext)?.atVersion(1);
const context = value ?? { bg: undefined };
if (!options) {
return context;
}
if ('leaf' in options) {
return { bg: resolveBgForLeaf(context.bg) };
}
return { bg: resolveBgForContainer(context.bg, options.bg) };
};
@@ -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;
}
@@ -21,4 +21,5 @@ export type {
PropDefConfig,
UseDefinitionOptions,
UseDefinitionResult,
BgPropsConstraint,
} from './types';
+14 -21
View File
@@ -36,25 +36,19 @@ 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';
bg?: 'container' | 'leaf';
}
/**
* 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.
* - If bg is 'container', P must include 'bg'
* - If bg is 'leaf', no prop constraint (fully automatic)
*/
export type SurfacePropsConstraint<P, Surface> = Surface extends 'leaf'
? 'onSurface' extends keyof P
export type BgPropsConstraint<P, BgMode> = BgMode extends 'container'
? '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 container components must include bg in props type.';
}
: {};
@@ -81,12 +75,11 @@ 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<
BgMode extends 'container' | 'leaf' | undefined = undefined,
> = BgMode extends 'container'
? { bgChildren: ReactNode; children?: never }
: { children: ReactNode; bgChildren?: never };
type DataAttributeKeys<PropDefs> = {
[K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true }
@@ -98,7 +91,7 @@ type DataAttributes<PropDefs> = {
[K in DataAttributeKeys<PropDefs> as `data-${Lowercase<
string & K
>}`]?: string;
} & { 'data-on-surface'?: string };
} & { 'data-bg'?: string };
export type UtilityKeys<D extends ComponentConfig<any, any>> =
D['utilityProps'] extends ReadonlyArray<infer K extends string> ? K : never;
@@ -127,7 +120,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 { useBg, BgProvider, UseBgOptions } from '../useBg';
import { resolveResponsiveValue, processUtilityProps } from './helpers';
import type {
ComponentConfig,
@@ -36,14 +36,14 @@ 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 }
const bgOptions: UseBgOptions | undefined =
definition.bg === 'container'
? { bg: props.bg }
: definition.bg === 'leaf'
? { leaf: true }
: undefined;
const { surface: resolvedSurface } = useSurface(surfaceOptions);
const { bg: resolvedBg } = useBg(bgOptions);
const ownPropKeys = new Set(Object.keys(definition.propDefs));
const utilityPropKeys = new Set(definition.utilityProps ?? []);
@@ -77,31 +77,26 @@ 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);
// Set data-bg for bg container components with the resolved value
// This covers both explicit bg props and auto-incremented values
if (definition.bg === 'container' && resolvedBg !== undefined) {
const bgValue =
typeof resolvedBg === 'object'
? resolveResponsiveValue(resolvedBg as any, breakpoint)
: resolvedBg;
if (bgValue !== undefined) {
dataAttributes['data-bg'] = String(bgValue);
}
}
// 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);
// Add data-bg for bg leaf components (auto-incremented from context)
if (definition.bg === 'leaf' && resolvedBg !== undefined) {
const bgValue =
typeof resolvedBg === 'object'
? resolveResponsiveValue(resolvedBg as any, breakpoint)
: resolvedBg;
if (bgValue !== undefined) {
dataAttributes['data-bg'] = String(bgValue);
}
}
@@ -125,13 +120,11 @@ export function useDefinition<
}
let children: ReactNode | undefined;
let surfaceChildren: ReactNode | undefined;
let bgChildren: ReactNode | undefined;
if (definition.surface === 'container') {
surfaceChildren = resolvedSurface ? (
<SurfaceProvider surface={resolvedSurface}>
{props.children}
</SurfaceProvider>
if (definition.bg === 'container') {
bgChildren = resolvedBg ? (
<BgProvider bg={resolvedBg}>{props.children}</BgProvider>
) : (
props.children
);
@@ -143,9 +136,7 @@ export function useDefinition<
ownProps: {
classes,
...ownPropsResolved,
...(definition.surface === 'container'
? { surfaceChildren }
: { children }),
...(definition.bg === 'container' ? { bgChildren } : { children }),
},
restProps,
dataAttributes,
-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,
};
};
+6
View File
@@ -64,3 +64,9 @@ export * from './types';
// Hooks
export { useBreakpoint } from './hooks/useBreakpoint';
export { useBg, BgProvider } from './hooks/useBg';
export type {
BgContextValue,
BgProviderProps,
UseBgOptions,
} from './hooks/useBg';
+9 -20
View File
@@ -182,29 +182,18 @@ 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-4') and
* intent backgrounds ('danger', 'warning', 'success').
*
* @public
*/
export type Surface =
| '0'
| '1'
| '2'
| '3'
export type Bg =
| 'neutral-1'
| 'neutral-2'
| 'neutral-3'
| 'neutral-4'
| 'danger'
| 'warning'
| 'success'
| 'auto';
/** @public */
export interface LeafSurfaceProps {
onSurface?: Responsive<Surface>;
}
/** @public */
export interface ContainerSurfaceProps {
surface?: Responsive<Surface>;
}
| 'success';