diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 91370e60ceebe5..c949cc9bcc3d5b 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- Remove `Box` component. Components that previously used `Box` should use the equivalent design tokens in their CSS directly ([#74981](https://github.com/WordPress/gutenberg/issues/74981)). + ### New Features - Add `Tabs` primitive ([#74652](https://github.com/WordPress/gutenberg/pull/74652)). diff --git a/packages/ui/README.md b/packages/ui/README.md index ef1a3ad5a59537..f22f7a3a076c02 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -39,13 +39,14 @@ import '@wordpress/theme/design-tokens.css'; ### Basic Component Usage ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { return ( - - Hello World - + +
Item 1
+
Item 2
+
); } ``` @@ -59,11 +60,11 @@ All components in the design system follow consistent patterns for maximum flexi Every component supports the `render` prop for complete control over the underlying HTML element: ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { - // Render Box as a instead of the default
- return }>{ /* ... */ }; + // Render Stack as a
instead of the default
+ return }>{ /* ... */ }; } ``` @@ -73,12 +74,12 @@ All components forward refs to their underlying DOM elements: ```tsx import { useRef } from '@wordpress/element'; -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { - const boxRef = useRef< HTMLDivElement >( null ); + const stackRef = useRef< HTMLDivElement >( null ); - return { /* ... */ }; + return { /* ... */ }; } ``` @@ -87,11 +88,11 @@ function MyComponent() { Components merge provided `className` props with their internal styles: ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { // Your custom CSS className is merged with component styles - return { /* ... */ }; + return { /* ... */ }; } ``` diff --git a/packages/ui/src/badge/badge.tsx b/packages/ui/src/badge/badge.tsx index 054f46066a9220..abd9fc40bf4616 100644 --- a/packages/ui/src/badge/badge.tsx +++ b/packages/ui/src/badge/badge.tsx @@ -1,88 +1,29 @@ +import { useRender, mergeProps } from '@base-ui/react'; +import clsx from 'clsx'; import { forwardRef } from '@wordpress/element'; -import { Box } from '../box'; -import { type BoxProps } from '../box/types'; import { type BadgeProps } from './types'; - -/** - * Default render function that renders a span element with the given props. - */ -const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'span' > ) => ( - -); - -/** - * Maps intent values to Box backgroundColor and color props. - * Uses strong emphasis styles (as emphasis prop has been removed). - */ -const getIntentStyles = ( - intent: BadgeProps[ 'intent' ] -): Partial< BoxProps > => { - switch ( intent ) { - case 'high': - return { - backgroundColor: 'error', - color: 'error', - }; - case 'medium': - return { - backgroundColor: 'warning', - color: 'warning', - }; - case 'low': - return { - backgroundColor: 'caution', - color: 'caution', - }; - case 'stable': - return { - backgroundColor: 'success', - color: 'success', - }; - case 'informational': - return { - backgroundColor: 'info', - color: 'info', - }; - case 'draft': - return { - backgroundColor: 'neutral-weak', - color: 'neutral', - }; - case 'none': - default: - return { - backgroundColor: 'neutral', - color: 'neutral-weak', - }; - } -}; +import styles from './style.module.css'; /** * A badge component for displaying labels with semantic intent. - * Built on the Box primitive for consistent theming and accessibility. */ -export const Badge = forwardRef< HTMLDivElement, BadgeProps >( function Badge( - { children, intent = 'none', render = DEFAULT_RENDER, ...props }, +export const Badge = forwardRef< HTMLSpanElement, BadgeProps >( function Badge( + { children, intent = 'none', render, className, ...props }, ref ) { - const intentStyles = getIntentStyles( intent ); + const element = useRender( { + render, + defaultTagName: 'span', + ref, + props: mergeProps< 'span' >( props, { + className: clsx( + styles.badge, + styles[ `is-${ intent }-intent` ], + className + ), + children, + } ), + } ); - return ( - - { children } - - ); + return element; } ); diff --git a/packages/ui/src/badge/style.module.css b/packages/ui/src/badge/style.module.css new file mode 100644 index 00000000000000..fe339a844c78a6 --- /dev/null +++ b/packages/ui/src/badge/style.module.css @@ -0,0 +1,48 @@ +@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides; + +@layer wp-ui-components { + .badge { + padding-inline: var(--wpds-dimension-padding-sm); + padding-block: var(--wpds-dimension-padding-xs); + border-radius: var(--wpds-border-radius-lg); + font-family: var(--wpds-font-family-body); + font-size: var(--wpds-font-size-sm); + font-weight: var(--wpds-font-weight-regular); + line-height: var(--wpds-font-line-height-xs); + } + + .is-high-intent { + background-color: var(--wpds-color-bg-surface-error); + color: var(--wpds-color-fg-content-error); + } + + .is-medium-intent { + background-color: var(--wpds-color-bg-surface-warning); + color: var(--wpds-color-fg-content-warning); + } + + .is-low-intent { + background-color: var(--wpds-color-bg-surface-caution); + color: var(--wpds-color-fg-content-caution); + } + + .is-stable-intent { + background-color: var(--wpds-color-bg-surface-success); + color: var(--wpds-color-fg-content-success); + } + + .is-informational-intent { + background-color: var(--wpds-color-bg-surface-info); + color: var(--wpds-color-fg-content-info); + } + + .is-draft-intent { + background-color: var(--wpds-color-bg-surface-neutral-weak); + color: var(--wpds-color-fg-content-neutral); + } + + .is-none-intent { + background-color: var(--wpds-color-bg-surface-neutral); + color: var(--wpds-color-fg-content-neutral-weak); + } +} diff --git a/packages/ui/src/box/box.tsx b/packages/ui/src/box/box.tsx deleted file mode 100644 index 180fb387645893..00000000000000 --- a/packages/ui/src/box/box.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useRender, mergeProps } from '@base-ui/react'; -import { forwardRef } from '@wordpress/element'; -import { type BoxProps } from './types'; - -/** - * Default render function that renders a div element with the given props. - */ -const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'div' > ) => ( -
-); - -/** - * Capitalizes the first character of a string. - */ -const capitalize = ( str: string ): string => - str.charAt( 0 ).toUpperCase() + str.slice( 1 ); - -/** - * Converts a size token name to a CSS design token property reference (with - * fallback). - * - * @param property The CSS property name. - * @param value The size token name. - * @return A CSS value string with variable references. - */ -const getSpacingValue = ( property: string, value: string ): string => - `var(--wpds-dimension-${ property }-${ value }, var(--wpds-dimension-${ property }-${ value }))`; - -/** - * Generates CSS styles for properties with optionally directional values, - * normalizing single values and objects with directional keys for logical - * properties. - * - * @param property The CSS property name from BoxProps. - * @param value The property value (single or object with directional keys). - * @return A CSSProperties object with the computed styles. - */ -const getDimensionVariantStyles = < T extends keyof BoxProps >( - property: T, - value: NonNullable< BoxProps[ T ] > -): React.CSSProperties => - typeof value !== 'object' - ? { [ property ]: getSpacingValue( property, value ) } - : Object.keys( value ).reduce( - ( result, key ) => ( { - ...result, - [ property + capitalize( key ) ]: getSpacingValue( - property, - value[ key ] - ), - } ), - {} as Record< string, string > - ); - -/** - * A low-level visual primitive that provides an interface for applying design - * token-based customization for background, text, padding, and more. - */ -export const Box = forwardRef< HTMLDivElement, BoxProps >( function Box( - { - target = 'surface', - backgroundColor, - color, - padding, - borderRadius, - borderWidth, - borderColor, - render = DEFAULT_RENDER, - ...props - }, - ref -) { - const style: React.CSSProperties = {}; - - if ( backgroundColor ) { - style.backgroundColor = `var(--wpds-color-bg-${ target }-${ backgroundColor }, var(--wpds-color-bg-surface-${ backgroundColor }))`; - } - - if ( color ) { - style.color = `var(--wpds-color-fg-${ target }-${ color }, var(--wpds-color-fg-content-${ color }))`; - } - - if ( padding ) { - Object.assign( style, getDimensionVariantStyles( 'padding', padding ) ); - } - - if ( borderRadius ) { - style.borderRadius = `var(--wpds-border-radius-${ target }-${ borderRadius }, var(--wpds-border-radius-${ borderRadius }))`; - } - - if ( borderWidth ) { - style.borderWidth = `var(--wpds-border-width-${ target }-${ borderWidth }, var(--wpds-border-width-${ borderWidth }))`; - style.borderStyle = 'solid'; - } - - if ( borderColor ) { - style.borderColor = `var(--wpds-color-stroke-${ target }-${ borderColor }, var(--wpds-color-stroke-surface-${ borderColor }))`; - } - - const element = useRender( { - render, - ref, - props: mergeProps< 'div' >( props, { style } ), - } ); - - return element; -} ); diff --git a/packages/ui/src/box/index.ts b/packages/ui/src/box/index.ts deleted file mode 100644 index aae32603581ca3..00000000000000 --- a/packages/ui/src/box/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Box } from './box'; diff --git a/packages/ui/src/box/stories/index.story.tsx b/packages/ui/src/box/stories/index.story.tsx deleted file mode 100644 index 0f5614432d8a44..00000000000000 --- a/packages/ui/src/box/stories/index.story.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/react-vite'; -import { type PaddingSize } from '@wordpress/theme'; -import { Box } from '../box'; - -const meta: Meta< typeof Box > = { - title: 'Design System/Components/Box', - component: Box, -}; -export default meta; - -type Story = StoryObj< typeof Box >; - -export const Default: Story = { - args: { - children: 'Box', - backgroundColor: 'info', - color: 'info', - padding: 'sm', - borderColor: 'brand', - borderRadius: 'md', - borderWidth: 'sm', - }, - argTypes: { - padding: { - control: 'select', - options: [ - 'xs', - 'sm', - 'md', - 'lg', - 'xl', - '2xl', - '3xl', - ] satisfies PaddingSize[], - }, - }, -}; - -export const DirectionalPadding: Story = { - ...Default, - args: { - ...Default.args, - padding: { - blockStart: 'sm', - inline: 'md', - blockEnd: 'lg', - }, - }, -}; diff --git a/packages/ui/src/box/test/box.test.tsx b/packages/ui/src/box/test/box.test.tsx deleted file mode 100644 index 912dbafdf49005..00000000000000 --- a/packages/ui/src/box/test/box.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { createRef } from '@wordpress/element'; -import { Box } from '../box'; - -describe( 'Box', () => { - it( 'forwards ref', () => { - const ref = createRef< HTMLDivElement >(); - - render( Content ); - - expect( ref.current ).toBeInstanceOf( HTMLDivElement ); - } ); - - it( 'merges props', () => { - render( - - Content - - ); - - const box = screen.getByText( 'Content' ); - - expect( box ).toHaveStyle( { - 'background-color': - 'var(--wpds-color-bg-surface-brand, var(--wpds-color-bg-surface-brand))', - width: '10px', - } ); - } ); -} ); diff --git a/packages/ui/src/box/types.ts b/packages/ui/src/box/types.ts deleted file mode 100644 index 22bae4eb9e1094..00000000000000 --- a/packages/ui/src/box/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - type PaddingSize, - type BorderRadiusSize, - type BorderWidthSize, - type Target, - type SurfaceBackgroundColor, - type ContentForegroundColor, - type SurfaceStrokeColor, -} from '@wordpress/theme'; -import { type ComponentProps } from '../utils/types'; - -type DimensionVariant< T > = { - block?: T; - blockStart?: T; - blockEnd?: T; - inline?: T; - inlineStart?: T; - inlineEnd?: T; -}; - -export interface BoxProps extends ComponentProps< 'div' > { - /** - * The target rendering element design token grouping to use for the box. - */ - target?: Target; - - /** - * The surface background design token for box background color. - */ - backgroundColor?: SurfaceBackgroundColor; - - /** - * The surface foreground design token for box text color. - */ - color?: ContentForegroundColor; - - /** - * The surface spacing design token or base unit multiplier for box padding. - */ - padding?: PaddingSize | DimensionVariant< PaddingSize >; - - /** - * The surface border radius design token. - */ - borderRadius?: BorderRadiusSize; - - /** - * The surface border width design token. - */ - borderWidth?: BorderWidthSize; - - /** - * The surface border stroke color design token. - */ - borderColor?: SurfaceStrokeColor; - - /** - * The content to be rendered inside the component. - */ - children?: React.ReactNode; -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 64c8c05f34ec93..b303bd87834b9d 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,5 +1,4 @@ export * from './badge'; -export * from './box'; export * from './button'; export * from './form/primitives'; export * from './icon'; diff --git a/packages/ui/src/stack/stories/index.story.tsx b/packages/ui/src/stack/stories/index.story.tsx index 66ab560c04c314..4dd2019beb93d7 100644 --- a/packages/ui/src/stack/stories/index.story.tsx +++ b/packages/ui/src/stack/stories/index.story.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Stack } from '../index'; -import { Box } from '../../box'; const meta: Meta< typeof Stack > = { title: 'Design System/Components/Stack', @@ -9,9 +8,9 @@ const meta: Meta< typeof Stack > = { export default meta; const DemoBox = ( { variant }: { variant?: 'lg' } ) => ( -