Clean Flex

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-10-11 09:47:35 +01:00
parent 749ec3cd38
commit 5289a4d961
9 changed files with 255 additions and 272 deletions
@@ -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 };
-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';
@@ -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 };
@@ -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',
@@ -43,17 +44,38 @@ const meta = {
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,12 +107,12 @@ export const RowDirection: Story = {
},
};
export const AlignLeft: Story = {
export const AlignStart: Story = {
render: () => (
<Flex align="start">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
@@ -98,19 +120,19 @@ export const AlignLeft: Story = {
export const AlignCenter: Story = {
render: () => (
<Flex align="center">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
export const AlignRight: Story = {
export const AlignEnd: Story = {
render: () => (
<Flex align="end">
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
@@ -118,9 +140,9 @@ export const AlignRight: Story = {
export const ResponsiveAlign: Story = {
render: () => (
<Flex align={{ xs: 'start', md: 'center', lg: 'end' }}>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
<DecorativeBox height="32px" />
<DecorativeBox height="24px" />
<DecorativeBox height="48px" />
</Flex>
),
};
+16 -19
View File
@@ -14,31 +14,28 @@
* 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 { children, ...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)}
style={style}
{...rest}
>
{children}
</div>
);
});
-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';
+10 -137
View File
@@ -16,139 +16,7 @@
import { useBreakpoint, breakpoints } from './useBreakpoint';
import { componentDefinitions } from '../utils/componentDefinitions';
import type { ComponentDefinitionName, ComponentClassNames } from '../types';
// Valid spacing values that have predefined utility classes
const VALID_SPACING_VALUES = [
'0.5',
'1',
'1.5',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'13',
'14',
] as const;
const utilityClassMap = {
m: {
class: 'bui-m',
cssVar: '--m',
values: VALID_SPACING_VALUES,
},
mb: {
class: 'bui-mb',
cssVar: '--mb',
values: VALID_SPACING_VALUES,
},
ml: {
class: 'bui-ml',
cssVar: '--ml',
values: VALID_SPACING_VALUES,
},
mr: {
class: 'bui-mr',
cssVar: '--mr',
values: VALID_SPACING_VALUES,
},
mt: {
class: 'bui-mt',
cssVar: '--mt',
values: VALID_SPACING_VALUES,
},
mx: {
class: 'bui-mx',
cssVar: '--mx',
values: VALID_SPACING_VALUES,
},
my: {
class: 'bui-my',
cssVar: '--my',
values: VALID_SPACING_VALUES,
},
p: {
class: 'bui-p',
cssVar: '--p',
values: VALID_SPACING_VALUES,
},
pb: {
class: 'bui-pb',
cssVar: '--pb',
values: VALID_SPACING_VALUES,
},
pl: {
class: 'bui-pl',
cssVar: '--pl',
values: VALID_SPACING_VALUES,
},
pr: {
class: 'bui-pr',
cssVar: '--pr',
values: VALID_SPACING_VALUES,
},
pt: {
class: 'bui-pt',
cssVar: '--pt',
values: VALID_SPACING_VALUES,
},
px: {
class: 'bui-px',
cssVar: '--px',
values: VALID_SPACING_VALUES,
},
py: {
class: 'bui-py',
cssVar: '--py',
values: VALID_SPACING_VALUES,
},
width: {
class: 'bui-w',
cssVar: '--width',
values: VALID_SPACING_VALUES,
},
minWidth: {
class: 'bui-min-w',
cssVar: '--min-width',
values: VALID_SPACING_VALUES,
},
maxWidth: {
class: 'bui-max-w',
cssVar: '--max-width',
values: VALID_SPACING_VALUES,
},
height: {
class: 'bui-h',
cssVar: '--height',
values: VALID_SPACING_VALUES,
},
minHeight: {
class: 'bui-min-h',
cssVar: '--min-height',
values: VALID_SPACING_VALUES,
},
maxHeight: {
class: 'bui-max-h',
cssVar: '--max-height',
values: VALID_SPACING_VALUES,
},
position: {
class: 'bui-position',
cssVar: '--position',
values: ['static', 'relative', 'absolute', 'fixed', 'sticky'],
},
display: {
class: 'bui-display',
cssVar: '--display',
values: ['none', 'flex', 'block', 'inline'],
},
};
import { utilityClassMap } from '../utils/utilityClassMap';
/**
* Resolve a responsive value based on the current breakpoint
@@ -248,19 +116,23 @@ export function useStyles<T extends ComponentDefinitionName>(
}
// Check if value is in the list of valid values for this utility
if (utilityConfig.values.includes(val as any)) {
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 {
} 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 as any since they're not part of CSSProperties
(generatedStyle as any)[cssVarKey] = val;
// 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
@@ -268,6 +140,7 @@ export function useStyles<T extends ComponentDefinitionName>(
: 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) {
@@ -121,6 +121,26 @@ 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: {
+166
View File
@@ -0,0 +1,166 @@
/*
* 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 VALID_SPACING_VALUES = [
'0.5',
'1',
'1.5',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'13',
'14',
] as const;
export const utilityClassMap: Record<
string,
{ class: string; cssVar?: string; values: readonly (string | number)[] }
> = {
m: {
class: 'bui-m',
cssVar: '--m',
values: VALID_SPACING_VALUES,
},
mb: {
class: 'bui-mb',
cssVar: '--mb',
values: VALID_SPACING_VALUES,
},
ml: {
class: 'bui-ml',
cssVar: '--ml',
values: VALID_SPACING_VALUES,
},
mr: {
class: 'bui-mr',
cssVar: '--mr',
values: VALID_SPACING_VALUES,
},
mt: {
class: 'bui-mt',
cssVar: '--mt',
values: VALID_SPACING_VALUES,
},
mx: {
class: 'bui-mx',
cssVar: '--mx',
values: VALID_SPACING_VALUES,
},
my: {
class: 'bui-my',
cssVar: '--my',
values: VALID_SPACING_VALUES,
},
p: {
class: 'bui-p',
cssVar: '--p',
values: VALID_SPACING_VALUES,
},
pb: {
class: 'bui-pb',
cssVar: '--pb',
values: VALID_SPACING_VALUES,
},
pl: {
class: 'bui-pl',
cssVar: '--pl',
values: VALID_SPACING_VALUES,
},
pr: {
class: 'bui-pr',
cssVar: '--pr',
values: VALID_SPACING_VALUES,
},
pt: {
class: 'bui-pt',
cssVar: '--pt',
values: VALID_SPACING_VALUES,
},
px: {
class: 'bui-px',
cssVar: '--px',
values: VALID_SPACING_VALUES,
},
py: {
class: 'bui-py',
cssVar: '--py',
values: VALID_SPACING_VALUES,
},
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: VALID_SPACING_VALUES,
},
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-justify',
values: ['start', 'center', 'end', 'between'],
},
direction: {
class: 'bui-fd',
values: ['row', 'column', 'row-reverse', 'column-reverse'],
},
};