Merge pull request #31389 from backstage/bui-restructure-usestyles

BUI - Restructure useStyles hook
This commit is contained in:
Patrik Oldsberg
2025-10-13 11:50:54 +02:00
committed by GitHub
39 changed files with 1180 additions and 1120 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Fix margin utility classes in Backstage UI.
+1 -1
View File
@@ -22,7 +22,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<Snippet
py={4}
preview={<BoxSnippet story="Preview" />}
preview={<BoxSnippet story="Default" />}
code={boxPreviewSnippet}
align="center"
/>
+41 -41
View File
@@ -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 {
+112 -179
View File
@@ -107,44 +107,34 @@ export const Box: ForwardRefExoticComponent<
BoxProps & RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
export type BoxOwnProps = GetPropDefTypes<typeof boxPropDefs>;
// @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<string>;
// (undocumented)
maxHeight?: HeightProps['maxHeight'];
maxHeight?: Responsive<string>;
// (undocumented)
maxWidth?: WidthProps['maxWidth'];
maxWidth?: Responsive<string>;
// (undocumented)
minHeight?: HeightProps['minHeight'];
minHeight?: Responsive<string>;
// (undocumented)
minWidth?: WidthProps['minWidth'];
minWidth?: Responsive<string>;
// (undocumented)
position?: PositionProps['position'];
position?: Responsive<
'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'
>;
// (undocumented)
style?: React.CSSProperties;
// (undocumented)
width?: WidthProps['width'];
width?: Responsive<string>;
}
// @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<T extends ComponentDefinitionName> =
@@ -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<typeof flexPropDefs>;
// @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<Space>;
// (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<typeof gridItemPropDefs>;
// @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<Columns>;
// (undocumented)
colSpan?: GridItemOwnProps['colSpan'];
colSpan?: Responsive<Columns>;
// (undocumented)
colStart?: GridItemOwnProps['colStart'];
colStart?: Responsive<Columns>;
// (undocumented)
rowSpan?: GridItemOwnProps['rowSpan'];
rowSpan?: Responsive<Columns>;
// (undocumented)
style?: React.CSSProperties;
}
// @public (undocumented)
export type GridOwnProps = GetPropDefTypes<typeof gridPropDefs>;
// @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<Columns>;
// (undocumented)
gap?: GapProps['gap'];
gap?: Responsive<Space>;
// (undocumented)
style?: React.CSSProperties;
}
@@ -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<typeof boxPropDefs>;
export { boxPropDefs };
export type { BoxOwnProps };
+265 -171
View File
@@ -36,35 +36,277 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
const Card = () => {
return (
<div
style={{
width: '64px',
height: '64px',
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
color: '#2563eb',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
};
const diagonalStripePattern = (() => {
const svg = `
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<g fill="#2563eb" fill-opacity="0.6" fill-rule="evenodd">
<path d="M5 0h1L0 6V5zM6 5v1H5z"/>
</g>
</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: <Card />,
display: 'inline',
},
export const Margin: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} ml="6" children="ML" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mr="6" children="MR" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mx="6" children="MX" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mt="6" children="MT" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mb="6" children="MB" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} my="6" children="MY" />
</Box>
</Flex>
),
};
export const ResponsiveMargin: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} ml={{ initial: '2', sm: '6' }} children="ML" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mr={{ initial: '2', sm: '6' }} children="MR" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mx={{ initial: '2', sm: '6' }} children="MX" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mt={{ initial: '2', sm: '6' }} children="MT" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mb={{ initial: '2', sm: '6' }} children="MB" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} my={{ initial: '2', sm: '6' }} children="MY" />
</Box>
</Flex>
),
};
export const CustomMargin: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} ml="42px" children="ML" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mr="42px" children="MR" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mx="42px" children="MX" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mt="42px" children="MT" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mb="42px" children="MB" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} my="42px" children="MY" />
</Box>
</Flex>
),
};
export const CustomResponsiveMargin: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} ml={{ initial: '9px', sm: '42px' }} children="ML" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mr={{ initial: '9px', sm: '42px' }} children="MR" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mx={{ initial: '9px', sm: '42px' }} children="MX" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mt={{ initial: '9px', sm: '42px' }} children="MT" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} mb={{ initial: '9px', sm: '42px' }} children="MB" />
</Box>
<Box style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} my={{ initial: '9px', sm: '42px' }} children="MY" />
</Box>
</Flex>
),
};
export const Padding: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box pl="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PL" />
</Box>
<Box pr="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PR" />
</Box>
<Box px="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PX" />
</Box>
<Box pt="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PT" />
</Box>
<Box pb="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PB" />
</Box>
<Box py="6" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PY" />
</Box>
</Flex>
),
};
export const ResponsivePadding: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box
pl={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PL" />
</Box>
<Box
pr={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PR" />
</Box>
<Box
px={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PX" />
</Box>
<Box
pt={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PT" />
</Box>
<Box
pb={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PB" />
</Box>
<Box
py={{ initial: '2', sm: '6' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PY" />
</Box>
</Flex>
),
};
export const CustomPadding: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box pl="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PL" />
</Box>
<Box pr="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PR" />
</Box>
<Box px="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PX" />
</Box>
<Box pt="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PT" />
</Box>
<Box pb="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PB" />
</Box>
<Box py="42px" style={{ border: '1px solid red', borderRadius: '5px' }}>
<Box {...args} children="PY" />
</Box>
</Flex>
),
};
export const CustomResponsivePadding: Story = {
args: { ...Default.args },
render: args => (
<Flex align="center">
<Box
pl={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PL" />
</Box>
<Box
pr={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PR" />
</Box>
<Box
px={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PX" />
</Box>
<Box
pt={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PT" />
</Box>
<Box
pb={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PB" />
</Box>
<Box
py={{ initial: '9px', sm: '42px' }}
style={{ border: '1px solid red', borderRadius: '5px' }}
>
<Box {...args} children="PY" />
</Box>
</Flex>
),
};
const CardDisplay = ({ children }: { children?: ReactNode }) => {
@@ -105,151 +347,3 @@ export const Display: Story = {
</Flex>
),
};
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 => (
<Flex direction="column" align="center" gap="4">
<Flex gap="4" align="center">
<Box p="3" style={styleInsideBox}>
<Box {...args}>Padding</Box>
</Box>
<Box px="3" style={styleInsideBox}>
<Box {...args}>Padding X</Box>
</Box>
<Box py="3" style={styleInsideBox}>
<Box {...args}>Padding Y</Box>
</Box>
</Flex>
<Flex gap="4" align="center">
<Box pt="3" style={styleInsideBox}>
<Box {...args}>Padding Top</Box>
</Box>
<Box pr="3" style={styleInsideBox}>
<Box {...args}>Padding Right</Box>
</Box>
<Box pb="3" style={styleInsideBox}>
<Box {...args}>Padding Bottom</Box>
</Box>
<Box pl="3" style={styleInsideBox}>
<Box {...args}>Padding Left</Box>
</Box>
</Flex>
<Flex gap="4" align="center">
<Box p="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding</Box>
</Box>
<Box px="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding X</Box>
</Box>
<Box py="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Y</Box>
</Box>
</Flex>
<Flex gap="4" align="center">
<Box pt="10px" pb="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Top Bottom</Box>
</Box>
<Box pl="10px" pr="10px" style={styleInsideBox}>
<Box {...args}>Custom Padding Left Right</Box>
</Box>
</Flex>
</Flex>
),
};
export const Margin: Story = {
args: {
style: {
background: '#1f47ff',
color: 'white',
borderRadius: '4px',
padding: '12px 12px',
},
},
render: args => (
<Flex direction="column" align="center" gap="4">
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box m="3" {...args}>
Margin
</Box>
</Box>
<Box style={styleInsideBox}>
<Box mx="3" {...args}>
Margin X
</Box>
</Box>
<Box style={styleInsideBox}>
<Box my="3" {...args}>
Margin Y
</Box>
</Box>
</Flex>
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box mt="3" {...args}>
Margin Top
</Box>
</Box>
<Box style={styleInsideBox}>
<Box mr="3" {...args}>
Margin Right
</Box>
</Box>
<Box style={styleInsideBox}>
<Box mb="3" {...args}>
Margin Bottom
</Box>
</Box>
<Box style={styleInsideBox}>
<Box ml="3" {...args}>
Margin Left
</Box>
</Box>
</Flex>
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box m="10px" {...args}>
Custom Margin
</Box>
</Box>
<Box style={styleInsideBox}>
<Box mx="10px" {...args}>
Custom Margin X
</Box>
</Box>
<Box style={styleInsideBox}>
<Box my="10px" {...args}>
Custom Margin Y
</Box>
</Box>
</Flex>
<Flex align="center" gap="4">
<Box style={styleInsideBox}>
<Box mt="10px" mb="10px" {...args}>
Custom Margin Top Bottom
</Box>
</Box>
<Box style={styleInsideBox}>
<Box ml="10px" mr="10px" {...args}>
Custom Margin Left Right
</Box>
</Box>
</Flex>
</Flex>
),
};
+14 -25
View File
@@ -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<HTMLDivElement, BoxProps>((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';
-2
View File
@@ -15,5 +15,3 @@
*/
export { Box } from './Box';
export type * from './types';
export type { BoxOwnProps } from './Box.props';
export { boxPropDefs } from './Box.props';
+12 -15
View File
@@ -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<string>;
minWidth?: Responsive<string>;
maxWidth?: Responsive<string>;
height?: Responsive<string>;
minHeight?: Responsive<string>;
maxHeight?: Responsive<string>;
position?: Responsive<
'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'
>;
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
+6 -13
View File
@@ -23,21 +23,14 @@ import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Button = forwardRef(
(props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
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 (
<RAButton
className={clsx(classNames.root, className)}
@@ -17,7 +17,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ButtonIcon } from './ButtonIcon';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { Icon } from '../Icon';
const meta = {
@@ -84,42 +83,3 @@ export const Responsive: Story = {
},
render: args => <ButtonIcon {...args} icon={<Icon name="cloud" />} />,
};
const variants = ['primary', 'secondary'] as const;
const sizes = ['small', 'medium'] as const;
export const Playground: Story = {
render: args => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{sizes.map(size => (
<Flex align="center" key={size}>
<ButtonIcon
{...args}
variant={variant}
size={size}
icon={<Icon name="cloud" />}
/>
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
variant={variant}
size={size}
/>
<ButtonIcon
{...args}
icon={<Icon name="chevron-right" />}
aria-label="Chevron right icon button"
variant={variant}
size={size}
/>
</Flex>
))}
</Flex>
))}
</Flex>
),
};
@@ -23,22 +23,16 @@ import { useStyles } from '../../hooks/useStyles';
/** @public */
export const ButtonIcon = forwardRef(
(props: ButtonIconProps, ref: Ref<HTMLButtonElement>) => {
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 (
<RAButton
className={clsx(classNames.root, classNamesButtonIcon.root, className)}
@@ -17,7 +17,6 @@
import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite';
import { ButtonLink } from './ButtonLink';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { Icon } from '../Icon';
import { MemoryRouter } from 'react-router-dom';
@@ -162,70 +161,3 @@ export const Responsive: Story = {
},
},
};
const variants = ['primary', 'secondary'] as const;
const sizes = ['small', 'medium'] as const;
export const Playground: Story = {
args: {
children: 'Button',
},
render: () => (
<Flex direction="column">
{variants.map(variant => (
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{sizes.map(size => (
<Flex align="center" key={size}>
<ButtonLink variant={variant} size={size}>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevron-right" />}
style={{ width: '200px' }}
variant={variant}
size={size}
>
Button
</ButtonLink>
<ButtonLink variant={variant} size={size} isDisabled>
Button
</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<Icon name="chevron-right" />}
variant={variant}
size={size}
isDisabled
>
Button
</ButtonLink>
</Flex>
))}
</Flex>
))}
</Flex>
),
};
@@ -26,24 +26,18 @@ import { isExternalLink } from '../../utils/isExternalLink';
export const ButtonLink = forwardRef(
(props: ButtonLinkProps, ref: Ref<HTMLAnchorElement>) => {
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
@@ -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<HTMLDivElement, ContainerProps>(
(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 (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
style={style}
{...rest}
/>
);
},
);
@@ -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<typeof flexPropDefs>;
export { flexPropDefs };
export type { FlexOwnProps };
+126 -44
View File
@@ -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<typeof Flex>;
export default meta;
type Story = StoryObj<typeof meta>;
const DecorativeBox = () => {
const DecorativeBox = ({
width = '48px',
height = '48px',
}: {
width?: string;
height?: string;
}) => {
const diagonalStripePattern = (() => {
const svg = `
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<g fill="#2563eb" fill-opacity="0.6" fill-rule="evenodd">
<path d="M5 0h1L0 6V5zM6 5v1H5z"/>
</g>
</svg>
`.trim();
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
})();
return (
<div
<Box
width={width}
height={height}
style={{
background: '#eaf2fd',
borderRadius: '4px',
boxShadow: '0 0 0 1px #2563eb',
height: '32px',
minWidth: '100px',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
border: '1px solid #2563eb',
backgroundImage: `url("${diagonalStripePattern}")`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
color: '#2563eb',
}}
/>
);
@@ -85,49 +104,109 @@ export const RowDirection: Story = {
},
};
export const AlignLeft: Story = {
render: () => (
<Flex align="start">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
export const AlignStartInColumn: Story = {
args: {
align: 'start',
direction: 'column',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignCenter: Story = {
render: () => (
<Flex align="center">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
export const AlignStartInRow: Story = {
args: {
align: 'start',
direction: 'row',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignRight: Story = {
render: () => (
<Flex align="end">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
export const AlignCenterInColumn: Story = {
args: {
align: 'center',
direction: 'column',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignCenterInRow: Story = {
args: {
align: 'center',
direction: 'row',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignEndInColumn: Story = {
args: {
align: 'end',
direction: 'column',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignEndInRow: Story = {
args: {
align: 'end',
direction: 'row',
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const ResponsiveAlign: Story = {
render: () => (
<Flex align={{ xs: 'start', md: 'center', lg: 'end' }}>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
args: {
align: { xs: 'start', md: 'center', lg: 'end' },
},
render: args => (
<Flex {...args}>
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const ResponsiveGap: Story = {
render: () => (
<Flex gap={{ xs: '4', md: '8', lg: '12' }}>
args: {
gap: { xs: '4', md: '8', lg: '12' },
},
render: args => (
<Flex {...args}>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
@@ -136,8 +215,11 @@ export const ResponsiveGap: Story = {
};
export const LargeGap: Story = {
render: () => (
<Flex gap="8">
args: {
gap: '8',
},
render: args => (
<Flex {...args}>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
+14 -19
View File
@@ -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<HTMLDivElement, FlexProps>((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 (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
style={style}
{...rest}
/>
);
});
-2
View File
@@ -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';
+6 -8
View File
@@ -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<Space>;
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;
}
@@ -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<typeof gridPropDefs>;
/** @public */
type GridItemOwnProps = GetPropDefTypes<typeof gridItemPropDefs>;
export { gridPropDefs, gridItemPropDefs };
export type { GridOwnProps, GridItemOwnProps };
+27 -32
View File
@@ -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<HTMLDivElement, GridProps>((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 (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
style={style}
{...rest}
/>
);
});
const GridItem = forwardRef<HTMLDivElement, GridItemProps>((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 (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
style={style}
{...rest}
/>
);
});
/** @public */
+1 -2
View File
@@ -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';
+7 -9
View File
@@ -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<Columns>;
gap?: Responsive<Space>;
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<Columns>;
colEnd?: Responsive<Columns>;
colStart?: Responsive<Columns>;
rowSpan?: Responsive<Columns>;
style?: React.CSSProperties;
}
@@ -32,17 +32,9 @@ import { RiEyeLine, RiEyeOffLine } from '@remixicon/react';
export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
(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<HTMLDivElement, PasswordFieldProps>(
}
}, [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<HTMLDivElement, PasswordFieldProps>(
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
type="password"
style={style}
{...rest}
ref={ref}
>
@@ -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;
@@ -32,19 +32,9 @@ import type { SearchFieldProps } from './types';
export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
(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<HTMLDivElement, SearchFieldProps>(
}
}, [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<HTMLDivElement, SearchFieldProps>(
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
data-start-collapsed={startCollapsed}
data-collapsed={isCollapsed}
onFocusChange={handleClick}
onChange={handleChange}
style={style}
{...rest}
ref={ref}
>
@@ -27,17 +27,9 @@ import { useStyles } from '../../hooks/useStyles';
export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
(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<HTMLDivElement, TextFieldProps>(
}
}, [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<HTMLDivElement, TextFieldProps>(
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
style={style}
{...rest}
ref={ref}
>
+5 -5
View File
@@ -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 {
+5 -5
View File
@@ -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 {
+5 -5
View File
@@ -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 {
+5 -5
View File
@@ -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 {
+10 -10
View File
@@ -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 {
+10 -10
View File
@@ -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 {
+121 -14
View File
@@ -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<T extends ComponentDefinitionName>(
componentName: T,
@@ -64,29 +65,135 @@ export function useStyles<T extends ComponentDefinitionName>(
): {
classNames: ComponentClassNames<T>;
dataAttributes: Record<string, string>;
resolvedProps: Record<string, string>;
utilityClasses: string;
style: React.CSSProperties;
cleanedProps: Record<string, any>;
} {
const { breakpoint } = useBreakpoint();
const classNames = componentDefinitions[componentName]
.classNames as ComponentClassNames<T>;
const componentDefinition = componentDefinitions[componentName];
const classNames = componentDefinition.classNames as ComponentClassNames<T>;
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<string, string> = {};
const resolvedProps: Record<string, string> = {};
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<string, unknown>)[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<string, any>);
// 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,
};
}
+15 -1
View File
@@ -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<string, DataAttributeValues>;
export interface ComponentDefinition {
classNames: ClassNamesMap;
dataAttributes?: DataAttributesMap;
utilityProps?: string[];
}
/**
+74 -1
View File
@@ -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: {
-110
View File
@@ -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 };
}
+202
View File
@@ -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,
},
};