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,
+ },
+};