From 03f1bbcc106d033bf6af11b1ad0f9e8dc407b3e1 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 6 Feb 2026 06:52:29 +0000 Subject: [PATCH 01/18] Add new surfaces to Card Signed-off-by: Charles de Dreuille --- .../ui/src/components/Card/Card.module.css | 30 ++++- .../ui/src/components/Card/Card.stories.tsx | 121 ++++++++++++++++++ packages/ui/src/components/Card/Card.tsx | 78 ++++++----- packages/ui/src/components/Card/definition.ts | 68 +++++++++- packages/ui/src/components/Card/index.ts | 15 +-- packages/ui/src/components/Card/types.ts | 52 ++++++-- .../src/hooks/useDefinition/useDefinition.tsx | 16 +++ 7 files changed, 312 insertions(+), 68 deletions(-) diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 85ae86901b..e6e5758e67 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -21,16 +21,42 @@ display: flex; flex-direction: column; gap: var(--bui-space-3); - background-color: var(--bui-bg-surface-1); border-radius: var(--bui-radius-3); padding-block: var(--bui-space-3); color: var(--bui-fg-primary); - border: 1px solid var(--bui-border); overflow: hidden; min-height: 0; width: 100%; } + .bui-Card[data-surface='0'] { + background-color: var(--bui-bg-surface-0); + } + + .bui-Card[data-surface='1'] { + background-color: var(--bui-bg-surface-1); + } + + .bui-Card[data-surface='2'] { + background-color: var(--bui-bg-surface-2); + } + + .bui-Card[data-surface='3'] { + background-color: var(--bui-bg-surface-3); + } + + .bui-Card[data-surface='danger'] { + background-color: var(--bui-bg-danger); + } + + .bui-Card[data-surface='warning'] { + background-color: var(--bui-bg-warning); + } + + .bui-Card[data-surface='success'] { + background-color: var(--bui-bg-success); + } + .bui-CardBody { flex: 1; min-height: 0; diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 438dd0e353..d1d6c43327 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -16,6 +16,9 @@ import preview from '../../../../../.storybook/preview'; import { Card, CardHeader, CardBody, CardFooter } from './Card'; import { Text } from '../..'; +import { Flex } from '../Flex'; +import { Box } from '../Box'; +import { Button } from '../Button'; const meta = preview.meta({ title: 'Backstage UI/Card', @@ -124,3 +127,121 @@ export const WithListRow = meta.story({ ), }); + +export const Surfaces = meta.story({ + render: args => ( + + + Default + No surface prop + + + Surface 0 + Explicit surface 0 + + + Surface 1 + Explicit surface 1 + + + Surface 2 + Explicit surface 2 + + + Surface 3 + Explicit surface 3 + + + Responsive + Surface 0 → 1 + + + Danger + Surface danger + + + Warning + Surface warning + + + Success + Surface success + + + ), +}); + +export const SurfacesNested = meta.story({ + render: args => ( + + + In this test, we are nesting cards on different surfaces to ensure that + the correct surface is applied to each element. If a Button is placed on + a surface that doesn't have the surface prop set, it will inherit the + surface from the parent. + + + Surface 1 + + + + Surface 2 + + + + Inherited + + + + + + + + + + ), +}); + +export const SurfacesAutoIncrement = meta.story({ + render: args => ( + + + Using surface="auto" automatically increments from the parent surface + level. This makes components more reusable as they don't need to know + their absolute surface level. Notice how each nested Card with + surface="auto" automatically increments: 0 → 1 → 2 → 3 (capped at 3). + + + Surface 0 (explicit) + + + Surface auto (becomes 1) + + + + Surface auto (becomes 2) + + + Surface auto (becomes 3) + + + Surface auto (stays 3 - capped) + Capped at max surface level + + + + + + + + + + + ), +}); diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index a3f4995588..b9ff308784 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -15,16 +15,19 @@ */ import { forwardRef } from 'react'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; -import { CardDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + CardDefinition, + CardHeaderDefinition, + CardBodyDefinition, + CardFooterDefinition, +} from './definition'; import type { CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, } from './types'; -import styles from './Card.module.css'; /** * Card component. @@ -32,18 +35,21 @@ import styles from './Card.module.css'; * @public */ export const Card = forwardRef((props, ref) => { - const { classNames, cleanedProps } = useStyles(CardDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps, dataAttributes } = useDefinition( + CardDefinition, + props, + ); + const { classes, surfaceChildren } = ownProps; return ( -
+
+ {surfaceChildren} +
); }); +Card.displayName = 'Card'; + /** * CardHeader component. * @@ -51,23 +57,19 @@ export const Card = forwardRef((props, ref) => { */ export const CardHeader = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles(CardDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(CardHeaderDefinition, props); + const { classes, children } = ownProps; return ( -
+
+ {children} +
); }, ); +CardHeader.displayName = 'CardHeader'; + /** * CardBody component. * @@ -75,19 +77,19 @@ export const CardHeader = forwardRef( */ export const CardBody = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles(CardDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(CardBodyDefinition, props); + const { classes, children } = ownProps; return ( -
+
+ {children} +
); }, ); +CardBody.displayName = 'CardBody'; + /** * CardFooter component. * @@ -95,19 +97,15 @@ export const CardBody = forwardRef( */ export const CardFooter = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles(CardDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(CardFooterDefinition, props); + const { classes, children } = ownProps; return ( -
+
+ {children} +
); }, ); + +CardFooter.displayName = 'CardFooter'; diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index cd0c5f634b..ec05494391 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -14,17 +14,73 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { + CardOwnProps, + CardHeaderOwnProps, + CardBodyOwnProps, + CardFooterOwnProps, +} from './types'; +import styles from './Card.module.css'; /** * Component definition for Card * @public */ -export const CardDefinition = { +export const CardDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Card', - header: 'bui-CardHeader', - body: 'bui-CardBody', - footer: 'bui-CardFooter', }, -} as const satisfies ComponentDefinition; + surface: 'container', + propDefs: { + surface: { dataAttribute: true, default: '1' }, + children: {}, + className: {}, + }, +}); + +/** + * Component definition for CardHeader + * @public + */ +export const CardHeaderDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-CardHeader', + }, + propDefs: { + children: {}, + className: {}, + }, +}); + +/** + * Component definition for CardBody + * @public + */ +export const CardBodyDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-CardBody', + }, + propDefs: { + children: {}, + className: {}, + }, +}); + +/** + * Component definition for CardFooter + * @public + */ +export const CardFooterDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-CardFooter', + }, + propDefs: { + children: {}, + className: {}, + }, +}); diff --git a/packages/ui/src/components/Card/index.ts b/packages/ui/src/components/Card/index.ts index dc7dbd4ead..f5596a717a 100644 --- a/packages/ui/src/components/Card/index.ts +++ b/packages/ui/src/components/Card/index.ts @@ -15,11 +15,10 @@ */ export { Card, CardHeader, CardBody, CardFooter } from './Card'; -export { CardDefinition } from './definition'; - -export type { - CardProps, - CardHeaderProps, - CardBodyProps, - CardFooterProps, -} from './types'; +export { + CardDefinition, + CardHeaderDefinition, + CardBodyDefinition, + CardFooterDefinition, +} from './definition'; +export type * from './types'; diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index 8b3419f6a2..50a94683e2 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -14,38 +14,66 @@ * limitations under the License. */ +import type { ReactNode } from 'react'; +import type { Responsive, Surface } from '../../types'; + +/** @public */ +export type CardOwnProps = { + surface?: Responsive; + children?: ReactNode; + className?: string; +}; + /** * Props for the Card component. * * @public */ -export interface CardProps extends React.HTMLAttributes { - children?: React.ReactNode; -} +export interface CardProps + extends CardOwnProps, + React.HTMLAttributes {} + +/** @public */ +export type CardHeaderOwnProps = { + children?: ReactNode; + className?: string; +}; /** * Props for the CardHeader component. * * @public */ -export interface CardHeaderProps extends React.HTMLAttributes { - children?: React.ReactNode; -} +export interface CardHeaderProps + extends CardHeaderOwnProps, + React.HTMLAttributes {} + +/** @public */ +export type CardBodyOwnProps = { + children?: ReactNode; + className?: string; +}; /** * Props for the CardBody component. * * @public */ -export interface CardBodyProps extends React.HTMLAttributes { - children?: React.ReactNode; -} +export interface CardBodyProps + extends CardBodyOwnProps, + React.HTMLAttributes {} + +/** @public */ +export type CardFooterOwnProps = { + children?: ReactNode; + className?: string; +}; /** * Props for the CardFooter component. * * @public */ -export interface CardFooterProps extends React.HTMLAttributes { - children?: React.ReactNode; -} +export interface CardFooterProps + extends CardFooterOwnProps, + React.HTMLAttributes {} diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index 0de2eb8358..c8bd3853dd 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -77,6 +77,22 @@ export function useDefinition< } } + // Override data-surface for container components with the resolved value + // This ensures 'auto' is replaced with the actual computed surface level + if ( + definition.surface === 'container' && + resolvedSurface !== undefined && + dataAttributes['data-surface'] !== undefined + ) { + const surfaceValue = + typeof resolvedSurface === 'object' + ? resolveResponsiveValue(resolvedSurface as any, breakpoint) + : resolvedSurface; + if (surfaceValue !== undefined) { + dataAttributes['data-surface'] = String(surfaceValue); + } + } + // Add data-on-surface for leaf components if (definition.surface === 'leaf' && resolvedSurface !== undefined) { // Handle responsive surface values - for data attributes, use the resolved string From 7c966d34eb2c7837859805c13864540042b98389 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 6 Feb 2026 06:56:56 +0000 Subject: [PATCH 02/18] Update Card.stories.tsx Signed-off-by: Charles de Dreuille --- .../ui/src/components/Card/Card.stories.tsx | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index d1d6c43327..d51c9ed5b3 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -208,40 +208,31 @@ export const SurfacesNested = meta.story({ export const SurfacesAutoIncrement = meta.story({ render: args => ( - - - Using surface="auto" automatically increments from the parent surface - level. This makes components more reusable as they don't need to know - their absolute surface level. Notice how each nested Card with - surface="auto" automatically increments: 0 → 1 → 2 → 3 (capped at 3). + + + + On surface 0 + Card auto → 1 + + + + + On surface 1 + Card auto → 2 + + + + + On surface 2 + Card auto → 3 + + + + + On surface 3 + Card auto → 3 (capped) + - - Surface 0 (explicit) - - - Surface auto (becomes 1) - - - - Surface auto (becomes 2) - - - Surface auto (becomes 3) - - - Surface auto (stays 3 - capped) - Capped at max surface level - - - - - - - - - ), }); From 09db540228a391f32ae25ca0c465c07f818d466f Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 6 Feb 2026 18:46:26 +0000 Subject: [PATCH 03/18] Migrate useSurface to useBg Signed-off-by: Charles de Dreuille --- packages/ui/report.api.md | 239 ++++++++++++------ .../ui/src/components/Alert/Alert.stories.tsx | 46 ++-- .../ui/src/components/Alert/definition.ts | 2 - packages/ui/src/components/Alert/types.ts | 8 +- packages/ui/src/components/Box/Box.module.css | 20 +- .../ui/src/components/Box/Box.stories.tsx | 100 +++----- packages/ui/src/components/Box/Box.tsx | 4 +- packages/ui/src/components/Box/definition.ts | 4 +- packages/ui/src/components/Box/types.ts | 4 +- .../src/components/Button/Button.module.css | 12 +- .../src/components/Button/Button.stories.tsx | 194 ++++++-------- .../ui/src/components/Button/definition.ts | 3 +- packages/ui/src/components/Button/types.ts | 4 +- .../ButtonIcon/ButtonIcon.module.css | 12 +- .../src/components/ButtonIcon/definition.ts | 3 +- .../ui/src/components/ButtonIcon/types.ts | 4 +- .../ButtonLink/ButtonLink.module.css | 12 +- .../src/components/ButtonLink/definition.ts | 3 +- .../ui/src/components/ButtonLink/types.ts | 4 +- .../ui/src/components/Card/Card.module.css | 22 +- .../ui/src/components/Card/Card.stories.tsx | 97 ++++--- packages/ui/src/components/Card/Card.tsx | 4 +- packages/ui/src/components/Card/definition.ts | 4 +- packages/ui/src/components/Card/types.ts | 4 +- .../ui/src/components/Flex/Flex.module.css | 20 +- .../ui/src/components/Flex/Flex.stories.tsx | 60 ++--- packages/ui/src/components/Flex/Flex.tsx | 17 +- packages/ui/src/components/Flex/definition.ts | 10 +- packages/ui/src/components/Flex/types.ts | 4 +- .../ui/src/components/Grid/Grid.module.css | 34 +-- .../ui/src/components/Grid/Grid.stories.tsx | 85 +++---- packages/ui/src/components/Grid/Grid.tsx | 32 +-- packages/ui/src/components/Grid/definition.ts | 20 +- packages/ui/src/components/Grid/types.ts | 12 +- .../ToggleButton/ToggleButton.stories.tsx | 18 +- .../components/ToggleButton/ToggleButton.tsx | 9 +- .../ui/src/components/ToggleButton/types.ts | 3 - .../ToggleButtonGroup.stories.tsx | 18 +- packages/ui/src/hooks/useBg.tsx | 166 ++++++++++++ .../hooks/useDefinition/defineComponent.ts | 4 +- packages/ui/src/hooks/useDefinition/index.ts | 1 + packages/ui/src/hooks/useDefinition/types.ts | 35 +-- .../src/hooks/useDefinition/useDefinition.tsx | 67 +++-- packages/ui/src/hooks/useSurface.tsx | 204 --------------- packages/ui/src/index.ts | 6 + packages/ui/src/types.ts | 29 +-- 46 files changed, 809 insertions(+), 854 deletions(-) create mode 100644 packages/ui/src/hooks/useBg.tsx delete mode 100644 packages/ui/src/hooks/useSurface.tsx diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 82069bc469..0db25deaf3 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -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 | undefined; +} + +// @public +export const BgProvider: ({ bg, children }: BgProviderProps) => JSX_2.Element; + +// @public (undocumented) +export interface BgProviderProps { + // (undocumented) + bg: Responsive; + // (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; + bg?: Responsive; 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 { - // (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 {} // @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 { - // (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 {} // @public export const CardHeader: ForwardRefExoticComponent< @@ -505,16 +573,41 @@ export const CardHeader: ForwardRefExoticComponent< >; // @public -export interface CardHeaderProps extends React.HTMLAttributes { - // (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 { - // (undocumented) - children?: React.ReactNode; -} +export interface CardHeaderProps + extends CardHeaderOwnProps, + React.HTMLAttributes {} + +// @public (undocumented) +export type CardOwnProps = { + bg?: Responsive; + children?: ReactNode; + className?: string; +}; + +// @public +export interface CardProps + extends CardOwnProps, + React.HTMLAttributes {} // @public (undocumented) export const Cell: { @@ -684,12 +777,6 @@ export interface ContainerProps { style?: React.CSSProperties; } -// @public (undocumented) -export interface ContainerSurfaceProps { - // (undocumented) - surface?: Responsive; -} - // @public (undocumented) export interface CursorParams { // (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; + // (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; } // @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; // (undocumented) children?: React.ReactNode; // (undocumented) @@ -978,12 +1067,12 @@ export interface GridItemProps { rowSpan?: Responsive; // (undocumented) style?: React.CSSProperties; - // (undocumented) - surface?: Responsive; } // @public (undocumented) export interface GridProps extends SpaceProps { + // (undocumented) + bg?: Responsive; // (undocumented) children?: React.ReactNode; // (undocumented) @@ -994,8 +1083,6 @@ export interface GridProps extends SpaceProps { gap?: Responsive; // (undocumented) style?: React.CSSProperties; - // (undocumented) - surface?: Responsive; } // @public @@ -1088,12 +1175,6 @@ export type JustifyContent = | 'around' | 'between'; -// @public (undocumented) -export interface LeafSurfaceProps { - // (undocumented) - onSurface?: Responsive; -} - // @public (undocumented) export const Link: ForwardRefExoticComponent< LinkProps & RefAttributes @@ -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 @@ -2079,7 +2149,6 @@ export interface ToggleButtonProps extends ToggleButtonProps_2 { iconEnd?: ReactElement; // (undocumented) iconStart?: ReactElement; - onSurface?: Responsive; // (undocumented) size?: 'small' | 'medium' | Partial>; } @@ -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 | undefined; + } + | { + leaf: true; + }; + // @public (undocumented) export const useBreakpoint: () => { breakpoint: Breakpoint; diff --git a/packages/ui/src/components/Alert/Alert.stories.tsx b/packages/ui/src/components/Alert/Alert.stories.tsx index a81337fe71..1cd21434d3 100644 --- a/packages/ui/src/components/Alert/Alert.stories.tsx +++ b/packages/ui/src/components/Alert/Alert.stories.tsx @@ -284,50 +284,46 @@ export const LongContent = meta.story({ ), }); -export const OnDifferentSurfaces = meta.story({ +export const OnDifferentBackgrounds = meta.story({ render: () => ( - Default Surface + Default - - + + - On Surface 0 - - - + On Neutral 1 + + + - On Surface 1 - - - + On Neutral 2 + + + - On Surface 2 - - - + On Neutral 3 + + + - On Surface 3 - - - + On Neutral 4 + + + @@ -349,7 +345,7 @@ export const Responsive = meta.story({ export const WithUtilityProps = meta.story({ render: () => ( - + ()({ 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()({ customActions: {}, title: {}, description: {}, - surface: {}, className: {}, style: {}, }, diff --git a/packages/ui/src/components/Alert/types.ts b/packages/ui/src/components/Alert/types.ts index b21fd6aa8c..c0829899f4 100644 --- a/packages/ui/src/components/Alert/types.ts +++ b/packages/ui/src/components/Alert/types.ts @@ -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; diff --git a/packages/ui/src/components/Box/Box.module.css b/packages/ui/src/components/Box/Box.module.css index 6a0ebb9fdd..07b0ac2dc1 100644 --- a/packages/ui/src/components/Box/Box.module.css +++ b/packages/ui/src/components/Box/Box.module.css @@ -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); } } diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index db0c05a24b..a0504d98d3 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -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 => ( Default - - Surface 0 + + Neutral 1 - - Surface 1 + + Neutral 2 - - Surface 2 + + Neutral 3 - - Surface 3 + + Responsive Neutral - - Responsive Surface + + Danger - - Surface Danger + + Warning - - Surface Warning - - - Surface Success + + Success ), }); -export const SurfacesNested = meta.story({ +export const NestedNeutralColors = meta.story({ args: { px: '6', py: '4' }, render: args => ( - - - 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. - - - - - + + +
With bg="neutral-1" — nested boxes and buttons auto-increment
+ + - - - -
-
- ), -}); - -export const SurfacesAutoIncrement = meta.story({ - args: { px: '6', py: '4' }, - render: args => ( - - - Using surface="auto" automatically increments from the parent surface - level. This makes components more reusable as they don't need to know - their absolute surface level. Notice how each nested Box with - surface="auto" automatically increments: 0 → 1 → 2 → 3 (capped at 3). - - - Surface 0 (explicit) - - Surface auto (becomes 1) - - - Surface auto (becomes 2) - - Surface auto (becomes 3) - - Surface auto (stays 3 - capped) - + + + - + + +
Without bg — no context, no auto-increment
+ + + + + + + + + +
), }); diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index 23ed5b9303..dc1a8521ea 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -25,7 +25,7 @@ export const Box = forwardRef((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((props, ref) => { ...dataAttributes, ...restProps, }, - surfaceChildren, + bgChildren, ); }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index f202291175..9d9c3b9417 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -27,10 +27,10 @@ export const BoxDefinition = defineComponent()({ classNames: { root: 'bui-Box', }, - surface: 'container', + bg: 'container', propDefs: { as: { default: 'div' }, - surface: { dataAttribute: true }, + bg: { dataAttribute: true }, children: {}, className: {}, style: {}, diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index 09b45429c3..89325cf8f2 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -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; + bg?: Responsive; children?: ReactNode; className?: string; style?: CSSProperties; diff --git a/packages/ui/src/components/Button/Button.module.css b/packages/ui/src/components/Button/Button.module.css index 9b6fbbd09d..847e9ced0d 100644 --- a/packages/ui/src/components/Button/Button.module.css +++ b/packages/ui/src/components/Button/Button.module.css @@ -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); } diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index b5eb885930..f04550ef57 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -81,32 +81,9 @@ export const Variants = meta.story({
- - Neutral 0 - - - - - - - - - Neutral 1 - + @@ -129,63 +106,72 @@ export const Variants = meta.story({ Neutral 2 - - - - - - - - - - + + + + + + + + Neutral 3 - - - - - - - - - - - - + + + + + + + + + + + Neutral 4 + + + + + + + + ), @@ -246,8 +232,8 @@ export const Destructive = meta.story({
- On Surface 1 - + On Neutral 1 + @@ -462,49 +448,33 @@ export const LoadingVariants = meta.story({ ), }); -export const OnSurfaceAuto = meta.story({ +export const AutoBg = meta.story({ render: () => (
- 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.
- - Surface 0 container + + Neutral 1 container - - - + + - - Surface 1 container + + Neutral 2 container - - - + + - - Surface 2 container + + Neutral 3 container - - - + +
diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index 65c4e82f53..6b6b49ab53 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent()({ 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()({ loading: { dataAttribute: true }, iconStart: {}, iconEnd: {}, - onSurface: {}, children: {}, className: {}, style: {}, diff --git a/packages/ui/src/components/Button/types.ts b/packages/ui/src/components/Button/types.ts index c50cd5c8fc..a18d9e9164 100644 --- a/packages/ui/src/components/Button/types.ts +++ b/packages/ui/src/components/Button/types.ts @@ -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; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css index 19a74db3bf..5730efee0d 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css @@ -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); } diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 3befeef7c1..03c2bd9387 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -29,13 +29,12 @@ export const ButtonIconDefinition = defineComponent()({ 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: {}, }, diff --git a/packages/ui/src/components/ButtonIcon/types.ts b/packages/ui/src/components/ButtonIcon/types.ts index 750e0db5e9..92618d5ff6 100644 --- a/packages/ui/src/components/ButtonIcon/types.ts +++ b/packages/ui/src/components/ButtonIcon/types.ts @@ -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; diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.module.css b/packages/ui/src/components/ButtonLink/ButtonLink.module.css index 1184a160b7..8713a62a58 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.module.css +++ b/packages/ui/src/components/ButtonLink/ButtonLink.module.css @@ -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); } diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index e6a80e09b3..1e3727c60e 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -28,13 +28,12 @@ export const ButtonLinkDefinition = defineComponent()({ 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: {}, diff --git a/packages/ui/src/components/ButtonLink/types.ts b/packages/ui/src/components/ButtonLink/types.ts index 0379d72adb..0f01c3cf40 100644 --- a/packages/ui/src/components/ButtonLink/types.ts +++ b/packages/ui/src/components/ButtonLink/types.ts @@ -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; diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 614aa31bf7..62c4c78cb7 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -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); } diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 06ef9a2fa2..b934048622 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -128,72 +128,71 @@ export const WithListRow = meta.story({ ), }); -export const Surfaces = meta.story({ +export const Backgrounds = meta.story({ render: args => ( Default - No surface prop + No bg prop - - Surface 0 - Explicit surface 0 + + Neutral 1 + Explicit neutral-1 - - Surface 1 - Explicit surface 1 + + Neutral 2 + Explicit neutral-2 - - Surface 2 - Explicit surface 2 + + Neutral 3 + Explicit neutral-3 - - Surface 3 - Explicit surface 3 + + Neutral 4 + Explicit neutral-4 Responsive - Surface 0 → 1 + Neutral 1 → 2 - + Danger - Surface danger + Bg danger - + Warning - Surface warning + Bg warning - + Success - Surface success + Bg success ), }); -export const SurfacesNested = meta.story({ +export const BgNested = meta.story({ render: args => ( - In this test, we are nesting cards on different surfaces to ensure that - the correct surface is applied to each element. If a Button is placed on - a surface that doesn't have the surface prop set, it will inherit the - surface from the parent. + 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. - - Surface 1 + + Neutral 1 - - Surface 2 + + Neutral 2 - Inherited + Auto-incremented @@ -206,31 +205,31 @@ export const SurfacesNested = meta.story({ ), }); -export const SurfacesAutoIncrement = meta.story({ +export const BgAutoIncrement = meta.story({ render: args => ( - - - On surface 0 - Card auto → 1 + + + On neutral-1 + Card auto → neutral-2 - - - On surface 1 - Card auto → 2 + + + On neutral-2 + Card auto → neutral-3 - - - On surface 2 - Card auto → 3 + + + On neutral-3 + Card auto → neutral-4 - - - On surface 3 - Card auto → 3 (capped) + + + On neutral-4 + Card auto → neutral-4 (capped) diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index b9ff308784..4e030fcc4c 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -39,11 +39,11 @@ export const Card = forwardRef((props, ref) => { CardDefinition, props, ); - const { classes, surfaceChildren } = ownProps; + const { classes, bgChildren } = ownProps; return (
- {surfaceChildren} + {bgChildren}
); }); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index ec05494391..73dc1b02af 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -32,9 +32,9 @@ export const CardDefinition = defineComponent()({ classNames: { root: 'bui-Card', }, - surface: 'container', + bg: 'container', propDefs: { - surface: { dataAttribute: true, default: '1' }, + bg: { dataAttribute: true }, children: {}, className: {}, }, diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index 50a94683e2..fae864e621 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -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; + bg?: Responsive; children?: ReactNode; className?: string; }; diff --git a/packages/ui/src/components/Flex/Flex.module.css b/packages/ui/src/components/Flex/Flex.module.css index 5923fa160e..01f9c29dbc 100644 --- a/packages/ui/src/components/Flex/Flex.module.css +++ b/packages/ui/src/components/Flex/Flex.module.css @@ -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); } } diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 998d97bc0a..5bc7258312 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -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 => ( Default - - Surface 0 + + Neutral 1 - - Surface 1 + + Neutral 2 - - Surface 2 + + Neutral 3 - - Surface 3 + + Neutral 4 - - Responsive Surface + + Responsive Bg - - Surface Danger + + Danger - - Surface Warning + + Warning - - Surface Success + + Success ), }); -export const SurfacesAutoIncrement = meta.story({ +export const BgAutoIncrement = meta.story({ args: { px: '6', py: '4', gap: '4' }, render: args => (
- 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.
- -
Surface 0 (explicit)
- -
Surface auto (becomes 1)
- -
Surface auto (becomes 2)
- -
Surface auto (becomes 3)
- -
Surface auto (stays 3 - capped)
+ +
Neutral 1 (explicit)
+ +
Auto (becomes neutral-2)
+ +
Auto (becomes neutral-3)
+ +
Auto (becomes neutral-4)
+ +
Auto (stays neutral-4 - capped)
diff --git a/packages/ui/src/components/Flex/Flex.tsx b/packages/ui/src/components/Flex/Flex.tsx index 3d29408b2b..1af1050811 100644 --- a/packages/ui/src/components/Flex/Flex.tsx +++ b/packages/ui/src/components/Flex/Flex.tsx @@ -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((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 = (
((props, ref) => { /> ); - return resolvedSurface ? ( - {content} + return resolvedBg ? ( + {content} ) : ( content ); diff --git a/packages/ui/src/components/Flex/definition.ts b/packages/ui/src/components/Flex/definition.ts index 3f747e153b..12772e107e 100644 --- a/packages/ui/src/components/Flex/definition.ts +++ b/packages/ui/src/components/Flex/definition.ts @@ -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; diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index 33bc476f93..a3503dc79c 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -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; + bg?: Responsive; } diff --git a/packages/ui/src/components/Grid/Grid.module.css b/packages/ui/src/components/Grid/Grid.module.css index aa281b8695..e8a7248bad 100644 --- a/packages/ui/src/components/Grid/Grid.module.css +++ b/packages/ui/src/components/Grid/Grid.module.css @@ -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); } } diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index b3524444fd..80c4327dce 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -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 => ( - - Surface 0 + + Neutral 1 - - Surface 1 + + Neutral 2 - - Surface 2 + + Neutral 3 - - Surface 3 + + Neutral 4 - - Responsive Surface + + Responsive Bg - - Surface Danger + + Danger - - Surface Warning + + Warning - - Surface Success + + Success - - Surface 0 + + Neutral 1 - - Surface 1 + + Neutral 2 - - Surface 2 + + Neutral 3 - - Surface 3 + + Neutral 4 - Responsive Surface + Responsive Bg - - Surface Danger + + Danger - - Surface Warning + + Warning - - Surface Success + + Success @@ -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 => (
- 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.
- - Surface 0 (Grid.Root) - Surface auto (becomes 1) + + Neutral 1 (Grid.Root) - - Nested: Surface auto (becomes 1) - - Nested: Surface auto (becomes 2) - + + Nested: Auto (becomes neutral-2) + Nested: Auto (becomes neutral-2) diff --git a/packages/ui/src/components/Grid/Grid.tsx b/packages/ui/src/components/Grid/Grid.tsx index 52dd22167e..45eb9c76d2 100644 --- a/packages/ui/src/components/Grid/Grid.tsx +++ b/packages/ui/src/components/Grid/Grid.tsx @@ -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((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 = (
((props, ref) => { /> ); - return resolvedSurface ? ( - {content} + return resolvedBg ? ( + {content} ) : ( content ); }); const GridItem = forwardRef((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 = (
((props, ref) => { /> ); - return resolvedSurface ? ( - {content} + return resolvedBg ? ( + {content} ) : ( content ); diff --git a/packages/ui/src/components/Grid/definition.ts b/packages/ui/src/components/Grid/definition.ts index a6c5c582c7..cf2e14c58f 100644 --- a/packages/ui/src/components/Grid/definition.ts +++ b/packages/ui/src/components/Grid/definition.ts @@ -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; diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index f5ce9ca9cb..cd782b88e4 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -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; gap?: Responsive; style?: React.CSSProperties; - surface?: Responsive; + bg?: Responsive; } /** @public */ @@ -41,5 +35,5 @@ export interface GridItemProps { colStart?: Responsive; rowSpan?: Responsive; style?: React.CSSProperties; - surface?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index 14183e6ede..1a40dc626a 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -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({ - On Surface 0 - + On Neutral 1 + Toggle - On Surface 1 - + On Neutral 2 + Toggle - On Surface 2 - + On Neutral 3 + Toggle - On Surface 3 - + On Neutral 4 + Toggle diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index 9c96304a80..a497d97886 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -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 ( {renderProps => { diff --git a/packages/ui/src/components/ToggleButton/types.ts b/packages/ui/src/components/ToggleButton/types.ts index de70bd3c29..2e5c5755f2 100644 --- a/packages/ui/src/components/ToggleButton/types.ts +++ b/packages/ui/src/components/ToggleButton/types.ts @@ -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>; iconStart?: ReactElement; iconEnd?: ReactElement; - /** Surface the toggle button is placed on. Defaults to context surface if available */ - onSurface?: Responsive; } diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 30e53f0669..458d57683a 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -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({ - On Surface 0 - + On Neutral 1 + - On Surface 1 - + On Neutral 2 + - On Surface 2 - + On Neutral 3 + - On Surface 3 - + On Neutral 4 + | undefined; +} + +/** @public */ +export interface BgProviderProps { + bg: Responsive; + 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 | 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 | undefined, + propBg: Responsive | undefined, +): Responsive | 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 | undefined, +): Responsive | 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 ( + + {children} + + ); +}; + +/** + * 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) }; +}; diff --git a/packages/ui/src/hooks/useDefinition/defineComponent.ts b/packages/ui/src/hooks/useDefinition/defineComponent.ts index 5e2a0139a7..6dbfb5742b 100644 --- a/packages/ui/src/hooks/useDefinition/defineComponent.ts +++ b/packages/ui/src/hooks/useDefinition/defineComponent.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import type { ComponentConfig, SurfacePropsConstraint } from './types'; +import type { ComponentConfig, BgPropsConstraint } from './types'; export function defineComponent

>() { return < const S extends Record, const C extends ComponentConfig, >( - config: C & SurfacePropsConstraint, + config: C & BgPropsConstraint, ): C => config; } diff --git a/packages/ui/src/hooks/useDefinition/index.ts b/packages/ui/src/hooks/useDefinition/index.ts index a180645975..533b7f8e21 100644 --- a/packages/ui/src/hooks/useDefinition/index.ts +++ b/packages/ui/src/hooks/useDefinition/index.ts @@ -21,4 +21,5 @@ export type { PropDefConfig, UseDefinitionOptions, UseDefinitionResult, + BgPropsConstraint, } from './types'; diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index 3cef56148e..1249e8e3db 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -36,25 +36,19 @@ export interface ComponentConfig< propDefs: { [K in keyof P]: PropDefConfig }; // 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 = Surface extends 'leaf' - ? 'onSurface' extends keyof P +export type BgPropsConstraint = 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; }; -type ChildrenProps = - 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 = { [K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true } @@ -98,7 +91,7 @@ type DataAttributes = { [K in DataAttributeKeys as `data-${Lowercase< string & K >}`]?: string; -} & { 'data-on-surface'?: string }; +} & { 'data-bg'?: string }; export type UtilityKeys> = D['utilityProps'] extends ReadonlyArray ? K : never; @@ -127,7 +120,7 @@ export interface UseDefinitionResult< ownProps: { classes: Record; } & ResolvedOwnProps & - ChildrenProps; + ChildrenProps; // Rest props excludes both propDefs keys AND utility prop keys restProps: keyof Omit> extends never diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index c8bd3853dd..9b60feab07 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -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 { 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 ? ( - - {props.children} - + if (definition.bg === 'container') { + bgChildren = resolvedBg ? ( + {props.children} ) : ( props.children ); @@ -143,9 +136,7 @@ export function useDefinition< ownProps: { classes, ...ownPropsResolved, - ...(definition.surface === 'container' - ? { surfaceChildren } - : { children }), + ...(definition.bg === 'container' ? { bgChildren } : { children }), }, restProps, dataAttributes, diff --git a/packages/ui/src/hooks/useSurface.tsx b/packages/ui/src/hooks/useSurface.tsx deleted file mode 100644 index a088fb3b63..0000000000 --- a/packages/ui/src/hooks/useSurface.tsx +++ /dev/null @@ -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 | undefined; -} - -/** @public */ -export interface SurfaceProviderProps { - surface: Responsive; - 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; - /** - * 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; -} - -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 | undefined, - requestedSurface: Responsive | undefined, -): Responsive | 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 | undefined, - requestedSurface: Responsive | undefined, -): Responsive | 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 ( - - {children} - - ); -}; - -/** - * 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, - }; -}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 4ce616d5f1..dc920900f3 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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'; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 933e1d8aa9..9587dd1142 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -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; -} - -/** @public */ -export interface ContainerSurfaceProps { - surface?: Responsive; -} + | 'success'; From 7898df0aa08d81a8e40616efbef45bae4bea7848 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sat, 7 Feb 2026 08:30:44 +0000 Subject: [PATCH 04/18] Add changeset Signed-off-by: Charles de Dreuille --- .changeset/clean-bags-occur.md | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .changeset/clean-bags-occur.md diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md new file mode 100644 index 0000000000..bf376e2300 --- /dev/null +++ b/.changeset/clean-bags-occur.md @@ -0,0 +1,69 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Replaced `Surface` / `onSurface` system with new `Bg` background system + +The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a new `Bg` type with semantic neutral levels (`'neutral-1'` through `'neutral-4'`) and intents (`'danger'`, `'warning'`, `'success'`). Leaf components like Button no longer need an explicit prop — backgrounds auto-increment from parent context. + +New `useBg` hook and `BgProvider` replace the deleted `useSurface` hook and `SurfaceProvider`. + +**Migration:** + +Rename the `surface` prop to `bg` on container components and update values: + +```diff +- ++ + +- ++ + +- ++ + +- ++ +``` + +Remove `onSurface` from leaf components — it is now fully automatic: + +```diff +- - - Neutral 4 - - - - - - - - - ), }); diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css index 5730efee0d..128e958594 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css @@ -85,19 +85,19 @@ --bg-active: var(--bui-bg-neutral-1-pressed); --fg: var(--bui-fg-primary); - &[data-bg='neutral-2'] { + &[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-bg='neutral-3'] { + &[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-bg='neutral-4'] { + &[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-bg='neutral-2'] { + &[data-on-bg='neutral-1'] { --bg-hover: var(--bui-bg-neutral-2-hover); --bg-active: var(--bui-bg-neutral-2-pressed); } - &[data-bg='neutral-3'] { + &[data-on-bg='neutral-2'] { --bg-hover: var(--bui-bg-neutral-3-hover); --bg-active: var(--bui-bg-neutral-3-pressed); } - &[data-bg='neutral-4'] { + &[data-on-bg='neutral-3'] { --bg-hover: var(--bui-bg-neutral-4-hover); --bg-active: var(--bui-bg-neutral-4-pressed); } diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.module.css b/packages/ui/src/components/ButtonLink/ButtonLink.module.css index 8713a62a58..74a3fe6ba6 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.module.css +++ b/packages/ui/src/components/ButtonLink/ButtonLink.module.css @@ -79,19 +79,19 @@ --bg-active: var(--bui-bg-neutral-1-pressed); --fg: var(--bui-fg-primary); - &[data-bg='neutral-2'] { + &[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-bg='neutral-3'] { + &[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-bg='neutral-4'] { + &[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-bg='neutral-2'] { + &[data-on-bg='neutral-1'] { --bg-hover: var(--bui-bg-neutral-2-hover); --bg-active: var(--bui-bg-neutral-2-pressed); } - &[data-bg='neutral-3'] { + &[data-on-bg='neutral-2'] { --bg-hover: var(--bui-bg-neutral-3-hover); --bg-active: var(--bui-bg-neutral-3-pressed); } - &[data-bg='neutral-4'] { + &[data-on-bg='neutral-3'] { --bg-hover: var(--bui-bg-neutral-4-hover); --bg-active: var(--bui-bg-neutral-4-pressed); } diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 62c4c78cb7..1ca0b7a1b4 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -42,10 +42,6 @@ background-color: var(--bui-bg-neutral-3); } - .bui-Card[data-bg='neutral-4'] { - background-color: var(--bui-bg-neutral-4); - } - .bui-Card[data-bg='danger'] { background-color: var(--bui-bg-danger); } diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index b934048622..80dd0ad8d6 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -147,10 +147,6 @@ export const Backgrounds = meta.story({ Neutral 3 Explicit neutral-3 - - Neutral 4 - Explicit neutral-4 - Card auto → neutral-4 - - - On neutral-4 - Card auto → neutral-4 (capped) - - ), }); diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index fae864e621..2b32bbf50a 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -15,11 +15,11 @@ */ import type { ReactNode } from 'react'; -import type { Responsive, Bg } from '../../types'; +import type { Responsive, ContainerBg } from '../../types'; /** @public */ export type CardOwnProps = { - bg?: Responsive; + bg?: Responsive; children?: ReactNode; className?: string; }; diff --git a/packages/ui/src/components/Flex/Flex.module.css b/packages/ui/src/components/Flex/Flex.module.css index 01f9c29dbc..499326b9ea 100644 --- a/packages/ui/src/components/Flex/Flex.module.css +++ b/packages/ui/src/components/Flex/Flex.module.css @@ -36,10 +36,6 @@ background-color: var(--bui-bg-neutral-3); } - .bui-Flex[data-bg='neutral-4'] { - background-color: var(--bui-bg-neutral-4); - } - .bui-Flex[data-bg='danger'] { background-color: var(--bui-bg-danger); } diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 5bc7258312..158110d20a 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -261,9 +261,6 @@ export const Backgrounds = meta.story({ Neutral 3 - - Neutral 4 - Responsive Bg diff --git a/packages/ui/src/components/Flex/definition.ts b/packages/ui/src/components/Flex/definition.ts index 12772e107e..d0452d6b2c 100644 --- a/packages/ui/src/components/Flex/definition.ts +++ b/packages/ui/src/components/Flex/definition.ts @@ -49,7 +49,6 @@ export const FlexDefinition = { 'neutral-1', 'neutral-2', 'neutral-3', - 'neutral-4', 'danger', 'warning', 'success', diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index a3503dc79c..ad5404e41d 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Responsive, Space, SpaceProps, Bg } from '../../types'; +import type { Responsive, Space, SpaceProps, ContainerBg } 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; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/components/Grid/Grid.module.css b/packages/ui/src/components/Grid/Grid.module.css index e8a7248bad..8fdaf15c3b 100644 --- a/packages/ui/src/components/Grid/Grid.module.css +++ b/packages/ui/src/components/Grid/Grid.module.css @@ -36,11 +36,6 @@ background-color: var(--bui-bg-neutral-3); } - .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); diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 80c4327dce..fe5b96dfe1 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -120,9 +120,6 @@ export const Backgrounds = meta.story({ Neutral 3 - - Neutral 4 - Responsive Bg @@ -152,11 +149,6 @@ export const Backgrounds = meta.story({ Neutral 3 - - - Neutral 4 - - ; gap?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } /** @public */ @@ -35,5 +41,5 @@ export interface GridItemProps { colStart?: Responsive; rowSpan?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index 1a40dc626a..a89bbe3862 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -81,12 +81,6 @@ export const Backgrounds = meta.story({ Toggle - - On Neutral 4 - - Toggle - - ), }); diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index 612db4945c..c9956b4f22 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -43,7 +43,7 @@ export const ToggleButton = forwardRef( className={clsx(classNames.root, styles[classNames.root], className)} ref={ref} {...dataAttributes} - {...(bg ? { 'data-bg': bg } : {})} + {...(bg ? { 'data-on-bg': bg } : {})} {...rest} > {renderProps => { diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 458d57683a..617cd0c0fa 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -137,19 +137,6 @@ export const Backgrounds = meta.story({ - - On Neutral 4 - - - Option 1 - Option 2 - Option 3 - - - ), }); diff --git a/packages/ui/src/hooks/useBg.tsx b/packages/ui/src/hooks/useBg.tsx index 7a681f9b19..fd3d4319e5 100644 --- a/packages/ui/src/hooks/useBg.tsx +++ b/packages/ui/src/hooks/useBg.tsx @@ -19,18 +19,18 @@ import { createVersionedContext, createVersionedValueMap, } from '@backstage/version-bridge'; -import { Bg, Responsive } from '../types'; +import { ContainerBg, Responsive } from '../types'; import { useBreakpoint } from './useBreakpoint'; import { resolveResponsiveValue } from './useDefinition/helpers'; /** @public */ export interface BgContextValue { - bg: Bg | undefined; + bg: ContainerBg | undefined; } /** @public */ export interface BgProviderProps { - bg: Bg; + bg: ContainerBg; children: ReactNode; } @@ -40,16 +40,18 @@ export interface UseBgOptions { * The bg mode of the component. * * - `'container'` — for components like Box, Card, Flex that establish bg context. - * If `bg` prop is provided, uses that value. Otherwise auto-increments from parent. + * If `bg` prop is provided, uses that value. Otherwise auto-increments from parent, + * capping at `neutral-3`. * - `'leaf'` — for components like Button that consume bg context. - * Always auto-increments from parent context. The `bg` prop is ignored. + * Returns the parent context bg unchanged (no increment). The leaf component's CSS + * handles the visual step-up. The `bg` prop is ignored. */ mode: 'container' | 'leaf'; /** * The explicit bg value from the component's prop. * Only used in container mode — leaf mode ignores this. */ - bg?: Responsive; + bg?: Responsive; } const BgContext = createVersionedContext<{ @@ -57,19 +59,23 @@ const BgContext = createVersionedContext<{ }>('bg-context'); /** - * Increments a neutral bg level by one, capping at 'neutral-4'. + * Increments a neutral bg level by one, capping at 'neutral-3'. * Intent backgrounds (danger, warning, success) pass through unchanged. * + * Only used by container components for auto-increment. The 'neutral-4' + * level is reserved for leaf components and is never set on containers. + * * @param bg - The current bg value * @returns The incremented bg value * @internal */ -function incrementNeutralBg(bg: Bg | undefined): Bg | undefined { +function incrementNeutralBg( + bg: ContainerBg | undefined, +): ContainerBg | 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 + if (bg === 'neutral-3') return 'neutral-3'; // capped at neutral-3 // Intent values pass through unchanged return bg; } @@ -84,9 +90,9 @@ function incrementNeutralBg(bg: Bg | undefined): Bg | undefined { * @internal */ function resolveBgForContainer( - contextBg: Bg | undefined, - propBg: Bg | undefined, -): Bg | undefined { + contextBg: ContainerBg | undefined, + propBg: ContainerBg | undefined, +): ContainerBg | undefined { // Explicit bg prop takes priority if (propBg !== undefined) { return propBg; @@ -103,16 +109,16 @@ function resolveBgForContainer( /** * Resolves the bg value for a leaf component. * - * Always auto-increments from parent context. If no context, returns undefined. + * Returns the parent context bg unchanged. The leaf component's CSS + * handles the visual step-up (e.g. on neutral-1 surface → use neutral-2 tokens). + * If no context, returns undefined. * * @internal */ -function resolveBgForLeaf(contextBg: Bg | undefined): Bg | undefined { - if (contextBg === undefined) { - return undefined; - } - - return incrementNeutralBg(contextBg); +function resolveBgForLeaf( + contextBg: ContainerBg | undefined, +): ContainerBg | undefined { + return contextBg; } /** @@ -135,8 +141,8 @@ export const BgProvider = ({ bg, children }: BgProviderProps) => { * and (for containers) the explicit bg prop value. * * - **Container mode** — uses explicit `bg` if provided, otherwise auto-increments - * from parent context. Caps at `neutral-4`. - * - **Leaf mode** — always auto-increments from parent context. No prop needed. + * from parent context. Caps at `neutral-3`. + * - **Leaf mode** — returns the parent context bg unchanged. No prop needed. * - **No options** — returns the raw context value without resolution. * * @param options - Configuration for bg resolution diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index d7fe0aaf98..d28b7e8552 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -90,7 +90,7 @@ type DataAttributes = { [K in DataAttributeKeys as `data-${Lowercase< string & K >}`]?: string; -} & { 'data-bg'?: string }; +} & { 'data-bg'?: string; 'data-on-bg'?: string }; export type UtilityKeys> = D['utilityProps'] extends ReadonlyArray ? K : never; diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index 4de6d40443..e7a4caca97 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -72,9 +72,11 @@ export function useDefinition< } } - // Set data-bg from the resolved bg value (works for both container and leaf) + // Set the bg data attribute from the resolved bg value + // Containers use data-bg, leaf components use data-on-bg if (definition.bg && resolvedBg !== undefined) { - dataAttributes['data-bg'] = String(resolvedBg); + const attrName = definition.bg === 'leaf' ? 'data-on-bg' : 'data-bg'; + dataAttributes[attrName] = String(resolvedBg); } const { utilityClasses, utilityStyle } = processUtilityProps>( diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 9587dd1142..4b0b783656 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -184,16 +184,18 @@ export interface ComponentDefinition { /** * Background type for the neutral bg system. * - * Supports neutral levels ('neutral-1' through 'neutral-4') and + * 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 Bg = +export type ContainerBg = | 'neutral-1' | 'neutral-2' | 'neutral-3' - | 'neutral-4' | 'danger' | 'warning' | 'success'; From c0183daf874ec29573869ad16ee27a764c223b0f Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 9 Feb 2026 18:36:42 +0000 Subject: [PATCH 10/18] Update clean-bags-occur.md Signed-off-by: Charles de Dreuille --- .changeset/clean-bags-occur.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index b6e1a1c329..37325426e1 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -2,9 +2,9 @@ '@backstage/ui': minor --- -**BREAKING**: Replaced `Surface` / `onSurface` system with new `Bg` background system +**BREAKING**: Replaced `Surface` / `onSurface` system with new `ContainerBg` background system -The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a new `Bg` type with semantic neutral levels (`'neutral-1'` through `'neutral-4'`) and intents (`'danger'`, `'warning'`, `'success'`). Leaf components like Button no longer need an explicit prop — backgrounds auto-increment from parent context. +The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a new `ContainerBg` type with semantic neutral levels (`'neutral-1'` through `'neutral-3'`) and intents (`'danger'`, `'warning'`, `'success'`). Containers are capped at `neutral-3`; the `neutral-4` level is reserved for leaf component CSS only. Leaf components like Button no longer need an explicit prop — they receive a `data-on-bg` attribute matching the parent container's bg, and CSS handles the visual step-up. New `useBg` hook and `BgProvider` replace the deleted `useSurface` hook and `SurfaceProvider`. @@ -50,7 +50,7 @@ Update type imports: ```diff - import type { Surface, LeafSurfaceProps, ContainerSurfaceProps } from '@backstage/ui'; -+ import type { Bg } from '@backstage/ui'; ++ import type { ContainerBg } from '@backstage/ui'; ``` Replace hook usage in custom components: @@ -73,7 +73,9 @@ Update CSS selectors targeting surface data attributes: + [data-bg='neutral-1'] { ... } - [data-on-surface='2'] { ... } -+ [data-bg='neutral-2'] { ... } ++ [data-on-bg='neutral-1'] { ... } ``` +Note: Container components use `data-bg` for their own background level. Leaf components now use `data-on-bg` which reflects the parent container's bg (not an auto-incremented value). + **Affected components:** Box, Button, ButtonIcon, ButtonLink, ToggleButton, Card, Alert, Flex, Grid From 46a9adc246464ea0e21770ca882c949f88a341a8 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 10 Feb 2026 11:08:06 +0000 Subject: [PATCH 11/18] Improve docs + container props on Grid + Flex Signed-off-by: Charles de Dreuille --- .changeset/alert-remove-surface.md | 12 ++++ .changeset/clean-bags-occur.md | 17 ++---- .../ui/src/components/Flex/Flex.stories.tsx | 19 +++--- packages/ui/src/components/Flex/Flex.tsx | 7 ++- .../ui/src/components/Grid/Grid.stories.tsx | 11 ++-- packages/ui/src/components/Grid/Grid.tsx | 14 +++-- .../components/ToggleButton/ToggleButton.tsx | 4 -- packages/ui/src/hooks/useBg.tsx | 60 +++++++------------ 8 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 .changeset/alert-remove-surface.md diff --git a/.changeset/alert-remove-surface.md b/.changeset/alert-remove-surface.md new file mode 100644 index 0000000000..ed32bd38d2 --- /dev/null +++ b/.changeset/alert-remove-surface.md @@ -0,0 +1,12 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Alert no longer accepts a `surface` prop + +The Alert component's background is now driven entirely by its `status` prop. The `surface` prop has been removed. + +```diff +- ++ +``` diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index 37325426e1..d3623a9a9d 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -4,7 +4,7 @@ **BREAKING**: Replaced `Surface` / `onSurface` system with new `ContainerBg` background system -The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a new `ContainerBg` type with semantic neutral levels (`'neutral-1'` through `'neutral-3'`) and intents (`'danger'`, `'warning'`, `'success'`). Containers are capped at `neutral-3`; the `neutral-4` level is reserved for leaf component CSS only. Leaf components like Button no longer need an explicit prop — they receive a `data-on-bg` attribute matching the parent container's bg, and CSS handles the visual step-up. +The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by `ContainerBg` — a union of `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'`. There is no `neutral-4` value; containers are capped at `neutral-3`. Leaf components like Button no longer accept a bg prop — they inherit the parent container's bg via a `data-on-bg` attribute, and CSS handles the visual step-up to the next neutral level. New `useBg` hook and `BgProvider` replace the deleted `useSurface` hook and `SurfaceProvider`. @@ -26,7 +26,7 @@ Rename the `surface` prop to `bg` on container components and update values: + ``` -Remove `onSurface` from leaf components — it is now fully automatic: +Remove `onSurface` from leaf components — they now always inherit from the parent container and can no longer override the value: ```diff - - - - - - - + + + + + + - - -

Without bg — no context, no auto-increment
- - - - - - - - - -
-
+ + ), }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index 9d9c3b9417..7c1150c2dc 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -27,7 +27,7 @@ export const BoxDefinition = defineComponent()({ classNames: { root: 'bui-Box', }, - bg: 'container', + bg: { provider: true }, propDefs: { as: { default: 'div' }, bg: { dataAttribute: true }, diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index dabd56fc48..f7641d2f27 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -15,12 +15,12 @@ */ import type { ReactNode, CSSProperties } from 'react'; -import type { Responsive, ContainerBg, SpaceProps } from '../../types'; +import type { Responsive, ProviderBg, SpaceProps } from '../../types'; /** @public */ export type BoxOwnProps = { as?: keyof JSX.IntrinsicElements; - bg?: Responsive; + bg?: Responsive; children?: ReactNode; className?: string; style?: CSSProperties; diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index 6b6b49ab53..3007064a48 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent()({ content: 'bui-ButtonContent', spinner: 'bui-ButtonSpinner', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 03c2bd9387..3750a7f3d9 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -29,7 +29,7 @@ export const ButtonIconDefinition = defineComponent()({ content: 'bui-ButtonIconContent', spinner: 'bui-ButtonIconSpinner', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index 1e3727c60e..d528b90917 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -28,7 +28,7 @@ export const ButtonLinkDefinition = defineComponent()({ root: 'bui-ButtonLink', content: 'bui-ButtonLinkContent', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 1ca0b7a1b4..5450c1feb4 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -30,28 +30,16 @@ width: 100%; } - .bui-Card[data-bg='neutral-1'] { - background-color: var(--bui-bg-neutral-1); - } - - .bui-Card[data-bg='neutral-2'] { + .bui-Card[data-on-bg='neutral-1'] { background-color: var(--bui-bg-neutral-2); } - .bui-Card[data-bg='neutral-3'] { + .bui-Card[data-on-bg='neutral-2'] { background-color: var(--bui-bg-neutral-3); } - .bui-Card[data-bg='danger'] { - background-color: var(--bui-bg-danger); - } - - .bui-Card[data-bg='warning'] { - background-color: var(--bui-bg-warning); - } - - .bui-Card[data-bg='success'] { - background-color: var(--bui-bg-success); + .bui-Card[data-on-bg='neutral-3'] { + background-color: var(--bui-bg-neutral-4); } .bui-CardBody { diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 80dd0ad8d6..3957b7beff 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -132,41 +132,27 @@ export const Backgrounds = meta.story({ render: args => ( - Default - No bg prop - - - Neutral 1 - Explicit neutral-1 - - - Neutral 2 - Explicit neutral-2 - - - Neutral 3 - Explicit neutral-3 - - - Responsive - Neutral 1 → 2 - - - Danger - Bg danger - - - Warning - Bg warning - - - Success - Bg success + No parent + Defaults to neutral-1 + + + On neutral-1 + Auto-increments to neutral-2 + + + + + On neutral-2 + Auto-increments to neutral-3 + + + + + On neutral-3 + Steps up to neutral-4 + + ), }); @@ -175,22 +161,25 @@ export const BgNested = meta.story({ render: args => ( - 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. + Nested cards auto-increment their neutral level. Buttons inherit the + parent card's bg via data-on-bg. - - Neutral 1 + + Card (visual: neutral-1, provides: neutral-1) - - - Neutral 2 + + + + Card (visual: neutral-2, provides: neutral-2) + - + - Auto-incremented + + Card (visual: neutral-4, provides: neutral-3) + - + @@ -201,27 +190,56 @@ export const BgNested = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgOnProviders = meta.story({ render: args => ( + + No provider + Card defaults to neutral-1 + On neutral-1 - Card auto → neutral-2 + Card auto-increments to neutral-2 On neutral-2 - Card auto → neutral-3 + Card auto-increments to neutral-3 On neutral-3 - Card auto → neutral-4 + Card visually at neutral-4 ), }); + +export const CustomCardWithBox = meta.story({ + render: () => ( + + + 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. + + + + + + Header + Body + Footer + + + ), +}); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index 73dc1b02af..0fc56b482b 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -32,9 +32,8 @@ export const CardDefinition = defineComponent()({ classNames: { root: 'bui-Card', }, - bg: 'container', + bg: { provider: true, consumer: true, defaultBg: 'neutral-auto' }, propDefs: { - bg: { dataAttribute: true }, children: {}, className: {}, }, diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index 2b32bbf50a..e31b2cfda2 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -15,11 +15,9 @@ */ import type { ReactNode } from 'react'; -import type { Responsive, ContainerBg } from '../../types'; /** @public */ export type CardOwnProps = { - bg?: Responsive; children?: ReactNode; className?: string; }; diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 3ea311da0e..434655880c 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -277,7 +277,7 @@ export const Backgrounds = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgNeutralAuto = meta.story({ args: { px: '6', py: '4', gap: '4' }, render: args => ( diff --git a/packages/ui/src/components/Flex/Flex.tsx b/packages/ui/src/components/Flex/Flex.tsx index c4b8f9744c..a798a88012 100644 --- a/packages/ui/src/components/Flex/Flex.tsx +++ b/packages/ui/src/components/Flex/Flex.tsx @@ -20,15 +20,11 @@ import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; import { FlexDefinition } from './definition'; import styles from './Flex.module.css'; -import { BgProvider, useBg } from '../../hooks/useBg'; +import { BgProvider, useBgProvider } from '../../hooks/useBg'; /** @public */ export const Flex = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // Flex is a layout primitive — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(FlexDefinition, { diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index ad5404e41d..1f04a4818d 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Responsive, Space, SpaceProps, ContainerBg } 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; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 187a97dbbd..d5b83d257d 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -177,7 +177,7 @@ export const Backgrounds = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgNeutralAuto = meta.story({ args: { px: '6', py: '4', columns: '2', gap: '4' }, render: args => ( diff --git a/packages/ui/src/components/Grid/Grid.tsx b/packages/ui/src/components/Grid/Grid.tsx index 3e3ad7f300..d6e4cc2437 100644 --- a/packages/ui/src/components/Grid/Grid.tsx +++ b/packages/ui/src/components/Grid/Grid.tsx @@ -20,14 +20,10 @@ import type { GridItemProps, GridProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { GridDefinition, GridItemDefinition } from './definition'; import styles from './Grid.module.css'; -import { BgProvider, useBg } from '../../hooks/useBg'; +import { BgProvider, useBgProvider } from '../../hooks/useBg'; const GridRoot = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // Grid is a layout primitive — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(GridDefinition, { @@ -62,11 +58,7 @@ const GridRoot = forwardRef((props, ref) => { }); const GridItem = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // GridItem is a layout slot — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(GridItemDefinition, { diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index 4f279f24cb..4b33c657f3 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -19,7 +19,7 @@ import type { SpaceProps, Responsive, Columns, - ContainerBg, + ProviderBg, } from '../../types'; /** @public */ @@ -29,7 +29,7 @@ export interface GridProps extends SpaceProps { columns?: Responsive; gap?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } /** @public */ @@ -41,5 +41,5 @@ export interface GridItemProps { colStart?: Responsive; rowSpan?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/hooks/useBg.tsx b/packages/ui/src/hooks/useBg.tsx index 3cec51302b..0b999ee097 100644 --- a/packages/ui/src/hooks/useBg.tsx +++ b/packages/ui/src/hooks/useBg.tsx @@ -19,7 +19,7 @@ import { createVersionedContext, createVersionedValueMap, } from '@backstage/version-bridge'; -import { ContainerBg, Responsive } from '../types'; +import { ContainerBg, ProviderBg, Responsive } from '../types'; import { useBreakpoint } from './useBreakpoint'; import { resolveResponsiveValue } from './useDefinition/helpers'; @@ -34,26 +34,6 @@ export interface BgProviderProps { children: ReactNode; } -/** @public */ -export interface UseBgOptions { - /** - * The bg mode of the component. - * - * - `'container'` — for components like Box, Card, Flex that establish bg context. - * If `bg` prop is provided, uses that value. Otherwise auto-increments from parent, - * capping at `neutral-3`. - * - `'leaf'` — for components like Button that consume bg context. - * Returns the parent context bg unchanged (no increment). The leaf component's CSS - * handles the visual step-up. The `bg` prop is ignored. - */ - mode: 'container' | 'leaf'; - /** - * The explicit bg value from the component's prop. - * Only used in container mode — leaf mode ignores this. - */ - bg?: Responsive; -} - const BgContext = createVersionedContext<{ 1: BgContextValue; }>('bg-context'); @@ -62,15 +42,13 @@ const BgContext = createVersionedContext<{ * 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 leaf component CSS and is never - * set on containers. + * The 'neutral-4' level is reserved for consumer component CSS and is + * never set on providers. * * @internal */ -function incrementNeutralBg( - bg: ContainerBg | undefined, -): ContainerBg | undefined { - if (!bg) return undefined; +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 @@ -78,32 +56,6 @@ function incrementNeutralBg( return bg; } -/** - * Resolves the bg for a container component. - * - * Uses the explicit `bg` prop if provided. Otherwise auto-increments from - * the parent context, capping at `neutral-3`. Returns undefined when there - * is no prop and no parent context. - * - * @internal - */ -function resolveContainerBg( - context: BgContextValue, - propBg: ContainerBg | undefined, -): BgContextValue { - // Explicit bg prop takes priority - if (propBg !== undefined) { - return { bg: propBg }; - } - - // No explicit bg: auto-increment from context if available - if (context.bg === undefined) { - return { bg: undefined }; - } - - return { bg: incrementNeutralBg(context.bg) }; -} - /** * Provider component that establishes the bg context for child components. * @@ -118,36 +70,44 @@ export const BgProvider = ({ bg, children }: BgProviderProps) => { }; /** - * Hook to access and resolve the current bg context. + * Hook for consumer components (e.g. Button) to read the parent bg context. * - * - **Container mode** — uses explicit `bg` if provided, otherwise auto-increments - * from parent context. Caps at `neutral-3`. - * - **Leaf mode** — returns the parent context bg unchanged. No prop needed. - * - **No options** — returns the raw context value without resolution. + * 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`). * - * @param options - Configuration for bg resolution * @public */ -export const useBg = (options?: UseBgOptions): BgContextValue => { - const { breakpoint } = useBreakpoint(); +export function useBgConsumer(): BgContextValue { const value = useContext(BgContext)?.atVersion(1); - const context = value ?? { bg: undefined }; + return value ?? { bg: undefined }; +} - if (!options) { - return context; +/** + * Hook for provider components (e.g. Box, Card) to resolve and provide bg context. + * + * - `bg` is `undefined` -- transparent, no context change, returns `{ bg: undefined }` + * - `bg` is a `ContainerBg` value -- uses that value directly + * - `bg` is `'neutral-auto'` -- increments from the parent context, capping at `neutral-3` + * + * The caller is responsible for wrapping children with `BgProvider` when the + * resolved bg is defined. + * + * @public + */ +export function useBgProvider(bg?: Responsive): BgContextValue { + const { breakpoint } = useBreakpoint(); + const context = useBgConsumer(); + + if (bg === undefined) { + return { bg: undefined }; } - // Leaf mode: return the parent context bg unchanged. - // The leaf component's CSS handles the visual step-up. - if (options.mode === 'leaf') { - return context; + const resolved = resolveResponsiveValue(bg, breakpoint); + + if (resolved === 'neutral-auto') { + return { bg: incrementNeutralBg(context.bg) }; } - // Resolve responsive prop value to a scalar for the current breakpoint - const propBg = - options.bg !== undefined - ? resolveResponsiveValue(options.bg, breakpoint) - : undefined; - - return resolveContainerBg(context, propBg); -}; + return { bg: resolved }; +} diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index d28b7e8552..925172d160 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -27,6 +27,21 @@ export interface PropDefConfig { export type UtilityPropKey = keyof typeof utilityClassMap; +/** + * Configuration for how a component participates in the bg system. + * + * - `provider` — calls `useBgProvider`, sets `data-bg`, wraps children in `BgProvider` + * - `consumer` — calls `useBgConsumer`, sets `data-on-bg` + * - `defaultBg` — default bg value when no `bg` prop is provided (e.g. `'neutral-auto'`) + * + * A component can be provider-only, consumer-only, or both. + */ +export interface BgConfig { + provider?: boolean; + consumer?: boolean; + defaultBg?: string; +} + export interface ComponentConfig< P extends Record, S extends Record, @@ -36,19 +51,22 @@ export interface ComponentConfig< propDefs: { [K in keyof P]: PropDefConfig }; // readonly for compatibility with const inference from factory utilityProps?: readonly UtilityPropKey[]; - bg?: 'container' | 'leaf'; + bg?: BgConfig; } /** * 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) + * - Provider-only components must include 'bg' in their props + * - Provider+consumer components (e.g. Card) don't need a bg prop (they auto-increment) + * - Consumer-only components don't need a bg prop */ -export type BgPropsConstraint = BgMode extends 'container' - ? 'bg' extends keyof P +export type BgPropsConstraint = Bg extends { provider: true } + ? Bg extends { consumer: true } + ? {} // provider+consumer: bg prop is optional (auto-increment via defaultBg) + : 'bg' extends keyof P ? {} : { - __error: 'Bg container components must include bg in props type.'; + __error: 'Bg provider components must include bg in props type.'; } : {}; @@ -75,10 +93,11 @@ type ResolvedOwnProps< [K in keyof PropDefs & keyof P]: ResolvePropType; }; -type ChildrenProps = - BgMode extends 'container' - ? { bgChildren: ReactNode; children?: never } - : { children: ReactNode; bgChildren?: never }; +type ChildrenProps = Bg extends { + provider: true; +} + ? { bgChildren: ReactNode; children?: never } + : { children: ReactNode; bgChildren?: never }; type DataAttributeKeys = { [K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true } diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index e7a4caca97..15817a754c 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -17,7 +17,7 @@ import { ReactNode } from 'react'; import clsx from 'clsx'; import { useBreakpoint } from '../useBreakpoint'; -import { useBg, BgProvider } from '../useBg'; +import { useBgProvider, useBgConsumer, BgProvider } from '../useBg'; import { resolveResponsiveValue, processUtilityProps } from './helpers'; import type { ComponentConfig, @@ -36,9 +36,17 @@ export function useDefinition< ): UseDefinitionResult { const { breakpoint } = useBreakpoint(); - const { bg: resolvedBg } = useBg( - definition.bg ? { mode: definition.bg, bg: props.bg } : undefined, - ); + // Resolve the effective bg value: use the bg prop if provided, + // otherwise fall back to the defaultBg from the bg config + const effectiveBg = definition.bg?.provider + ? props.bg ?? definition.bg.defaultBg + : undefined; + + // Provider: resolve bg and provide context for children + const providerBg = useBgProvider(effectiveBg); + + // Consumer: read parent context bg + const consumerBg = useBgConsumer(); const ownPropKeys = new Set(Object.keys(definition.propDefs)); const utilityPropKeys = new Set(definition.utilityProps ?? []); @@ -65,6 +73,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); @@ -72,11 +83,18 @@ export function useDefinition< } } - // Set the bg data attribute from the resolved bg value - // Containers use data-bg, leaf components use data-on-bg - if (definition.bg && resolvedBg !== undefined) { - const attrName = definition.bg === 'leaf' ? 'data-on-bg' : 'data-bg'; - dataAttributes[attrName] = String(resolvedBg); + // Provider-only: set data-bg (provider+consumer components use data-on-bg instead) + if ( + definition.bg?.provider && + !definition.bg?.consumer && + 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>( @@ -101,9 +119,9 @@ export function useDefinition< let children: ReactNode | undefined; let bgChildren: ReactNode | undefined; - if (definition.bg === 'container') { - bgChildren = resolvedBg ? ( - {props.children} + if (definition.bg?.provider) { + bgChildren = providerBg.bg ? ( + {props.children} ) : ( props.children ); @@ -115,7 +133,7 @@ export function useDefinition< ownProps: { classes, ...ownPropsResolved, - ...(definition.bg === 'container' ? { bgChildren } : { children }), + ...(definition.bg?.provider ? { bgChildren } : { children }), }, restProps, dataAttributes, diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index dc920900f3..c9f2a3763c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -64,9 +64,5 @@ export * from './types'; // Hooks export { useBreakpoint } from './hooks/useBreakpoint'; -export { useBg, BgProvider } from './hooks/useBg'; -export type { - BgContextValue, - BgProviderProps, - UseBgOptions, -} from './hooks/useBg'; +export { useBgProvider, useBgConsumer, BgProvider } from './hooks/useBg'; +export type { BgContextValue, BgProviderProps } from './hooks/useBg'; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 4b0b783656..d3dfd996aa 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -199,3 +199,13 @@ export type ContainerBg = | 'danger' | 'warning' | 'success'; + +/** + * 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'; From ad510a5519fdade22917a0b7c1083bd5a7b2f37c Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 10 Feb 2026 16:58:09 +0000 Subject: [PATCH 14/18] Update clean-bags-occur.md Signed-off-by: Charles de Dreuille --- .changeset/clean-bags-occur.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index d0b675a25a..ca7cb06b1a 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -4,19 +4,19 @@ **BREAKING**: Replaced `Surface` / `onSurface` system with new provider/consumer background system -The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer bg architecture. +The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer `bg` architecture. **Types:** - `ContainerBg` — `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'` - `ProviderBg` — `ContainerBg | 'neutral-auto'` -There is no `neutral-4` prop value; containers are capped at `neutral-3`. Consumer components (e.g. Button) inherit the parent's bg via `data-on-bg`, and CSS handles the visual step-up. +There is no `neutral-4` prop value; containers are capped at `neutral-3`. Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. **Hooks:** -- `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no bg is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. -- `useBgConsumer()` — for consumer components. Returns the parent container's bg unchanged. +- `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no `bg` is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. +- `useBgConsumer()` — for consumer components. Returns the parent container's `bg` unchanged. - The old `useBg` hook and `UseBgOptions` interface have been removed. **Component roles:** From c67fb1730294c1c11e5b1b4390817e923b730116 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 11 Feb 2026 09:58:39 +0000 Subject: [PATCH 15/18] cleaned up bg props Signed-off-by: Charles de Dreuille --- .changeset/alert-remove-surface.md | 2 + .changeset/clean-bags-occur.md | 2 +- packages/ui/src/components/Box/Box.tsx | 4 +- packages/ui/src/components/Box/definition.ts | 2 +- .../ui/src/components/Button/definition.ts | 2 +- .../src/components/ButtonIcon/definition.ts | 2 +- .../src/components/ButtonLink/definition.ts | 2 +- .../ui/src/components/Card/Card.module.css | 13 ------ packages/ui/src/components/Card/Card.tsx | 15 +++++-- packages/ui/src/components/Card/definition.ts | 1 - packages/ui/src/hooks/useDefinition/types.ts | 43 +++++++------------ .../src/hooks/useDefinition/useDefinition.tsx | 32 ++++++-------- 12 files changed, 48 insertions(+), 72 deletions(-) diff --git a/.changeset/alert-remove-surface.md b/.changeset/alert-remove-surface.md index ed32bd38d2..fcdd9303e5 100644 --- a/.changeset/alert-remove-surface.md +++ b/.changeset/alert-remove-surface.md @@ -10,3 +10,5 @@ The Alert component's background is now driven entirely by its `status` prop. Th - + ``` + +**Affected components:** Alert diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index ca7cb06b1a..27986eee24 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -86,6 +86,6 @@ Update CSS selectors targeting surface data attributes: + [data-on-bg='neutral-1'] { ... } ``` -Note: Provider components use `data-bg` (values: `neutral-1` through `neutral-3`, plus intents). Consumer components use `data-on-bg`, which reflects the parent container's bg directly (no auto-increment). +Note: Provider components use `data-bg` (values: `neutral-1` through `neutral-3`, plus intents). Consumer components use `data-on-bg`, which reflects the parent container's `bg` directly. **Affected components:** Box, Button, ButtonIcon, ButtonLink, ToggleButton, Card, Flex, Grid diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index dc1a8521ea..fb87be9ae1 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -25,7 +25,7 @@ export const Box = forwardRef((props, ref) => { BoxDefinition, props, ); - const { classes, as, bgChildren } = ownProps; + const { classes, as, childrenWithBgProvider } = ownProps; return createElement( as, @@ -36,7 +36,7 @@ export const Box = forwardRef((props, ref) => { ...dataAttributes, ...restProps, }, - bgChildren, + childrenWithBgProvider, ); }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index 7c1150c2dc..3837dc8ea4 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -27,7 +27,7 @@ export const BoxDefinition = defineComponent()({ classNames: { root: 'bui-Box', }, - bg: { provider: true }, + bg: 'provider', propDefs: { as: { default: 'div' }, bg: { dataAttribute: true }, diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index 3007064a48..d15ceabd77 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent()({ content: 'bui-ButtonContent', spinner: 'bui-ButtonSpinner', }, - bg: { consumer: true }, + bg: 'consumer', propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 3750a7f3d9..76f6be68ce 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -29,7 +29,7 @@ export const ButtonIconDefinition = defineComponent()({ content: 'bui-ButtonIconContent', spinner: 'bui-ButtonIconSpinner', }, - bg: { consumer: true }, + bg: 'consumer', propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index d528b90917..c86cb026b5 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -28,7 +28,7 @@ export const ButtonLinkDefinition = defineComponent()({ root: 'bui-ButtonLink', content: 'bui-ButtonLinkContent', }, - bg: { consumer: true }, + bg: 'consumer', propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 5450c1feb4..445873f6ba 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -21,7 +21,6 @@ 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); @@ -30,18 +29,6 @@ width: 100%; } - .bui-Card[data-on-bg='neutral-1'] { - background-color: var(--bui-bg-neutral-2); - } - - .bui-Card[data-on-bg='neutral-2'] { - background-color: var(--bui-bg-neutral-3); - } - - .bui-Card[data-on-bg='neutral-3'] { - background-color: var(--bui-bg-neutral-4); - } - .bui-CardBody { flex: 1; min-height: 0; diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index 4e030fcc4c..e3e4781eac 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -28,6 +28,7 @@ import type { CardBodyProps, CardFooterProps, } from './types'; +import { Box } from '../Box/Box'; /** * Card component. @@ -39,12 +40,18 @@ export const Card = forwardRef((props, ref) => { CardDefinition, props, ); - const { classes, bgChildren } = ownProps; + const { classes, children } = ownProps; return ( -
- {bgChildren} -
+ + {children} + ); }); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index 0fc56b482b..6493d01839 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -32,7 +32,6 @@ export const CardDefinition = defineComponent()({ classNames: { root: 'bui-Card', }, - bg: { provider: true, consumer: true, defaultBg: 'neutral-auto' }, propDefs: { children: {}, className: {}, diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index 925172d160..91ebdb8ef5 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -27,21 +27,6 @@ export interface PropDefConfig { export type UtilityPropKey = keyof typeof utilityClassMap; -/** - * Configuration for how a component participates in the bg system. - * - * - `provider` — calls `useBgProvider`, sets `data-bg`, wraps children in `BgProvider` - * - `consumer` — calls `useBgConsumer`, sets `data-on-bg` - * - `defaultBg` — default bg value when no `bg` prop is provided (e.g. `'neutral-auto'`) - * - * A component can be provider-only, consumer-only, or both. - */ -export interface BgConfig { - provider?: boolean; - consumer?: boolean; - defaultBg?: string; -} - export interface ComponentConfig< P extends Record, S extends Record, @@ -51,19 +36,22 @@ export interface ComponentConfig< propDefs: { [K in keyof P]: PropDefConfig }; // readonly for compatibility with const inference from factory utilityProps?: readonly UtilityPropKey[]; - bg?: BgConfig; + /** + * 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 bg props are present in the props type. - * - Provider-only components must include 'bg' in their props - * - Provider+consumer components (e.g. Card) don't need a bg prop (they auto-increment) - * - Consumer-only components don't need a bg prop + * - Provider components must include 'bg' in their props + * - Consumer components don't need a bg prop */ -export type BgPropsConstraint = Bg extends { provider: true } - ? Bg extends { consumer: true } - ? {} // provider+consumer: bg prop is optional (auto-increment via defaultBg) - : 'bg' extends keyof P +export type BgPropsConstraint = Bg extends 'provider' + ? 'bg' extends keyof P ? {} : { __error: 'Bg provider components must include bg in props type.'; @@ -93,11 +81,10 @@ type ResolvedOwnProps< [K in keyof PropDefs & keyof P]: ResolvePropType; }; -type ChildrenProps = Bg extends { - provider: true; -} - ? { bgChildren: ReactNode; children?: never } - : { children: ReactNode; bgChildren?: never }; +type ChildrenProps = + Bg extends 'provider' + ? { childrenWithBgProvider: ReactNode; children?: never } + : { children: ReactNode; childrenWithBgProvider?: never }; type DataAttributeKeys = { [K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true } diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index 15817a754c..d1fe9af96c 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -36,14 +36,10 @@ export function useDefinition< ): UseDefinitionResult { const { breakpoint } = useBreakpoint(); - // Resolve the effective bg value: use the bg prop if provided, - // otherwise fall back to the defaultBg from the bg config - const effectiveBg = definition.bg?.provider - ? props.bg ?? definition.bg.defaultBg - : undefined; - // Provider: resolve bg and provide context for children - const providerBg = useBgProvider(effectiveBg); + const providerBg = useBgProvider( + definition.bg === 'provider' ? props.bg : undefined, + ); // Consumer: read parent context bg const consumerBg = useBgConsumer(); @@ -74,7 +70,7 @@ export function useDefinition< ownPropsResolved[key] = finalValue; // Skip data-bg for bg prop when the provider path handles it - if (key === 'bg' && definition.bg?.provider) continue; + if (key === 'bg' && definition.bg === 'provider') continue; if ((config as any).dataAttribute) { // eslint-disable-next-line no-restricted-syntax @@ -83,17 +79,13 @@ export function useDefinition< } } - // Provider-only: set data-bg (provider+consumer components use data-on-bg instead) - if ( - definition.bg?.provider && - !definition.bg?.consumer && - providerBg.bg !== undefined - ) { + // 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) { + if (definition.bg === 'consumer' && consumerBg.bg !== undefined) { dataAttributes['data-on-bg'] = String(consumerBg.bg); } @@ -117,10 +109,10 @@ export function useDefinition< } let children: ReactNode | undefined; - let bgChildren: ReactNode | undefined; + let childrenWithBgProvider: ReactNode | undefined; - if (definition.bg?.provider) { - bgChildren = providerBg.bg ? ( + if (definition.bg === 'provider') { + childrenWithBgProvider = providerBg.bg ? ( {props.children} ) : ( props.children @@ -133,7 +125,9 @@ export function useDefinition< ownProps: { classes, ...ownPropsResolved, - ...(definition.bg?.provider ? { bgChildren } : { children }), + ...(definition.bg === 'provider' + ? { childrenWithBgProvider } + : { children }), }, restProps, dataAttributes, From 53df9549ee409837796c303b34efa6a4d8ee28bf Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 11 Feb 2026 10:07:08 +0000 Subject: [PATCH 16/18] Update Flex.stories.tsx Signed-off-by: Charles de Dreuille --- .../ui/src/components/Flex/Flex.stories.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 434655880c..8b3b7af220 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -282,16 +282,16 @@ export const BgNeutralAuto = meta.story({ render: args => (
- Flex is a layout primitive and is transparent to the bg system by - default. Only an explicit bg prop establishes a new bg level. Nested - Flex components without a bg prop inherit the parent context unchanged. + 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.
- -
Neutral 1 (explicit)
- -
Neutral 2 (explicit)
- -
Neutral 3 (explicit, capped)
+ +
Neutral 1 (auto, no parent)
+ +
Neutral 2 (auto-incremented)
+ +
Neutral 3 (auto-incremented, capped)
From 0fe7bf12bec104c979d36b1d4863ac06c10d0401 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 11 Feb 2026 10:17:50 +0000 Subject: [PATCH 17/18] Update report.api.md Signed-off-by: Charles de Dreuille --- packages/ui/report.api.md | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index f6138c3c3d..4d09023b69 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -249,9 +249,7 @@ export const BoxDefinition: { readonly classNames: { readonly root: 'bui-Box'; }; - readonly bg: { - readonly provider: true; - }; + readonly bg: 'provider'; readonly propDefs: { readonly as: { readonly default: 'div'; @@ -337,9 +335,7 @@ export const ButtonDefinition: { readonly content: 'bui-ButtonContent'; readonly spinner: 'bui-ButtonSpinner'; }; - readonly bg: { - readonly consumer: true; - }; + readonly bg: 'consumer'; readonly propDefs: { readonly size: { readonly dataAttribute: true; @@ -378,9 +374,7 @@ export const ButtonIconDefinition: { readonly content: 'bui-ButtonIconContent'; readonly spinner: 'bui-ButtonIconSpinner'; }; - readonly bg: { - readonly consumer: true; - }; + readonly bg: 'consumer'; readonly propDefs: { readonly size: { readonly dataAttribute: true; @@ -428,9 +422,7 @@ export const ButtonLinkDefinition: { readonly root: 'bui-ButtonLink'; readonly content: 'bui-ButtonLinkContent'; }; - readonly bg: { - readonly consumer: true; - }; + readonly bg: 'consumer'; readonly propDefs: { readonly size: { readonly dataAttribute: true; @@ -525,11 +517,6 @@ export const CardDefinition: { readonly classNames: { readonly root: 'bui-Card'; }; - readonly bg: { - readonly provider: true; - readonly consumer: true; - readonly defaultBg: 'neutral-auto'; - }; readonly propDefs: { readonly children: {}; readonly className: {}; From 048d9c47d49bcb1173b161fb1b597547507e77bb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 11 Feb 2026 10:36:32 +0000 Subject: [PATCH 18/18] Cleanup Signed-off-by: Charles de Dreuille --- .changeset/clean-bags-occur.md | 15 +++++++++------ packages/ui/src/hooks/useBg.tsx | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index 27986eee24..ea4ae25aa6 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -11,19 +11,22 @@ The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surf - `ContainerBg` — `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'` - `ProviderBg` — `ContainerBg | 'neutral-auto'` -There is no `neutral-4` prop value; containers are capped at `neutral-3`. Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. +Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. See "Neutral level capping" below for details on how levels are bounded. **Hooks:** - `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no `bg` is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. - `useBgConsumer()` — for consumer components. Returns the parent container's `bg` unchanged. -- The old `useBg` hook and `UseBgOptions` interface have been removed. **Component roles:** -- **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. Transparent by default. -- **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit from parent. -- **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Defaults to `neutral-auto`. +- **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. **Transparent by default** — they do _not_ auto-increment; pass `bg="neutral-auto"` explicitly if you want automatic neutral stepping. +- **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit the parent container's `bg` unchanged. +- **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Card passes `bg="neutral-auto"` to its inner Box, so it auto-increments from the parent context. + +**Neutral level capping:** + +Provider components cap at `neutral-3`. There is no `neutral-4` prop value. The `neutral-4` level exists only in consumer component CSS — for example, a Button sitting on a `neutral-3` surface uses `neutral-4` tokens internally via `data-on-bg`. **Migration:** @@ -86,6 +89,6 @@ Update CSS selectors targeting surface data attributes: + [data-on-bg='neutral-1'] { ... } ``` -Note: Provider components use `data-bg` (values: `neutral-1` through `neutral-3`, plus intents). Consumer components use `data-on-bg`, which reflects the parent container's `bg` directly. +Note: Provider components use `data-bg` (values: `neutral-1` through `neutral-3`, plus intent values). Consumer components use `data-on-bg`, which reflects the parent container's `bg` directly. The `neutral-4` level never appears as a prop or `data-bg` value — it is used only in consumer CSS. **Affected components:** Box, Button, ButtonIcon, ButtonLink, ToggleButton, Card, Flex, Grid diff --git a/packages/ui/src/hooks/useBg.tsx b/packages/ui/src/hooks/useBg.tsx index 0b999ee097..6e298fedf9 100644 --- a/packages/ui/src/hooks/useBg.tsx +++ b/packages/ui/src/hooks/useBg.tsx @@ -86,9 +86,20 @@ export function useBgConsumer(): BgContextValue { /** * Hook for provider components (e.g. Box, Card) to resolve and provide bg context. * - * - `bg` is `undefined` -- transparent, no context change, returns `{ bg: undefined }` - * - `bg` is a `ContainerBg` value -- uses that value directly - * - `bg` is `'neutral-auto'` -- increments from the parent context, capping at `neutral-3` + * **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.