diff --git a/.changeset/deep-masks-bow.md b/.changeset/deep-masks-bow.md new file mode 100644 index 0000000000..e36748e395 --- /dev/null +++ b/.changeset/deep-masks-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fix margin utility classes in Backstage UI. diff --git a/docs-ui/src/content/components/box.mdx b/docs-ui/src/content/components/box.mdx index 84fca48349..d1a9dd43fe 100644 --- a/docs-ui/src/content/components/box.mdx +++ b/docs-ui/src/content/components/box.mdx @@ -22,7 +22,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent'; } + preview={} code={boxPreviewSnippet} align="center" /> diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 24d4669e9d..da2dd973a5 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -1352,7 +1352,7 @@ } } - &[data-start-collapsed="true"] { + &[data-startCollapsed="true"] { flex: 0 auto; padding: 0; transition: flex-basis .3s ease-in-out; @@ -5455,7 +5455,7 @@ @media (width >= 640px) { .xs\:bui-ml { - margin-left: var(--pl-xs); + margin-left: var(--ml-xs); } .xs\:bui-ml-0\.5 { @@ -5525,7 +5525,7 @@ @media (width >= 768px) { .sm\:bui-ml { - margin-left: var(--pl-sm); + margin-left: var(--ml-sm); } .sm\:bui-ml-0\.5 { @@ -5595,7 +5595,7 @@ @media (width >= 1024px) { .md\:bui-ml { - margin-left: var(--pl-md); + margin-left: var(--ml-md); } .md\:bui-ml-0\.5 { @@ -5665,7 +5665,7 @@ @media (width >= 1280px) { .lg\:bui-ml { - margin-left: var(--pl-lg); + margin-left: var(--ml-lg); } .lg\:bui-ml-0\.5 { @@ -5735,7 +5735,7 @@ @media (width >= 1536px) { .xl\:bui-ml { - margin-left: var(--pl-xl); + margin-left: var(--ml-xl); } .xl\:bui-ml-0\.5 { @@ -5873,7 +5873,7 @@ @media (width >= 640px) { .xs\:bui-mr { - margin-right: var(--pr-xs); + margin-right: var(--mr-xs); } .xs\:bui-mr-0\.5 { @@ -5943,7 +5943,7 @@ @media (width >= 768px) { .sm\:bui-mr { - margin-right: var(--pr-sm); + margin-right: var(--mr-sm); } .sm\:bui-mr-0\.5 { @@ -6013,7 +6013,7 @@ @media (width >= 1024px) { .md\:bui-mr { - margin-right: var(--pr-md); + margin-right: var(--mr-md); } .md\:bui-mr-0\.5 { @@ -6083,7 +6083,7 @@ @media (width >= 1280px) { .lg\:bui-mr { - margin-right: var(--pr-lg); + margin-right: var(--mr-lg); } .lg\:bui-mr-0\.5 { @@ -6153,7 +6153,7 @@ @media (width >= 1536px) { .xl\:bui-mr { - margin-right: var(--pr-xl); + margin-right: var(--mr-xl); } .xl\:bui-mr-0\.5 { @@ -6291,7 +6291,7 @@ @media (width >= 640px) { .xs\:bui-mt { - margin-top: var(--pt-xs); + margin-top: var(--mt-xs); } .xs\:bui-mt-0\.5 { @@ -6361,7 +6361,7 @@ @media (width >= 768px) { .sm\:bui-mt { - margin-top: var(--pt-sm); + margin-top: var(--mt-sm); } .sm\:bui-mt-0\.5 { @@ -6431,7 +6431,7 @@ @media (width >= 1024px) { .md\:bui-mt { - margin-top: var(--pt-md); + margin-top: var(--mt-md); } .md\:bui-mt-0\.5 { @@ -6501,7 +6501,7 @@ @media (width >= 1280px) { .lg\:bui-mt { - margin-top: var(--pt-lg); + margin-top: var(--mt-lg); } .lg\:bui-mt-0\.5 { @@ -6571,7 +6571,7 @@ @media (width >= 1536px) { .xl\:bui-mt { - margin-top: var(--pt-xl); + margin-top: var(--mt-xl); } .xl\:bui-mt-0\.5 { @@ -6709,7 +6709,7 @@ @media (width >= 640px) { .xs\:bui-mb { - margin-bottom: var(--pb-xs); + margin-bottom: var(--mb-xs); } .xs\:bui-mb-0\.5 { @@ -6779,7 +6779,7 @@ @media (width >= 768px) { .sm\:bui-mb { - margin-bottom: var(--pb-sm); + margin-bottom: var(--mb-sm); } .sm\:bui-mb-0\.5 { @@ -6849,7 +6849,7 @@ @media (width >= 1024px) { .md\:bui-mb { - margin-bottom: var(--pb-md); + margin-bottom: var(--mb-md); } .md\:bui-mb-0\.5 { @@ -6919,7 +6919,7 @@ @media (width >= 1280px) { .lg\:bui-mb { - margin-bottom: var(--pb-lg); + margin-bottom: var(--mb-lg); } .lg\:bui-mb-0\.5 { @@ -6989,7 +6989,7 @@ @media (width >= 1536px) { .xl\:bui-mb { - margin-bottom: var(--pb-xl); + margin-bottom: var(--mb-xl); } .xl\:bui-mb-0\.5 { @@ -7144,8 +7144,8 @@ @media (width >= 640px) { .xs\:bui-my { - margin-top: var(--py-xs); - margin-bottom: var(--py-xs); + margin-top: var(--my-xs); + margin-bottom: var(--my-xs); } .xs\:bui-my-0\.5 { @@ -7231,8 +7231,8 @@ @media (width >= 768px) { .sm\:bui-my { - margin-top: var(--py-sm); - margin-bottom: var(--py-sm); + margin-top: var(--my-sm); + margin-bottom: var(--my-sm); } .sm\:bui-my-0\.5 { @@ -7318,8 +7318,8 @@ @media (width >= 1024px) { .md\:bui-my { - margin-top: var(--py-md); - margin-bottom: var(--py-md); + margin-top: var(--my-md); + margin-bottom: var(--my-md); } .md\:bui-my-0\.5 { @@ -7405,8 +7405,8 @@ @media (width >= 1280px) { .lg\:bui-my { - margin-top: var(--py-lg); - margin-bottom: var(--py-lg); + margin-top: var(--my-lg); + margin-bottom: var(--my-lg); } .lg\:bui-my-0\.5 { @@ -7492,8 +7492,8 @@ @media (width >= 1536px) { .xl\:bui-my { - margin-top: var(--py-xl); - margin-bottom: var(--py-xl); + margin-top: var(--my-xl); + margin-bottom: var(--my-xl); } .xl\:bui-my-0\.5 { @@ -7664,8 +7664,8 @@ @media (width >= 640px) { .xs\:bui-mx { - margin-left: var(--px-xs); - margin-right: var(--px-xs); + margin-left: var(--mx-xs); + margin-right: var(--mx-xs); } .xs\:bui-mx-0\.5 { @@ -7751,8 +7751,8 @@ @media (width >= 768px) { .sm\:bui-mx { - margin-left: var(--px-sm); - margin-right: var(--px-sm); + margin-left: var(--mx-sm); + margin-right: var(--mx-sm); } .sm\:bui-mx-0\.5 { @@ -7838,8 +7838,8 @@ @media (width >= 1024px) { .md\:bui-mx { - margin-left: var(--px-md); - margin-right: var(--px-md); + margin-left: var(--mx-md); + margin-right: var(--mx-md); } .md\:bui-mx-0\.5 { @@ -7925,8 +7925,8 @@ @media (width >= 1280px) { .lg\:bui-mx { - margin-left: var(--px-lg); - margin-right: var(--px-lg); + margin-left: var(--mx-lg); + margin-right: var(--mx-lg); } .lg\:bui-mx-0\.5 { @@ -8012,8 +8012,8 @@ @media (width >= 1536px) { .xl\:bui-mx { - margin-left: var(--px-xl); - margin-right: var(--px-xl); + margin-left: var(--mx-xl); + margin-right: var(--mx-xl); } .xl\:bui-mx-0\.5 { diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 1674fef093..1aa13e3328 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -107,44 +107,34 @@ export const Box: ForwardRefExoticComponent< BoxProps & RefAttributes >; -// @public (undocumented) -export type BoxOwnProps = GetPropDefTypes; - -// @public (undocumented) -export const boxPropDefs: { - as: { - type: 'enum'; - values: readonly ['div', 'span']; - default: 'div'; - }; -}; - // @public (undocumented) export interface BoxProps extends SpaceProps { // (undocumented) - as?: BoxOwnProps['as']; + as?: keyof JSX.IntrinsicElements; // (undocumented) children?: React.ReactNode; // (undocumented) className?: string; // (undocumented) - display?: DisplayProps['display']; + display?: Responsive<'none' | 'flex' | 'block' | 'inline'>; // (undocumented) - height?: HeightProps['height']; + height?: Responsive; // (undocumented) - maxHeight?: HeightProps['maxHeight']; + maxHeight?: Responsive; // (undocumented) - maxWidth?: WidthProps['maxWidth']; + maxWidth?: Responsive; // (undocumented) - minHeight?: HeightProps['minHeight']; + minHeight?: Responsive; // (undocumented) - minWidth?: WidthProps['minWidth']; + minWidth?: Responsive; // (undocumented) - position?: PositionProps['position']; + position?: Responsive< + 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' + >; // (undocumented) style?: React.CSSProperties; // (undocumented) - width?: WidthProps['width']; + width?: Responsive; } // @public (undocumented) @@ -358,7 +348,20 @@ export const Column: ( ) => JSX_2.Element; // @public (undocumented) -export type Columns = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'auto'; +export type Columns = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | '12' + | 'auto'; // @public export type ComponentClassNames = @@ -370,6 +373,8 @@ export interface ComponentDefinition { classNames: ClassNamesMap; // (undocumented) dataAttributes?: DataAttributesMap; + // (undocumented) + utilityProps?: string[]; } // @public @@ -391,6 +396,30 @@ export const componentDefinitions: { readonly classNames: { readonly root: 'bui-Box'; }; + readonly utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'position', + 'display', + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + ]; }; readonly Button: { readonly classNames: { @@ -440,6 +469,7 @@ export const componentDefinitions: { readonly classNames: { readonly root: 'bui-Container'; }; + readonly utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display']; }; readonly Dialog: { readonly classNames: { @@ -463,12 +493,55 @@ export const componentDefinitions: { readonly classNames: { readonly root: 'bui-Flex'; }; + readonly utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'gap', + 'align', + 'justify', + 'direction', + ]; }; readonly Grid: { readonly classNames: { readonly root: 'bui-Grid'; - readonly item: 'bui-GridItem'; }; + readonly utilityProps: [ + 'columns', + 'gap', + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + ]; + }; + readonly GridItem: { + readonly classNames: { + readonly root: 'bui-GridItem'; + }; + readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan']; }; readonly Header: { readonly classNames: { @@ -546,6 +619,9 @@ export const componentDefinitions: { readonly root: 'bui-PasswordField'; readonly inputVisibility: 'bui-InputVisibility'; }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium']; + }; }; readonly Popover: { readonly classNames: { @@ -574,6 +650,7 @@ export const componentDefinitions: { }; readonly dataAttributes: { readonly startCollapsed: readonly [true, false]; + readonly size: readonly ['small', 'medium']; }; }; readonly Select: { @@ -670,6 +747,7 @@ export const componentDefinitions: { readonly dataAttributes: { readonly invalid: readonly [true, false]; readonly disabled: readonly [true, false]; + readonly size: readonly ['small', 'medium']; }; }; readonly Tooltip: { @@ -825,45 +903,20 @@ export const Flex: ForwardRefExoticComponent< // @public (undocumented) export type FlexDirection = 'row' | 'column'; -// @public (undocumented) -export type FlexOwnProps = GetPropDefTypes; - -// @public (undocumented) -export const flexPropDefs: { - align: { - type: 'enum'; - className: string; - values: readonly ['start', 'center', 'end', 'baseline', 'stretch']; - responsive: true; - }; - direction: { - type: 'enum'; - className: string; - values: readonly ['row', 'column', 'row-reverse', 'column-reverse']; - responsive: true; - }; - justify: { - type: 'enum'; - className: string; - values: readonly ['start', 'center', 'end', 'between']; - responsive: true; - }; -}; - // @public (undocumented) export interface FlexProps extends SpaceProps { // (undocumented) - align?: FlexOwnProps['align']; + align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>; // (undocumented) - children: React.ReactNode; + children?: React.ReactNode; // (undocumented) className?: string; // (undocumented) - direction?: FlexOwnProps['direction']; + direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; // (undocumented) - gap?: GapProps['gap']; + gap?: Responsive; // (undocumented) - justify?: FlexOwnProps['justify']; + justify?: Responsive<'start' | 'center' | 'end' | 'between'>; // (undocumented) style?: React.CSSProperties; } @@ -922,97 +975,6 @@ export const Grid: { >; }; -// @public (undocumented) -export type GridItemOwnProps = GetPropDefTypes; - -// @public (undocumented) -export const gridItemPropDefs: { - colSpan: { - type: 'enum | string'; - className: string; - customProperties: '--col-span'[]; - values: readonly [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', - ]; - responsive: true; - }; - colEnd: { - type: 'enum | string'; - className: string; - customProperties: '--col-end'[]; - values: readonly [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', - ]; - responsive: true; - }; - colStart: { - type: 'enum | string'; - className: string; - customProperties: '--col-start'[]; - values: readonly [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', - ]; - responsive: true; - }; - rowSpan: { - type: 'enum | string'; - className: string; - customProperties: '--row-span'[]; - values: readonly [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', - ]; - responsive: true; - }; -}; - // @public (undocumented) export interface GridItemProps { // (undocumented) @@ -1020,46 +982,17 @@ export interface GridItemProps { // (undocumented) className?: string; // (undocumented) - colEnd?: GridItemOwnProps['colEnd']; + colEnd?: Responsive; // (undocumented) - colSpan?: GridItemOwnProps['colSpan']; + colSpan?: Responsive; // (undocumented) - colStart?: GridItemOwnProps['colStart']; + colStart?: Responsive; // (undocumented) - rowSpan?: GridItemOwnProps['rowSpan']; + rowSpan?: Responsive; // (undocumented) style?: React.CSSProperties; } -// @public (undocumented) -export type GridOwnProps = GetPropDefTypes; - -// @public (undocumented) -export const gridPropDefs: { - columns: { - type: 'enum | string'; - className: string; - customProperties: '--columns'[]; - values: readonly [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', - ]; - responsive: true; - default: string; - }; -}; - // @public (undocumented) export interface GridProps extends SpaceProps { // (undocumented) @@ -1067,9 +1000,9 @@ export interface GridProps extends SpaceProps { // (undocumented) className?: string; // (undocumented) - columns?: GridOwnProps['columns']; + columns?: Responsive; // (undocumented) - gap?: GapProps['gap']; + gap?: Responsive; // (undocumented) style?: React.CSSProperties; } diff --git a/packages/ui/src/components/Box/Box.props.ts b/packages/ui/src/components/Box/Box.props.ts deleted file mode 100644 index 3cf0291e43..0000000000 --- a/packages/ui/src/components/Box/Box.props.ts +++ /dev/null @@ -1,33 +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 type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -const as = ['div', 'span'] as const; - -/** @public */ -const boxPropDefs = { - as: { type: 'enum', values: as, default: 'div' }, -} satisfies { - as: PropDef<(typeof as)[number]>; -}; - -// Use all of the imported prop defs to ensure that JSDoc works -/** @public */ -type BoxOwnProps = GetPropDefTypes; - -export { boxPropDefs }; -export type { BoxOwnProps }; diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index f0086c3d25..6637f45740 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -36,35 +36,277 @@ const meta = { export default meta; type Story = StoryObj; -const Card = () => { - return ( -
- ); -}; +const diagonalStripePattern = (() => { + const svg = ` + + + + + + `.trim(); + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +})(); export const Default: Story = { args: { - children: 'Hello World', - mb: '4', + width: '64px', + height: '64px', + style: { + background: '#eaf2fd', + borderRadius: '4px', + border: '1px solid #2563eb', + backgroundImage: `url("${diagonalStripePattern}")`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 'bold', + color: '#2563eb', + }, }, }; -export const Preview: Story = { - args: { - children: , - display: 'inline', - }, +export const Margin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const ResponsiveMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomResponsiveMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const Padding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const ResponsivePadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomPadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomResponsivePadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), }; const CardDisplay = ({ children }: { children?: ReactNode }) => { @@ -105,151 +347,3 @@ export const Display: Story = { ), }; - -const styleInsideBox = { - background: 'rgb(196, 202, 251)', - color: 'white', - borderRadius: '4px', -}; - -export const Padding: Story = { - args: { - style: { - background: '#1f47ff', - color: 'white', - borderRadius: '4px', - padding: '12px 12px', - }, - }, - render: args => ( - - - - Padding - - - Padding X - - - Padding Y - - - - - Padding Top - - - Padding Right - - - Padding Bottom - - - Padding Left - - - - - Custom Padding - - - Custom Padding X - - - Custom Padding Y - - - - - Custom Padding Top Bottom - - - Custom Padding Left Right - - - - ), -}; - -export const Margin: Story = { - args: { - style: { - background: '#1f47ff', - color: 'white', - borderRadius: '4px', - padding: '12px 12px', - }, - }, - render: args => ( - - - - - Margin - - - - - Margin X - - - - - Margin Y - - - - - - - Margin Top - - - - - Margin Right - - - - - Margin Bottom - - - - - Margin Left - - - - - - - Custom Margin - - - - - Custom Margin X - - - - - Custom Margin Y - - - - - - - Custom Margin Top Bottom - - - - - Custom Margin Left Right - - - - - ), -}; diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index 06d7d1d434..2bb4e3cd15 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -17,38 +17,27 @@ import { createElement, forwardRef } from 'react'; import { BoxProps } from './types'; import clsx from 'clsx'; -import { extractProps } from '../../utils/extractProps'; -import { spacingPropDefs } from '../../props/spacing.props'; -import { boxPropDefs } from './Box.props'; -import { widthPropDefs } from '../../props/width.props'; -import { heightPropDefs } from '../../props/height.props'; -import { positionPropDefs } from '../../props/position.props'; -import { displayPropDefs } from '../../props/display.props'; import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Box = forwardRef((props, ref) => { - const { children } = props; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'Box', + props, + ); - const propDefs = { - ...spacingPropDefs, - ...widthPropDefs, - ...heightPropDefs, - ...positionPropDefs, - ...displayPropDefs, - ...boxPropDefs, - }; + const { as = 'div', children, className, ...rest } = cleanedProps; - const { classNames } = useStyles('Box'); - const { className, style, dataProps } = extractProps(props, propDefs); - - return createElement(props.as || 'div', { - ref, - className: clsx(classNames.root, className), - ...dataProps, - style, + return createElement( + as, + { + ref, + className: clsx(classNames.root, utilityClasses, className), + style, + ...rest, + }, children, - }); + ); }); Box.displayName = 'Box'; diff --git a/packages/ui/src/components/Box/index.tsx b/packages/ui/src/components/Box/index.tsx index 087a940a2d..d47e653dfd 100644 --- a/packages/ui/src/components/Box/index.tsx +++ b/packages/ui/src/components/Box/index.tsx @@ -15,5 +15,3 @@ */ export { Box } from './Box'; export type * from './types'; -export type { BoxOwnProps } from './Box.props'; -export { boxPropDefs } from './Box.props'; diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index c70012dd56..c0f4a71035 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -14,24 +14,21 @@ * limitations under the License. */ -import type { HeightProps } from '../../props/height.props'; -import type { WidthProps } from '../../props/width.props'; -import type { PositionProps } from '../../props/position.props'; -import type { DisplayProps } from '../../props/display.props'; -import type { SpaceProps } from '../../types'; -import type { BoxOwnProps } from './Box.props'; +import type { SpaceProps, Responsive } from '../../types'; /** @public */ export interface BoxProps extends SpaceProps { - display?: DisplayProps['display']; - as?: BoxOwnProps['as']; - width?: WidthProps['width']; - minWidth?: WidthProps['minWidth']; - maxWidth?: WidthProps['maxWidth']; - height?: HeightProps['height']; - minHeight?: HeightProps['minHeight']; - maxHeight?: HeightProps['maxHeight']; - position?: PositionProps['position']; + display?: Responsive<'none' | 'flex' | 'block' | 'inline'>; + as?: keyof JSX.IntrinsicElements; + width?: Responsive; + minWidth?: Responsive; + maxWidth?: Responsive; + height?: Responsive; + minHeight?: Responsive; + maxHeight?: Responsive; + position?: Responsive< + 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' + >; children?: React.ReactNode; className?: string; style?: React.CSSProperties; diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 40879e51ed..610f29b9b0 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -23,21 +23,14 @@ import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Button = forwardRef( (props: ButtonProps, ref: Ref) => { - const { - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - ...rest - } = props; - - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); + const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + return ( } />, }; - -const variants = ['primary', 'secondary'] as const; -const sizes = ['small', 'medium'] as const; - -export const Playground: Story = { - render: args => ( - - {variants.map(variant => ( - - {variant} - {sizes.map(size => ( - - } - /> - } - aria-label="Chevron right icon button" - variant={variant} - size={size} - /> - } - aria-label="Chevron right icon button" - variant={variant} - size={size} - /> - - ))} - - ))} - - ), -}; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx index 072ed20963..19ed2de3f3 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx @@ -23,22 +23,16 @@ import { useStyles } from '../../hooks/useStyles'; /** @public */ export const ButtonIcon = forwardRef( (props: ButtonIconProps, ref: Ref) => { - const { - size = 'small', - variant = 'primary', - icon, - className, - style, - ...rest - } = props; - - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); const { classNames: classNamesButtonIcon } = useStyles('ButtonIcon'); + const { className, icon, ...rest } = cleanedProps; + return ( ( - - {variants.map(variant => ( - - {variant} - {sizes.map(size => ( - - - Button - - } - variant={variant} - size={size} - > - Button - - } - variant={variant} - size={size} - > - Button - - } - iconEnd={} - style={{ width: '200px' }} - variant={variant} - size={size} - > - Button - - - Button - - } - variant={variant} - size={size} - isDisabled - > - Button - - } - variant={variant} - size={size} - isDisabled - > - Button - - - ))} - - ))} - - ), -}; diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index b5b5ab208d..5c621bc057 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -26,24 +26,18 @@ import { isExternalLink } from '../../utils/isExternalLink'; export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { const navigate = useNavigate(); - const { - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - href, - ...rest - } = props; - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); const { classNames: classNamesButtonLink } = useStyles('ButtonLink'); + const { children, className, iconStart, iconEnd, href, ...rest } = + cleanedProps; + const isExternal = isExternalLink(href); // If it's an external link, render RALink without RouterProvider diff --git a/packages/ui/src/components/Container/Container.tsx b/packages/ui/src/components/Container/Container.tsx index eef1e187c4..f313f95eaa 100644 --- a/packages/ui/src/components/Container/Container.tsx +++ b/packages/ui/src/components/Container/Container.tsx @@ -14,43 +14,28 @@ * limitations under the License. */ -import { createElement, forwardRef } from 'react'; +import { forwardRef } from 'react'; import { ContainerProps } from './types'; import clsx from 'clsx'; -import { displayPropDefs } from '../../props/display.props'; -import { extractProps } from '../../utils/extractProps'; -import { spacingPropDefs } from '../../props/spacing.props'; import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Container = forwardRef( (props, ref) => { - const { children } = props; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'Container', + props, + ); - const { classNames } = useStyles('Container'); + const { className, ...rest } = cleanedProps; - // Create a subset of spacing props that match the interface - const containerSpacingProps = { - my: spacingPropDefs.my, - mt: spacingPropDefs.mt, - mb: spacingPropDefs.mb, - py: spacingPropDefs.py, - pt: spacingPropDefs.pt, - pb: spacingPropDefs.pb, - }; - - const propDefs = { - ...displayPropDefs, - ...containerSpacingProps, - }; - const { className, style, dataProps } = extractProps(props, propDefs); - - return createElement('div', { - ref, - className: clsx(classNames.root, className), - ...dataProps, - style, - children, - }); + return ( +
+ ); }, ); diff --git a/packages/ui/src/components/Flex/Flex.props.ts b/packages/ui/src/components/Flex/Flex.props.ts deleted file mode 100644 index b1d9c019d3..0000000000 --- a/packages/ui/src/components/Flex/Flex.props.ts +++ /dev/null @@ -1,58 +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 type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -const alignValues = ['start', 'center', 'end', 'baseline', 'stretch'] as const; -const directionValues = [ - 'row', - 'column', - 'row-reverse', - 'column-reverse', -] as const; -const justifyValues = ['start', 'center', 'end', 'between'] as const; - -/** @public */ -const flexPropDefs = { - align: { - type: 'enum', - className: 'bui-align', - values: alignValues, - responsive: true, - }, - direction: { - type: 'enum', - className: 'bui-fd', - values: directionValues, - responsive: true, - }, - justify: { - type: 'enum', - className: 'bui-jc', - values: justifyValues, - responsive: true, - }, -} satisfies { - align: PropDef<(typeof alignValues)[number]>; - direction: PropDef<(typeof directionValues)[number]>; - justify: PropDef<(typeof justifyValues)[number]>; -}; - -/** @public */ -type FlexOwnProps = GetPropDefTypes; - -export { flexPropDefs }; -export type { FlexOwnProps }; diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 40f9339ad6..87854e89c7 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -17,6 +17,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Flex } from './Flex'; import { Text } from '../Text'; +import { Box } from '../Box'; const meta = { title: 'Backstage UI/Flex', @@ -24,36 +25,54 @@ const meta = { argTypes: { align: { control: 'inline-radio', - options: ['left', 'center', 'right'], + options: ['start', 'center', 'end', 'baseline', 'stretch'], }, - children: { - control: false, + justify: { + control: 'inline-radio', + options: ['start', 'center', 'end', 'between'], }, - className: { - control: 'text', + direction: { + control: 'inline-radio', + options: ['row', 'column', 'row-reverse', 'column-reverse'], }, }, - args: { - align: 'stretch', - gap: '4', - children: 'hello world', - }, } satisfies Meta; export default meta; type Story = StoryObj; -const DecorativeBox = () => { +const DecorativeBox = ({ + width = '48px', + height = '48px', +}: { + width?: string; + height?: string; +}) => { + const diagonalStripePattern = (() => { + const svg = ` + + + + + + `.trim(); + return `data:image/svg+xml,${encodeURIComponent(svg)}`; + })(); + return ( -
); @@ -85,49 +104,109 @@ export const RowDirection: Story = { }, }; -export const AlignLeft: Story = { - render: () => ( - - - - +export const AlignStartInColumn: Story = { + args: { + align: 'start', + direction: 'column', + }, + render: args => ( + + + + ), }; -export const AlignCenter: Story = { - render: () => ( - - - - +export const AlignStartInRow: Story = { + args: { + align: 'start', + direction: 'row', + }, + render: args => ( + + + + ), }; -export const AlignRight: Story = { - render: () => ( - - - - +export const AlignCenterInColumn: Story = { + args: { + align: 'center', + direction: 'column', + }, + render: args => ( + + + + + + ), +}; + +export const AlignCenterInRow: Story = { + args: { + align: 'center', + direction: 'row', + }, + render: args => ( + + + + + + ), +}; + +export const AlignEndInColumn: Story = { + args: { + align: 'end', + direction: 'column', + }, + render: args => ( + + + + + + ), +}; + +export const AlignEndInRow: Story = { + args: { + align: 'end', + direction: 'row', + }, + render: args => ( + + + + ), }; export const ResponsiveAlign: Story = { - render: () => ( - - - - + args: { + align: { xs: 'start', md: 'center', lg: 'end' }, + }, + render: args => ( + + + + ), }; export const ResponsiveGap: Story = { - render: () => ( - + args: { + gap: { xs: '4', md: '8', lg: '12' }, + }, + render: args => ( + @@ -136,8 +215,11 @@ export const ResponsiveGap: Story = { }; export const LargeGap: Story = { - render: () => ( - + args: { + gap: '8', + }, + render: args => ( + diff --git a/packages/ui/src/components/Flex/Flex.tsx b/packages/ui/src/components/Flex/Flex.tsx index 021fe87820..b1f5913c06 100644 --- a/packages/ui/src/components/Flex/Flex.tsx +++ b/packages/ui/src/components/Flex/Flex.tsx @@ -14,31 +14,26 @@ * limitations under the License. */ -import { createElement, forwardRef } from 'react'; +import { forwardRef } from 'react'; import { FlexProps } from './types'; import clsx from 'clsx'; -import { flexPropDefs } from './Flex.props'; -import { extractProps } from '../../utils/extractProps'; -import { gapPropDefs } from '../../props/gap-props'; -import { spacingPropDefs } from '../../props/spacing.props'; import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Flex = forwardRef((props, ref) => { - const propDefs = { - ...gapPropDefs, - ...flexPropDefs, - ...spacingPropDefs, - }; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'Flex', + { gap: '4', ...props }, + ); - const { classNames } = useStyles('Flex'); - const { className, style, dataProps } = extractProps(props, propDefs); + const { className, ...rest } = cleanedProps; - return createElement('div', { - ref, - className: clsx(classNames.root, className), - ...dataProps, - style, - children: props.children, - }); + return ( +
+ ); }); diff --git a/packages/ui/src/components/Flex/index.ts b/packages/ui/src/components/Flex/index.ts index 033a9bec42..4e4c883119 100644 --- a/packages/ui/src/components/Flex/index.ts +++ b/packages/ui/src/components/Flex/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ export { Flex } from './Flex'; -export { flexPropDefs } from './Flex.props'; export type { FlexProps } from './types'; -export type { FlexOwnProps } from './Flex.props'; diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index d449bc609a..7699e0ddc5 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import type { SpaceProps } from '../../types'; -import { FlexOwnProps } from './Flex.props'; -import type { GapProps } from '../../props/gap-props'; +import type { Responsive, Space, SpaceProps } from '../../types'; /** @public */ export interface FlexProps extends SpaceProps { - children: React.ReactNode; - gap?: GapProps['gap']; - align?: FlexOwnProps['align']; - justify?: FlexOwnProps['justify']; - direction?: FlexOwnProps['direction']; + children?: React.ReactNode; + gap?: Responsive; + align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>; + justify?: Responsive<'start' | 'center' | 'end' | 'between'>; + direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; className?: string; style?: React.CSSProperties; } diff --git a/packages/ui/src/components/Grid/Grid.props.ts b/packages/ui/src/components/Grid/Grid.props.ts deleted file mode 100644 index b6997cb01b..0000000000 --- a/packages/ui/src/components/Grid/Grid.props.ts +++ /dev/null @@ -1,93 +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 type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -const columnsValues = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', - 'auto', -] as const; - -/** @public */ -const gridPropDefs = { - columns: { - type: 'enum | string', - className: 'bui-columns', - customProperties: ['--columns'], - values: columnsValues, - responsive: true, - default: 'auto', - }, -} satisfies { - columns: PropDef<(typeof columnsValues)[number]>; -}; - -/** @public */ -const gridItemPropDefs = { - colSpan: { - type: 'enum | string', - className: 'bui-col-span', - customProperties: ['--col-span'], - values: columnsValues, - responsive: true, - }, - colEnd: { - type: 'enum | string', - className: 'bui-col-end', - customProperties: ['--col-end'], - values: columnsValues, - responsive: true, - }, - colStart: { - type: 'enum | string', - className: 'bui-col-start', - customProperties: ['--col-start'], - values: columnsValues, - responsive: true, - }, - rowSpan: { - type: 'enum | string', - className: 'bui-row-span', - customProperties: ['--row-span'], - values: columnsValues, - responsive: true, - }, -} satisfies { - colSpan: PropDef<(typeof columnsValues)[number]>; - colEnd: PropDef<(typeof columnsValues)[number]>; - colStart: PropDef<(typeof columnsValues)[number]>; - rowSpan: PropDef<(typeof columnsValues)[number]>; -}; - -/** @public */ -type GridOwnProps = GetPropDefTypes; - -/** @public */ -type GridItemOwnProps = GetPropDefTypes; - -export { gridPropDefs, gridItemPropDefs }; -export type { GridOwnProps, GridItemOwnProps }; diff --git a/packages/ui/src/components/Grid/Grid.tsx b/packages/ui/src/components/Grid/Grid.tsx index 18ed40b87b..6ef72e381b 100644 --- a/packages/ui/src/components/Grid/Grid.tsx +++ b/packages/ui/src/components/Grid/Grid.tsx @@ -14,50 +14,45 @@ * limitations under the License. */ -import { createElement, forwardRef } from 'react'; -import { gapPropDefs } from '../../props/gap-props'; -import { extractProps } from '../../utils/extractProps'; -import { gridItemPropDefs, gridPropDefs } from './Grid.props'; +import { forwardRef } from 'react'; import clsx from 'clsx'; import type { GridItemProps, GridProps } from './types'; -import { spacingPropDefs } from '../../props/spacing.props'; import { useStyles } from '../../hooks/useStyles'; const GridRoot = forwardRef((props, ref) => { - const propDefs = { - ...gapPropDefs, - ...gridPropDefs, - ...spacingPropDefs, - }; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'Grid', + { columns: 'auto', gap: '4', ...props }, + ); - const { classNames } = useStyles('Grid'); + const { className, ...rest } = cleanedProps; - const { className, style, dataProps } = extractProps(props, propDefs); - - return createElement('div', { - ref, - className: clsx(classNames.root, className), - ...dataProps, - style, - children: props.children, - }); + return ( +
+ ); }); const GridItem = forwardRef((props, ref) => { - const propDefs = { - ...gridItemPropDefs, - }; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'GridItem', + props, + ); - const { classNames } = useStyles('Grid'); - const { className, style, dataProps } = extractProps(props, propDefs); + const { className, ...rest } = cleanedProps; - return createElement('div', { - ref, - className: clsx(classNames.item, className), - ...dataProps, - style, - children: props.children, - }); + return ( +
+ ); }); /** @public */ diff --git a/packages/ui/src/components/Grid/index.ts b/packages/ui/src/components/Grid/index.ts index fd10e7fe68..870ab55856 100644 --- a/packages/ui/src/components/Grid/index.ts +++ b/packages/ui/src/components/Grid/index.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { Grid } from './Grid'; -export { gridPropDefs, gridItemPropDefs } from './Grid.props'; export type { GridProps, GridItemProps } from './types'; -export type { GridOwnProps, GridItemOwnProps } from './Grid.props'; diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index 8d658fff70..d9d18b4718 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -14,16 +14,14 @@ * limitations under the License. */ -import type { GapProps } from '../../props/gap-props'; -import type { SpaceProps } from '../../types'; -import type { GridItemOwnProps, GridOwnProps } from './Grid.props'; +import type { Space, SpaceProps, Responsive, Columns } from '../../types'; /** @public */ export interface GridProps extends SpaceProps { children?: React.ReactNode; className?: string; - columns?: GridOwnProps['columns']; - gap?: GapProps['gap']; + columns?: Responsive; + gap?: Responsive; style?: React.CSSProperties; } @@ -31,9 +29,9 @@ export interface GridProps extends SpaceProps { export interface GridItemProps { children?: React.ReactNode; className?: string; - colSpan?: GridItemOwnProps['colSpan']; - colEnd?: GridItemOwnProps['colEnd']; - colStart?: GridItemOwnProps['colStart']; - rowSpan?: GridItemOwnProps['rowSpan']; + colSpan?: Responsive; + colEnd?: Responsive; + colStart?: Responsive; + rowSpan?: Responsive; style?: React.CSSProperties; } diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index fc5dae1088..8701d50652 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -32,17 +32,9 @@ import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; export const PasswordField = forwardRef( (props, ref) => { const { - className, - icon, - size = 'small', label, - secondaryLabel, - description, - isRequired, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, - placeholder, - ...rest } = props; useEffect(() => { @@ -53,15 +45,28 @@ export const PasswordField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames: passwordFieldClassNames, dataAttributes } = useStyles( - 'PasswordField', - { - size, - }, - ); - const { classNames: textFieldClassNames } = useStyles('TextField', {}); + const { + classNames: passwordFieldClassNames, + dataAttributes, + style, + cleanedProps, + } = useStyles('PasswordField', { + size: 'small', + ...props, + }); + + const { + className, + description, + icon, + isRequired, + secondaryLabel, + placeholder, + ...rest + } = cleanedProps; + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); @@ -76,6 +81,7 @@ export const PasswordField = forwardRef( aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} type="password" + style={style} {...rest} ref={ref} > diff --git a/packages/ui/src/components/SearchField/SearchField.styles.css b/packages/ui/src/components/SearchField/SearchField.styles.css index 996ee1eb07..37ef5e9b2c 100644 --- a/packages/ui/src/components/SearchField/SearchField.styles.css +++ b/packages/ui/src/components/SearchField/SearchField.styles.css @@ -25,7 +25,7 @@ } } - &[data-start-collapsed='true'] { + &[data-startCollapsed='true'] { transition: flex-basis 0.3s ease-in-out; padding: 0; flex: 0 1 auto; diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index 1c4bb7fa18..cb0eb41eda 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -32,19 +32,9 @@ import type { SearchFieldProps } from './types'; export const SearchField = forwardRef( (props, ref) => { const { - className, - icon, - size = 'small', label, - secondaryLabel, - description, - isRequired, - onChange, - placeholder = 'Search', - startCollapsed = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, - ...rest } = props; const [isCollapsed, setIsCollapsed] = useState(false); @@ -58,14 +48,30 @@ export const SearchField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames: textFieldClassNames, dataAttributes } = useStyles( - 'TextField', - { - size, - }, - ); + const { classNames: textFieldClassNames } = useStyles('TextField'); - const { classNames: searchFieldClassNames } = useStyles('SearchField', {}); + const { + classNames: searchFieldClassNames, + dataAttributes, + style, + cleanedProps, + } = useStyles('SearchField', { + size: 'small', + placeholder: 'Search', + startCollapsed: false, + ...props, + }); + + const { + className, + description, + icon, + isRequired, + secondaryLabel, + placeholder, + startCollapsed, + ...rest + } = cleanedProps; // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = @@ -101,10 +107,10 @@ export const SearchField = forwardRef( {...dataAttributes} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} - data-start-collapsed={startCollapsed} data-collapsed={isCollapsed} onFocusChange={handleClick} onChange={handleChange} + style={style} {...rest} ref={ref} > diff --git a/packages/ui/src/components/TextField/TextField.tsx b/packages/ui/src/components/TextField/TextField.tsx index 2756c3c85f..84b4642d93 100644 --- a/packages/ui/src/components/TextField/TextField.tsx +++ b/packages/ui/src/components/TextField/TextField.tsx @@ -27,17 +27,9 @@ import { useStyles } from '../../hooks/useStyles'; export const TextField = forwardRef( (props, ref) => { const { - className, - icon, - size = 'small', label, - secondaryLabel, - description, - isRequired, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, - placeholder, - ...rest } = props; useEffect(() => { @@ -48,9 +40,23 @@ export const TextField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames, dataAttributes } = useStyles('TextField', { - size, - }); + const { classNames, dataAttributes, style, cleanedProps } = useStyles( + 'TextField', + { + size: 'small', + ...props, + }, + ); + + const { + className, + description, + icon, + isRequired, + secondaryLabel, + placeholder, + ...rest + } = cleanedProps; // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = @@ -62,6 +68,7 @@ export const TextField = forwardRef( {...dataAttributes} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} + style={style} {...rest} ref={ref} > diff --git a/packages/ui/src/css/utilities/mb.css b/packages/ui/src/css/utilities/mb.css index 6394937af4..d2c3df3779 100644 --- a/packages/ui/src/css/utilities/mb.css +++ b/packages/ui/src/css/utilities/mb.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mb { - margin-bottom: var(--pb-xs); + margin-bottom: var(--mb-xs); } .xs\:bui-mb-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mb { - margin-bottom: var(--pb-sm); + margin-bottom: var(--mb-sm); } .sm\:bui-mb-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mb { - margin-bottom: var(--pb-md); + margin-bottom: var(--mb-md); } .md\:bui-mb-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mb { - margin-bottom: var(--pb-lg); + margin-bottom: var(--mb-lg); } .lg\:bui-mb-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mb { - margin-bottom: var(--pb-xl); + margin-bottom: var(--mb-xl); } .xl\:bui-mb-0\.5 { diff --git a/packages/ui/src/css/utilities/ml.css b/packages/ui/src/css/utilities/ml.css index 7b82b73ab8..bfa17b1700 100644 --- a/packages/ui/src/css/utilities/ml.css +++ b/packages/ui/src/css/utilities/ml.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-ml { - margin-left: var(--pl-xs); + margin-left: var(--ml-xs); } .xs\:bui-ml-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-ml { - margin-left: var(--pl-sm); + margin-left: var(--ml-sm); } .sm\:bui-ml-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-ml { - margin-left: var(--pl-md); + margin-left: var(--ml-md); } .md\:bui-ml-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-ml { - margin-left: var(--pl-lg); + margin-left: var(--ml-lg); } .lg\:bui-ml-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-ml { - margin-left: var(--pl-xl); + margin-left: var(--ml-xl); } .xl\:bui-ml-0\.5 { diff --git a/packages/ui/src/css/utilities/mr.css b/packages/ui/src/css/utilities/mr.css index bac16b3ed3..96704994d8 100644 --- a/packages/ui/src/css/utilities/mr.css +++ b/packages/ui/src/css/utilities/mr.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mr { - margin-right: var(--pr-xs); + margin-right: var(--mr-xs); } .xs\:bui-mr-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mr { - margin-right: var(--pr-sm); + margin-right: var(--mr-sm); } .sm\:bui-mr-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mr { - margin-right: var(--pr-md); + margin-right: var(--mr-md); } .md\:bui-mr-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mr { - margin-right: var(--pr-lg); + margin-right: var(--mr-lg); } .lg\:bui-mr-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mr { - margin-right: var(--pr-xl); + margin-right: var(--mr-xl); } .xl\:bui-mr-0\.5 { diff --git a/packages/ui/src/css/utilities/mt.css b/packages/ui/src/css/utilities/mt.css index 2f6ecc4aa8..d72cac5f93 100644 --- a/packages/ui/src/css/utilities/mt.css +++ b/packages/ui/src/css/utilities/mt.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mt { - margin-top: var(--pt-xs); + margin-top: var(--mt-xs); } .xs\:bui-mt-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mt { - margin-top: var(--pt-sm); + margin-top: var(--mt-sm); } .sm\:bui-mt-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mt { - margin-top: var(--pt-md); + margin-top: var(--mt-md); } .md\:bui-mt-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mt { - margin-top: var(--pt-lg); + margin-top: var(--mt-lg); } .lg\:bui-mt-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mt { - margin-top: var(--pt-xl); + margin-top: var(--mt-xl); } .xl\:bui-mt-0\.5 { diff --git a/packages/ui/src/css/utilities/mx.css b/packages/ui/src/css/utilities/mx.css index 4898fc2352..f5c06b8b4c 100644 --- a/packages/ui/src/css/utilities/mx.css +++ b/packages/ui/src/css/utilities/mx.css @@ -103,8 +103,8 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mx { - margin-left: var(--px-xs); - margin-right: var(--px-xs); + margin-left: var(--mx-xs); + margin-right: var(--mx-xs); } .xs\:bui-mx-0\.5 { @@ -191,8 +191,8 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mx { - margin-left: var(--px-sm); - margin-right: var(--px-sm); + margin-left: var(--mx-sm); + margin-right: var(--mx-sm); } .sm\:bui-mx-0\.5 { @@ -279,8 +279,8 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mx { - margin-left: var(--px-md); - margin-right: var(--px-md); + margin-left: var(--mx-md); + margin-right: var(--mx-md); } .md\:bui-mx-0\.5 { @@ -367,8 +367,8 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mx { - margin-left: var(--px-lg); - margin-right: var(--px-lg); + margin-left: var(--mx-lg); + margin-right: var(--mx-lg); } .lg\:bui-mx-0\.5 { @@ -455,8 +455,8 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mx { - margin-left: var(--px-xl); - margin-right: var(--px-xl); + margin-left: var(--mx-xl); + margin-right: var(--mx-xl); } .xl\:bui-mx-0\.5 { diff --git a/packages/ui/src/css/utilities/my.css b/packages/ui/src/css/utilities/my.css index a165ad478e..f00167f88e 100644 --- a/packages/ui/src/css/utilities/my.css +++ b/packages/ui/src/css/utilities/my.css @@ -103,8 +103,8 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-my { - margin-top: var(--py-xs); - margin-bottom: var(--py-xs); + margin-top: var(--my-xs); + margin-bottom: var(--my-xs); } .xs\:bui-my-0\.5 { @@ -191,8 +191,8 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-my { - margin-top: var(--py-sm); - margin-bottom: var(--py-sm); + margin-top: var(--my-sm); + margin-bottom: var(--my-sm); } .sm\:bui-my-0\.5 { @@ -279,8 +279,8 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-my { - margin-top: var(--py-md); - margin-bottom: var(--py-md); + margin-top: var(--my-md); + margin-bottom: var(--my-md); } .md\:bui-my-0\.5 { @@ -367,8 +367,8 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-my { - margin-top: var(--py-lg); - margin-bottom: var(--py-lg); + margin-top: var(--my-lg); + margin-bottom: var(--my-lg); } .lg\:bui-my-0\.5 { @@ -455,8 +455,8 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-my { - margin-top: var(--py-xl); - margin-bottom: var(--py-xl); + margin-top: var(--my-xl); + margin-bottom: var(--my-xl); } .xl\:bui-my-0\.5 { diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index 19f9823236..db0bb98be3 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -16,6 +16,7 @@ import { useBreakpoint, breakpoints } from './useBreakpoint'; import { componentDefinitions } from '../utils/componentDefinitions'; import type { ComponentDefinitionName, ComponentClassNames } from '../types'; +import { utilityClassMap } from '../utils/utilityClassMap'; /** * Resolve a responsive value based on the current breakpoint @@ -55,8 +56,8 @@ function resolveResponsiveValue( /** * React hook to get class names and data attributes for a component with responsive support * @param componentName - The name of the component - * @param props - Object with prop values (can be responsive) - * @returns Object with classNames and dataAttributes + * @param props - All component props + * @returns Object with classNames, dataAttributes, utilityClasses, style, and cleanedProps */ export function useStyles( componentName: T, @@ -64,29 +65,135 @@ export function useStyles( ): { classNames: ComponentClassNames; dataAttributes: Record; - resolvedProps: Record; + utilityClasses: string; + style: React.CSSProperties; + cleanedProps: Record; } { const { breakpoint } = useBreakpoint(); - const classNames = componentDefinitions[componentName] - .classNames as ComponentClassNames; + const componentDefinition = componentDefinitions[componentName]; + const classNames = componentDefinition.classNames as ComponentClassNames; + const utilityPropNames = + ('utilityProps' in componentDefinition + ? componentDefinition.utilityProps + : []) || []; - // Resolve responsive values and generate data attributes + // Extract data attribute names from component definition + const dataAttributeNames = + 'dataAttributes' in componentDefinition + ? Object.keys(componentDefinition.dataAttributes || {}) + : []; + + // Extract existing style from props + const incomingStyle = props.style || {}; + + // Generate data attributes from component definition const dataAttributes: Record = {}; - const resolvedProps: Record = {}; - - for (const [key, value] of Object.entries(props)) { + for (const key of dataAttributeNames) { + const value = props[key]; if (value !== undefined && value !== null) { - const resolvedValue = resolveResponsiveValue(value, breakpoint); - if (resolvedValue !== undefined) { - resolvedProps[key] = resolvedValue; - dataAttributes[`data-${key}`] = resolvedValue; + // Handle boolean and number values directly + if (typeof value === 'boolean' || typeof value === 'number') { + dataAttributes[`data-${key}`] = String(value); + } else { + const resolvedValue = resolveResponsiveValue(value, breakpoint); + if (resolvedValue !== undefined) { + dataAttributes[`data-${key}`] = resolvedValue; + } } } } + // Generate utility classes and custom styles from component's allowed utility props + const utilityClassList: string[] = []; + const generatedStyle: React.CSSProperties = {}; + + const handleUtilityValue = ( + key: string, + val: unknown, + prefix: string = '', + ) => { + // Get utility class configuration for this key + const utilityConfig = utilityClassMap[key as keyof typeof utilityClassMap]; + + if (!utilityConfig) { + // Skip if no config found for this key + return; + } + + // Check if value is in the list of valid values for this utility + if ( + utilityConfig.values.length > 0 && + utilityConfig.values.includes(val as string | number) + ) { + // Generate utility class with value suffix and optional breakpoint prefix + const className = prefix + ? `${prefix}${utilityConfig.class}-${val}` + : `${utilityConfig.class}-${val}`; + utilityClassList.push(className); + } else if (utilityConfig.cssVar) { + // Custom value - add CSS custom property AND utility class name + // Only if cssVar is defined (properties with fixed values don't have cssVar) + const cssVarKey = prefix + ? `${utilityConfig.cssVar}-${prefix.slice(0, -1)}` + : utilityConfig.cssVar; + // CSS custom properties need to be set on the style object as strings + (generatedStyle as Record)[cssVarKey] = val; + + // Add utility class name (without value suffix) with optional breakpoint prefix + const className = prefix + ? `${prefix}${utilityConfig.class}` + : utilityConfig.class; + utilityClassList.push(className); + } + // If no cssVar and value is not in valid values, skip (invalid value for fixed-value property) + }; + + for (const key of utilityPropNames) { + const value = props[key]; + if (value === undefined || value === null) { + continue; + } + + // Check if value is a responsive object + if (typeof value === 'object' && value !== null) { + const breakpointValues = value as { [key: string]: unknown }; + // Handle responsive object values + for (const bp in breakpointValues) { + const prefix = bp === 'initial' ? '' : `${bp}:`; + handleUtilityValue(key, breakpointValues[bp], prefix); + } + } else { + // Handle simple value + handleUtilityValue(key, value); + } + } + + // Create cleaned props by excluding data attributes, utility props, and style + // Component-specific props like 'as' and 'children' remain in cleanedProps + const processedKeys = new Set([ + ...dataAttributeNames, + ...utilityPropNames, + 'style', + ]); + + const cleanedProps = Object.keys(props).reduce((acc, key) => { + if (!processedKeys.has(key)) { + acc[key] = props[key]; + } + return acc; + }, {} as Record); + + // Merge incoming style with generated styles (incoming styles take precedence) + const mergedStyle = { + ...generatedStyle, + ...incomingStyle, + }; + return { classNames, dataAttributes, - resolvedProps, // Also return resolved props for convenience + utilityClasses: utilityClassList.join(' '), + style: mergedStyle, + cleanedProps, }; } diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index d0011bfe8a..db23974186 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -78,7 +78,20 @@ export type BorderRadius = export type Border = 'none' | 'base' | 'error' | 'warning' | 'selected'; /** @public */ -export type Columns = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'auto'; +export type Columns = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | '12' + | 'auto'; /** @public */ export interface SpaceProps { @@ -160,6 +173,7 @@ export type DataAttributesMap = Record; export interface ComponentDefinition { classNames: ClassNamesMap; dataAttributes?: DataAttributesMap; + utilityProps?: string[]; } /** diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index 5f71a2a21f..582e4c7fcc 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -35,6 +35,30 @@ export const componentDefinitions = { classNames: { root: 'bui-Box', }, + utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'position', + 'display', + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + ], }, Button: { classNames: { @@ -84,6 +108,7 @@ export const componentDefinitions = { classNames: { root: 'bui-Container', }, + utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display'], }, Dialog: { classNames: { @@ -107,12 +132,55 @@ export const componentDefinitions = { classNames: { root: 'bui-Flex', }, + utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'gap', + 'align', + 'justify', + 'direction', + ], }, Grid: { classNames: { root: 'bui-Grid', - item: 'bui-GridItem', }, + utilityProps: [ + 'columns', + 'gap', + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + ], + }, + GridItem: { + classNames: { + root: 'bui-GridItem', + }, + utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan'], }, Header: { classNames: { @@ -190,6 +258,9 @@ export const componentDefinitions = { root: 'bui-PasswordField', inputVisibility: 'bui-InputVisibility', }, + dataAttributes: { + size: ['small', 'medium'] as const, + }, }, Popover: { classNames: { @@ -218,6 +289,7 @@ export const componentDefinitions = { }, dataAttributes: { startCollapsed: [true, false] as const, + size: ['small', 'medium'] as const, }, }, Select: { @@ -308,6 +380,7 @@ export const componentDefinitions = { dataAttributes: { invalid: [true, false] as const, disabled: [true, false] as const, + size: ['small', 'medium'] as const, }, }, Tooltip: { diff --git a/packages/ui/src/utils/extractProps.ts b/packages/ui/src/utils/extractProps.ts deleted file mode 100644 index 3914072ff4..0000000000 --- a/packages/ui/src/utils/extractProps.ts +++ /dev/null @@ -1,110 +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. - */ - -type BasePropDef = { - type: string; - values?: readonly unknown[]; - default?: unknown; - required?: boolean; - className?: string; - responsive?: true; - customProperties?: string[]; -}; - -export function extractProps( - props: { - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; - as?: keyof JSX.IntrinsicElements; - [key: string]: any; - }, - propDefs: { [name in string]: BasePropDef }, -) { - let className: string[] = (props.className || '').split(' '); - let style: React.CSSProperties = { ...props.style }; - - for (const key in propDefs) { - const propDef = propDefs[key]; - - // Check if the prop is present or has a default value - if (!Object.hasOwn(props, key) && !propDef.hasOwnProperty('default')) { - continue; // Skip processing if neither is present - } - - const value = Object.hasOwn(props, key) - ? (props[key] as unknown) - : propDefs[key].default; - const propDefsValues = propDef.values; - const propDefsCustomProperties = propDef.customProperties; - const propDefsClassName = propDef.className; - const isResponsive = propDef.responsive; - - const handleValue = (val: unknown, prefix: string = '') => { - // Skip adding class name if the key is "as" - if (key === 'as') return; - - if (propDefsValues?.includes(val)) { - className.push(`${prefix}${propDefsClassName}-${val}`); - } else { - if (propDefsCustomProperties) { - for (const customProperty of propDefsCustomProperties) { - const customPropertyKey = - isResponsive && prefix - ? `${customProperty}-${prefix.slice(0, -1)}` - : customProperty; - style[customPropertyKey as keyof typeof style] = val as any; - } - } - className.push(`${prefix}${propDefsClassName}`); - } - }; - - if (isResponsive && typeof value === 'object' && value !== null) { - const breakpointValues = value as { [key: string]: unknown }; - // Handle responsive object values - for (const breakpoint in breakpointValues) { - const prefix = breakpoint === 'initial' ? '' : `${breakpoint}:`; - handleValue(breakpointValues[breakpoint], prefix); - } - } else { - handleValue(value); - } - } - - // Ensure keys from props that are defined in propDefs are removed - const { cleanedProps, dataProps } = Object.keys(props).reduce( - (acc, key) => { - if (key.startsWith('data-')) { - acc.dataProps[key] = props[key]; - } - if (!propDefs.hasOwnProperty(key)) { - acc.cleanedProps[key] = props[key]; - } - return acc; - }, - { dataProps: {}, cleanedProps: {} } as { - dataProps: { [key: string]: any }; - cleanedProps: { [key: string]: any }; - }, - ); - - const newClassNames = className - .filter(name => name && name.trim() !== '') - .join(' '); - - return { ...cleanedProps, className: newClassNames, style, dataProps }; -} diff --git a/packages/ui/src/utils/utilityClassMap.ts b/packages/ui/src/utils/utilityClassMap.ts new file mode 100644 index 0000000000..261d43632d --- /dev/null +++ b/packages/ui/src/utils/utilityClassMap.ts @@ -0,0 +1,202 @@ +/* + * 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. + */ + +// Valid spacing values that have predefined utility classes +const validSpacingValues = [ + '0.5', + '1', + '1.5', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', +] as const; + +const columnsValues = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + 'auto', +] as const; + +export const utilityClassMap: Record< + string, + { class: string; cssVar?: string; values: readonly (string | number)[] } +> = { + m: { + class: 'bui-m', + cssVar: '--m', + values: validSpacingValues, + }, + mb: { + class: 'bui-mb', + cssVar: '--mb', + values: validSpacingValues, + }, + ml: { + class: 'bui-ml', + cssVar: '--ml', + values: validSpacingValues, + }, + mr: { + class: 'bui-mr', + cssVar: '--mr', + values: validSpacingValues, + }, + mt: { + class: 'bui-mt', + cssVar: '--mt', + values: validSpacingValues, + }, + mx: { + class: 'bui-mx', + cssVar: '--mx', + values: validSpacingValues, + }, + my: { + class: 'bui-my', + cssVar: '--my', + values: validSpacingValues, + }, + p: { + class: 'bui-p', + cssVar: '--p', + values: validSpacingValues, + }, + pb: { + class: 'bui-pb', + cssVar: '--pb', + values: validSpacingValues, + }, + pl: { + class: 'bui-pl', + cssVar: '--pl', + values: validSpacingValues, + }, + pr: { + class: 'bui-pr', + cssVar: '--pr', + values: validSpacingValues, + }, + pt: { + class: 'bui-pt', + cssVar: '--pt', + values: validSpacingValues, + }, + px: { + class: 'bui-px', + cssVar: '--px', + values: validSpacingValues, + }, + py: { + class: 'bui-py', + cssVar: '--py', + values: validSpacingValues, + }, + width: { + class: 'bui-w', + cssVar: '--width', + values: [], // Always use custom value + }, + minWidth: { + class: 'bui-min-w', + cssVar: '--min-width', + values: [], // Always use custom value + }, + maxWidth: { + class: 'bui-max-w', + cssVar: '--max-width', + values: [], // Always use custom value + }, + height: { + class: 'bui-h', + cssVar: '--height', + values: [], // Always use custom value + }, + minHeight: { + class: 'bui-min-h', + cssVar: '--min-height', + values: [], // Always use custom value + }, + maxHeight: { + class: 'bui-max-h', + cssVar: '--max-height', + values: [], // Always use custom value + }, + gap: { + class: 'bui-gap', + cssVar: '--gap', + values: validSpacingValues, + }, + position: { + class: 'bui-position', + values: ['static', 'relative', 'absolute', 'fixed', 'sticky'], + }, + display: { + class: 'bui-display', + values: ['none', 'flex', 'block', 'inline'], + }, + align: { + class: 'bui-align', + values: ['start', 'center', 'end', 'baseline', 'stretch'], + }, + justify: { + class: 'bui-jc', + values: ['start', 'center', 'end', 'between'], + }, + direction: { + class: 'bui-fd', + values: ['row', 'column', 'row-reverse', 'column-reverse'], + }, + columns: { + class: 'bui-columns', + values: columnsValues, + }, + colSpan: { + class: 'bui-col-span', + values: columnsValues, + }, + colEnd: { + class: 'bui-col-end', + values: columnsValues, + }, + colStart: { + class: 'bui-col-start', + values: columnsValues, + }, + rowSpan: { + class: 'bui-row-span', + values: columnsValues, + }, +};