Improve leaf props for background

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-09 18:34:11 +00:00
parent 576d1d3902
commit 2899ed0241
26 changed files with 89 additions and 168 deletions
+17 -21
View File
@@ -205,20 +205,10 @@ 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: Bg | undefined;
bg: ContainerBg | undefined;
}
// @public
@@ -227,7 +217,7 @@ export const BgProvider: ({ bg, children }: BgProviderProps) => JSX_2.Element;
// @public (undocumented)
export interface BgProviderProps {
// (undocumented)
bg: Bg;
bg: ContainerBg;
// (undocumented)
children: ReactNode;
}
@@ -300,7 +290,7 @@ export const BoxDefinition: {
// @public (undocumented)
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -599,7 +589,7 @@ export interface CardHeaderProps
// @public (undocumented)
export type CardOwnProps = {
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
children?: ReactNode;
className?: string;
};
@@ -747,6 +737,15 @@ export const Container: ForwardRefExoticComponent<
ContainerProps & RefAttributes<HTMLDivElement>
>;
// @public
export type ContainerBg =
| 'neutral-1'
| 'neutral-2'
| 'neutral-3'
| 'danger'
| 'warning'
| 'success';
// @public
export const ContainerDefinition: {
readonly classNames: {
@@ -952,7 +951,6 @@ export const FlexDefinition: {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -968,7 +966,7 @@ export interface FlexProps extends SpaceProps {
// (undocumented)
align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>;
// (undocumented)
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -1022,7 +1020,6 @@ export const GridDefinition: {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -1041,7 +1038,6 @@ export const GridItemDefinition: {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -1052,7 +1048,7 @@ export const GridItemDefinition: {
// @public (undocumented)
export interface GridItemProps {
// (undocumented)
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -1072,7 +1068,7 @@ export interface GridItemProps {
// @public (undocumented)
export interface GridProps extends SpaceProps {
// (undocumented)
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
@@ -2182,7 +2178,7 @@ export const useBg: (options?: UseBgOptions) => BgContextValue;
// @public (undocumented)
export interface UseBgOptions {
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
mode: 'container' | 'leaf';
}
@@ -318,14 +318,6 @@ export const OnDifferentBackgrounds = meta.story({
<Alert status="success" icon={true} title="Alert on neutral-3" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 4</Text>
<Flex direction="column" gap="2" bg="neutral-4" p="4">
<Alert status="info" icon={true} title="Alert on neutral-4" />
<Alert status="success" icon={true} title="Alert on neutral-4" />
</Flex>
</Flex>
</Flex>
),
});
@@ -35,10 +35,6 @@
background-color: var(--bui-bg-neutral-3);
}
.bui-Box[data-bg='neutral-4'] {
background-color: var(--bui-bg-neutral-4);
}
.bui-Box[data-bg='danger'] {
background-color: var(--bui-bg-danger);
}
+2 -2
View File
@@ -15,12 +15,12 @@
*/
import type { ReactNode, CSSProperties } from 'react';
import type { Responsive, Bg, SpaceProps } from '../../types';
import type { Responsive, ContainerBg, SpaceProps } from '../../types';
/** @public */
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -118,19 +118,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);
@@ -182,17 +182,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);
}
@@ -150,29 +150,6 @@ export const Variants = meta.story({
</Button>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>Neutral 4</Text>
<Flex align="center" bg="neutral-4" p="4">
<Button iconStart={<RiCloudLine />} variant="primary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary">
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
Button
</Button>
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
Button
</Button>
</Flex>
</Flex>
</Flex>
),
});
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -147,10 +147,6 @@ export const Backgrounds = meta.story({
<CardHeader>Neutral 3</CardHeader>
<CardBody>Explicit neutral-3</CardBody>
</Card>
<Card {...args} bg="neutral-4" style={{ width: '200px' }}>
<CardHeader>Neutral 4</CardHeader>
<CardBody>Explicit neutral-4</CardBody>
</Card>
<Card
{...args}
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
@@ -226,12 +222,6 @@ export const BgAutoIncrement = meta.story({
<CardBody>Card auto neutral-4</CardBody>
</Card>
</Box>
<Box bg="neutral-4" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-4</CardHeader>
<CardBody>Card auto neutral-4 (capped)</CardBody>
</Card>
</Box>
</Flex>
),
});
+2 -2
View File
@@ -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>;
bg?: Responsive<ContainerBg>;
children?: ReactNode;
className?: string;
};
@@ -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);
}
@@ -261,9 +261,6 @@ export const Backgrounds = meta.story({
<Flex bg="neutral-3" {...args}>
Neutral 3
</Flex>
<Flex bg="neutral-4" {...args}>
Neutral 4
</Flex>
<Flex bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Bg
</Flex>
@@ -49,7 +49,6 @@ export const FlexDefinition = {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
+2 -2
View File
@@ -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>;
bg?: Responsive<ContainerBg>;
}
@@ -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);
@@ -120,9 +120,6 @@ export const Backgrounds = meta.story({
<Grid.Root {...args} bg="neutral-3">
Neutral 3
</Grid.Root>
<Grid.Root {...args} bg="neutral-4">
Neutral 4
</Grid.Root>
<Grid.Root {...args} bg={{ initial: 'neutral-1', sm: 'neutral-2' }}>
Responsive Bg
</Grid.Root>
@@ -152,11 +149,6 @@ export const Backgrounds = meta.story({
Neutral 3
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item bg="neutral-4" style={{ padding: '4px' }}>
Neutral 4
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
@@ -47,7 +47,6 @@ export const GridDefinition = {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
@@ -69,7 +68,6 @@ export const GridItemDefinition = {
'neutral-1',
'neutral-2',
'neutral-3',
'neutral-4',
'danger',
'warning',
'success',
+9 -3
View File
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import type { Space, SpaceProps, Responsive, Columns, Bg } from '../../types';
import type {
Space,
SpaceProps,
Responsive,
Columns,
ContainerBg,
} from '../../types';
/** @public */
export interface GridProps extends SpaceProps {
@@ -23,7 +29,7 @@ export interface GridProps extends SpaceProps {
columns?: Responsive<Columns>;
gap?: Responsive<Space>;
style?: React.CSSProperties;
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
}
/** @public */
@@ -35,5 +41,5 @@ export interface GridItemProps {
colStart?: Responsive<Columns>;
rowSpan?: Responsive<Columns>;
style?: React.CSSProperties;
bg?: Responsive<Bg>;
bg?: Responsive<ContainerBg>;
}
@@ -81,12 +81,6 @@ export const Backgrounds = meta.story({
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 4</Text>
<Flex align="center" bg="neutral-4" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
</Flex>
),
});
@@ -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 => {
@@ -137,19 +137,6 @@ export const Backgrounds = meta.story({
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 4</Text>
<Flex align="center" bg="neutral-4" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
</Flex>
),
});
+28 -22
View File
@@ -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>;
bg?: Responsive<ContainerBg>;
}
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
+1 -1
View File
@@ -90,7 +90,7 @@ type DataAttributes<PropDefs> = {
[K in DataAttributeKeys<PropDefs> as `data-${Lowercase<
string & K
>}`]?: string;
} & { 'data-bg'?: string };
} & { 'data-bg'?: string; 'data-on-bg'?: string };
export type UtilityKeys<D extends ComponentConfig<any, any>> =
D['utilityProps'] extends ReadonlyArray<infer K extends string> ? K : never;
@@ -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<UtilityKeys<D>>(
+5 -3
View File
@@ -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';