feat(ui): simplify neutral bg prop to single auto-incrementing 'neutral' value

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-25 12:53:35 +00:00
parent e1c87b33ca
commit a27dc7795d
13 changed files with 237 additions and 186 deletions
@@ -195,7 +195,7 @@ export const AutoBg = meta.story({
</AccordionPanel>
</Accordion>
</Flex>
<Box bg="neutral-1" p="4">
<Box bg="neutral" p="4">
<Text>Neutral 1 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
@@ -210,35 +210,41 @@ export const AutoBg = meta.story({
</Accordion>
</Flex>
</Box>
<Box bg="neutral-2" p="4">
<Text>Neutral 2 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-3)" />
<AccordionPanel>
<Content />
<Flex mt="3" gap="2">
<Button>Action</Button>
<Button variant="secondary">Cancel</Button>
</Flex>
</AccordionPanel>
</Accordion>
</Flex>
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 2 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-3)" />
<AccordionPanel>
<Content />
<Flex mt="3" gap="2">
<Button>Action</Button>
<Button variant="secondary">Cancel</Button>
</Flex>
</AccordionPanel>
</Accordion>
</Flex>
</Box>
</Box>
<Box bg="neutral-3" p="4">
<Text>Neutral 3 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-3)" />
<AccordionPanel>
<Content />
<Flex mt="3" gap="2">
<Button>Action</Button>
<Button variant="secondary">Cancel</Button>
</Flex>
</AccordionPanel>
</Accordion>
</Flex>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 3 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-3)" />
<AccordionPanel>
<Content />
<Flex mt="3" gap="2">
<Button>Action</Button>
<Button variant="secondary">Cancel</Button>
</Flex>
</AccordionPanel>
</Accordion>
</Flex>
</Box>
</Box>
</Box>
</Flex>
),
@@ -34,7 +34,7 @@ export const AccordionDefinition = defineComponent<AccordionOwnProps>()({
},
bg: 'provider',
propDefs: {
bg: { dataAttribute: true, default: 'neutral-auto' },
bg: { dataAttribute: true, default: 'neutral' },
children: {},
className: {},
},
@@ -297,7 +297,7 @@ export const OnDifferentBackgrounds = meta.story({
<Flex direction="column" gap="4">
<Text>On Neutral 1</Text>
<Flex direction="column" gap="2" bg="neutral-1" p="4">
<Flex direction="column" gap="2" bg="neutral" p="4">
<Alert status="info" icon={true} title="Alert on neutral-1" />
<Alert status="success" icon={true} title="Alert on neutral-1" />
</Flex>
@@ -305,18 +305,24 @@ export const OnDifferentBackgrounds = meta.story({
<Flex direction="column" gap="4">
<Text>On Neutral 2</Text>
<Flex direction="column" gap="2" bg="neutral-2" p="4">
<Alert status="info" icon={true} title="Alert on neutral-2" />
<Alert status="success" icon={true} title="Alert on neutral-2" />
</Flex>
<Box bg="neutral">
<Flex direction="column" gap="2" bg="neutral" p="4">
<Alert status="info" icon={true} title="Alert on neutral-2" />
<Alert status="success" icon={true} title="Alert on neutral-2" />
</Flex>
</Box>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 3</Text>
<Flex direction="column" gap="2" bg="neutral-3" p="4">
<Alert status="info" icon={true} title="Alert on neutral-3" />
<Alert status="success" icon={true} title="Alert on neutral-3" />
</Flex>
<Box bg="neutral">
<Box bg="neutral">
<Flex direction="column" gap="2" bg="neutral" p="4">
<Alert status="info" icon={true} title="Alert on neutral-3" />
<Alert status="success" icon={true} title="Alert on neutral-3" />
</Flex>
</Box>
</Box>
</Flex>
</Flex>
),
@@ -337,7 +343,7 @@ export const Responsive = meta.story({
export const WithUtilityProps = meta.story({
render: () => (
<Box bg="neutral-1" py="4">
<Box bg="neutral" py="4">
<Alert
status="success"
icon={true}
+15 -12
View File
@@ -353,17 +353,20 @@ export const BackgroundColors = meta.story({
render: args => (
<Flex align="center" style={{ flexWrap: 'wrap' }}>
<Box {...args}>Default</Box>
<Box bg="neutral-1" {...args}>
Neutral 1
<Box bg="neutral" {...args}>
Neutral (level 1)
</Box>
<Box bg="neutral-2" {...args}>
Neutral 2
<Box bg="neutral">
<Box bg="neutral" {...args}>
Neutral (level 2)
</Box>
</Box>
<Box bg="neutral-3" {...args}>
Neutral 3
</Box>
<Box bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Neutral
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" {...args}>
Neutral (level 3)
</Box>
</Box>
</Box>
<Box bg="danger" {...args}>
Danger
@@ -381,11 +384,11 @@ export const BackgroundColors = meta.story({
export const NestedNeutralColors = meta.story({
args: { px: '6', py: '4', children: null },
render: args => (
<Box {...args} bg="neutral-1">
<Box {...args} bg="neutral">
<Button variant="secondary">Button (on neutral-1)</Button>
<Box {...args} bg="neutral-2" mt="4">
<Box {...args} bg="neutral" mt="4">
<Button variant="secondary">Button (on neutral-2)</Button>
<Box {...args} bg="neutral-3" mt="4">
<Box {...args} bg="neutral" mt="4">
<Button variant="secondary">Button (on neutral-3)</Button>
</Box>
</Box>
@@ -357,26 +357,32 @@ export const AutoBg = meta.story({
neutral level by 1. No prop is needed on the button -- it's fully
automatic.
</div>
<Box bg="neutral-1" p="4">
<Box bg="neutral" p="4">
<Text>Neutral 1 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Auto (neutral-2)</Button>
<Button variant="tertiary">Auto (neutral-2)</Button>
</Flex>
</Box>
<Box bg="neutral-2" p="4">
<Text>Neutral 2 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Auto (neutral-3)</Button>
<Button variant="tertiary">Auto (neutral-3)</Button>
</Flex>
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 2 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Auto (neutral-3)</Button>
<Button variant="tertiary">Auto (neutral-3)</Button>
</Flex>
</Box>
</Box>
<Box bg="neutral-3" p="4">
<Text>Neutral 3 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Auto (neutral-4)</Button>
<Button variant="tertiary">Auto (neutral-4)</Button>
</Flex>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 3 container</Text>
<Flex gap="2" mt="2">
<Button variant="secondary">Auto (neutral-4)</Button>
<Button variant="tertiary">Auto (neutral-4)</Button>
</Flex>
</Box>
</Box>
</Box>
</Flex>
),
@@ -135,23 +135,29 @@ export const Backgrounds = meta.story({
<CardHeader>No parent</CardHeader>
<CardBody>Defaults to neutral-1</CardBody>
</Card>
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-1</CardHeader>
<CardBody>Auto-increments to neutral-2</CardBody>
</Card>
</Box>
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Auto-increments to neutral-3</CardBody>
</Card>
<Box bg="neutral">
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Auto-increments to neutral-3</CardBody>
</Card>
</Box>
</Box>
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Steps up to neutral-4</CardBody>
</Card>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Steps up to neutral-4</CardBody>
</Card>
</Box>
</Box>
</Box>
</Flex>
),
@@ -197,23 +203,29 @@ export const BgOnProviders = meta.story({
<CardHeader>No provider</CardHeader>
<CardBody>Card defaults to neutral-1</CardBody>
</Card>
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-1</CardHeader>
<CardBody>Card auto-increments to neutral-2</CardBody>
</Card>
</Box>
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Card auto-increments to neutral-3</CardBody>
</Card>
<Box bg="neutral">
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-2</CardHeader>
<CardBody>Card auto-increments to neutral-3</CardBody>
</Card>
</Box>
</Box>
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Card visually at neutral-4</CardBody>
</Card>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4" style={{ borderRadius: '8px' }}>
<Card {...args} style={{ width: '200px' }}>
<CardHeader>On neutral-3</CardHeader>
<CardBody>Card visually at neutral-4</CardBody>
</Card>
</Box>
</Box>
</Box>
</Flex>
),
@@ -226,11 +238,7 @@ export const CustomCardWithBox = meta.story({
A custom card built with Box. Use Box with an explicit bg prop to create
a card-like container that participates in the bg system as a provider.
</Box>
<Box
bg="neutral-auto"
p="4"
style={{ borderRadius: '8px', width: '300px' }}
>
<Box bg="neutral" p="4" style={{ borderRadius: '8px', width: '300px' }}>
<Button variant="secondary" style={{ marginTop: '8px' }}>
Button (on neutral-1)
</Button>
+1 -1
View File
@@ -44,7 +44,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
return (
<Box
bg="neutral-auto"
bg="neutral"
ref={ref}
className={classes.root}
{...dataAttributes}
@@ -253,18 +253,21 @@ export const Backgrounds = meta.story({
render: args => (
<Flex align="center" style={{ flexWrap: 'wrap' }}>
<Flex {...args}>Default</Flex>
<Flex bg="neutral-1" {...args}>
Neutral 1
</Flex>
<Flex bg="neutral-2" {...args}>
Neutral 2
</Flex>
<Flex bg="neutral-3" {...args}>
Neutral 3
</Flex>
<Flex bg={{ initial: 'neutral-1', sm: 'neutral-2' }} {...args}>
Responsive Bg
<Flex bg="neutral" {...args}>
Neutral (level 1)
</Flex>
<Box bg="neutral">
<Flex bg="neutral" {...args}>
Neutral (level 2)
</Flex>
</Box>
<Box bg="neutral">
<Box bg="neutral">
<Flex bg="neutral" {...args}>
Neutral (level 3)
</Flex>
</Box>
</Box>
<Flex bg="danger" {...args}>
Danger
</Flex>
@@ -278,20 +281,20 @@ export const Backgrounds = meta.story({
),
});
export const BgNeutralAuto = meta.story({
export const BgNeutral = meta.story({
args: { px: '6', py: '4', gap: '4' },
render: args => (
<Flex direction="column">
<div style={{ maxWidth: '600px', marginBottom: '16px' }}>
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
Using bg="neutral" on Flex auto-increments from the parent context. The
first Flex defaults to neutral-1 (no parent), then each nested Flex
increments by one, capping at neutral-3.
</div>
<Flex {...args} bg="neutral-auto" direction="column">
<div>Neutral 1 (auto, no parent)</div>
<Flex {...args} bg="neutral-auto" direction="column">
<Flex {...args} bg="neutral" direction="column">
<div>Neutral 1 (no parent)</div>
<Flex {...args} bg="neutral" direction="column">
<div>Neutral 2 (auto-incremented)</div>
<Flex {...args} bg="neutral-auto" direction="column">
<Flex {...args} bg="neutral" direction="column">
<div>Neutral 3 (auto-incremented, capped)</div>
</Flex>
</Flex>
@@ -113,18 +113,21 @@ export const Backgrounds = meta.story({
render: args => (
<Flex direction="column">
<Flex style={{ flexWrap: 'wrap' }}>
<Grid.Root {...args} bg="neutral-1">
Neutral 1
</Grid.Root>
<Grid.Root {...args} bg="neutral-2">
Neutral 2
</Grid.Root>
<Grid.Root {...args} bg="neutral-3">
Neutral 3
</Grid.Root>
<Grid.Root {...args} bg={{ initial: 'neutral-1', sm: 'neutral-2' }}>
Responsive Bg
<Grid.Root {...args} bg="neutral">
Neutral (level 1)
</Grid.Root>
<Box bg="neutral">
<Grid.Root {...args} bg="neutral">
Neutral (level 2)
</Grid.Root>
</Box>
<Box bg="neutral">
<Box bg="neutral">
<Grid.Root {...args} bg="neutral">
Neutral (level 3)
</Grid.Root>
</Box>
</Box>
<Grid.Root {...args} bg="danger">
Danger
</Grid.Root>
@@ -137,28 +140,26 @@ export const Backgrounds = meta.story({
</Flex>
<Flex style={{ flexWrap: 'wrap' }}>
<Grid.Root {...args}>
<Grid.Item bg="neutral-1" style={{ padding: '4px' }}>
Neutral 1
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item bg="neutral-2" style={{ padding: '4px' }}>
Neutral 2
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item bg="neutral-3" style={{ padding: '4px' }}>
Neutral 3
</Grid.Item>
</Grid.Root>
<Grid.Root {...args}>
<Grid.Item
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
style={{ padding: '4px' }}
>
Responsive Bg
<Grid.Item bg="neutral" style={{ padding: '4px' }}>
Neutral (level 1)
</Grid.Item>
</Grid.Root>
<Box bg="neutral">
<Grid.Root {...args}>
<Grid.Item bg="neutral" style={{ padding: '4px' }}>
Neutral (level 2)
</Grid.Item>
</Grid.Root>
</Box>
<Box bg="neutral">
<Box bg="neutral">
<Grid.Root {...args}>
<Grid.Item bg="neutral" style={{ padding: '4px' }}>
Neutral (level 3)
</Grid.Item>
</Grid.Root>
</Box>
</Box>
<Grid.Root {...args}>
<Grid.Item bg="danger" style={{ padding: '4px' }}>
Danger
@@ -179,7 +180,7 @@ export const Backgrounds = meta.story({
),
});
export const BgNeutralAuto = meta.story({
export const BgNeutral = meta.story({
args: { px: '6', py: '4', columns: '2', gap: '4' },
render: args => (
<Flex direction="column">
@@ -188,12 +189,12 @@ export const BgNeutralAuto = meta.story({
default. Only an explicit bg prop establishes a new bg level. Nested
grids without a bg prop inherit the parent context unchanged.
</div>
<Grid.Root {...args} bg="neutral-1">
<Grid.Root {...args} bg="neutral">
<Grid.Item>Neutral 1 (Grid.Root)</Grid.Item>
<Grid.Item>
<Grid.Root {...args} bg="neutral-2">
<Grid.Item>Nested: neutral-2 (explicit)</Grid.Item>
<Grid.Item>Nested: neutral-2 (explicit)</Grid.Item>
<Grid.Root {...args} bg="neutral">
<Grid.Item>Nested: neutral-2 (auto-incremented)</Grid.Item>
<Grid.Item>Nested: neutral-2 (auto-incremented)</Grid.Item>
</Grid.Root>
</Grid.Item>
</Grid.Root>
@@ -16,6 +16,7 @@
import preview from '../../../../../.storybook/preview';
import { ToggleButton } from './ToggleButton';
import { Box } from '../Box';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { useState } from 'react';
@@ -65,21 +66,27 @@ export const Backgrounds = meta.story({
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 1</Text>
<Flex align="center" bg="neutral-1" p="4">
<Flex align="center" bg="neutral" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 2</Text>
<Flex align="center" bg="neutral-2" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
<Box bg="neutral">
<Flex align="center" bg="neutral" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Box>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
<Box bg="neutral">
<Box bg="neutral">
<Flex align="center" bg="neutral" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Box>
</Box>
</Flex>
</Flex>
),
@@ -17,6 +17,7 @@
import preview from '../../../../../.storybook/preview';
import { ToggleButtonGroup } from './ToggleButtonGroup';
import { ToggleButton } from '../ToggleButton/ToggleButton';
import { Box } from '../Box';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { useState } from 'react';
@@ -100,7 +101,7 @@ export const Backgrounds = meta.story({
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 1</Text>
<Flex align="center" bg="neutral-1" p="4" gap="4">
<Flex align="center" bg="neutral" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
@@ -113,29 +114,35 @@ export const Backgrounds = meta.story({
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 2</Text>
<Flex align="center" bg="neutral-2" p="4" gap="4">
<ToggleButtonGroup
selectionMode="single"
defaultSelectedKeys={['option1']}
>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
<Box bg="neutral">
<Flex align="center" bg="neutral" 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>
</Box>
</Flex>
<Flex direction="column" gap="4">
<Text>On Neutral 3</Text>
<Flex align="center" bg="neutral-3" 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>
<Box bg="neutral">
<Box bg="neutral">
<Flex align="center" bg="neutral" 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>
</Box>
</Box>
</Flex>
</Flex>
),
+5 -5
View File
@@ -90,10 +90,10 @@ export function useBgConsumer(): BgContextValue {
*
* - `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.
* - `bg` is `'neutral'` -- increments the neutral level from the parent context,
* capping at `neutral-3`. The increment is always relative to the parent; it is
* not possible to pin a container to an explicit neutral level.
* - `bg` is `'danger'` | `'warning'` | `'success'` -- used as-is.
*
* **Capping:**
*
@@ -116,7 +116,7 @@ export function useBgProvider(bg?: Responsive<ProviderBg>): BgContextValue {
const resolved = resolveResponsiveValue(bg, breakpoint);
if (resolved === 'neutral-auto') {
if (resolved === 'neutral') {
return { bg: incrementNeutralBg(context.bg) };
}
+10 -6
View File
@@ -182,7 +182,7 @@ export interface ComponentDefinition {
}
/**
* Background type for the neutral bg system.
* Resolved background level stored in context and applied as `data-bg` on DOM elements.
*
* Supports neutral levels ('neutral-1' through 'neutral-3') and
* intent backgrounds ('danger', 'warning', 'success').
@@ -190,7 +190,9 @@ export interface ComponentDefinition {
* 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
* This type is internal. Use `ProviderBg` for component prop types.
*
* @internal
*/
export type ContainerBg =
| 'neutral-1'
@@ -201,11 +203,13 @@ export type ContainerBg =
| 'success';
/**
* Background values accepted by provider components.
* Background values accepted by provider components (Box, Flex, Grid, Card, etc.).
*
* Includes all `ContainerBg` values plus `'neutral-auto'` which
* automatically increments the neutral level from the parent context.
* - `'neutral'` automatically increments the neutral level from the parent context,
* capping at the maximum level. This is always incremental; explicit levels cannot
* be set directly.
* - `'danger'` | `'warning'` | `'success'` intent backgrounds used as-is.
*
* @public
*/
export type ProviderBg = ContainerBg | 'neutral-auto';
export type ProviderBg = 'neutral' | 'danger' | 'warning' | 'success';