Restructure useStyles hook

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-10-11 09:04:26 +01:00
parent fbb4abef52
commit 749ec3cd38
19 changed files with 617 additions and 409 deletions
+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"
/>
+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, ...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),
style,
...rest,
},
children,
});
);
});
Box.displayName = 'Box';
+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;
@@ -41,6 +41,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
className: 'caca',
},
};
+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
+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 {
+240 -11
View File
@@ -17,6 +17,139 @@ 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'],
},
};
/**
* Resolve a responsive value based on the current breakpoint
* @param value - The responsive value (string or object with breakpoint keys)
@@ -55,8 +188,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 +197,125 @@ 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;
}
}
}
// 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.includes(val as any)) {
// Generate utility class with value suffix and optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}-${val}`
: `${utilityConfig.class}-${val}`;
utilityClassList.push(className);
} else {
// Custom value - add CSS custom property AND utility class name
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;
// Add utility class name (without value suffix) with optional breakpoint prefix
const className = prefix
? `${prefix}${utilityConfig.class}`
: utilityConfig.class;
utilityClassList.push(className);
}
};
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,
};
}
+1
View File
@@ -160,6 +160,7 @@ export type DataAttributesMap = Record<string, DataAttributeValues>;
export interface ComponentDefinition {
classNames: ClassNamesMap;
dataAttributes?: DataAttributesMap;
utilityProps?: string[];
}
/**
@@ -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: {