diff --git a/docs-ui/src/content/components/box.mdx b/docs-ui/src/content/components/box.mdx index 84fca48349..d1a9dd43fe 100644 --- a/docs-ui/src/content/components/box.mdx +++ b/docs-ui/src/content/components/box.mdx @@ -22,7 +22,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent'; } + preview={} code={boxPreviewSnippet} align="center" /> diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index f0086c3d25..6637f45740 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -36,35 +36,277 @@ const meta = { export default meta; type Story = StoryObj; -const Card = () => { - return ( -
- ); -}; +const diagonalStripePattern = (() => { + const svg = ` + + + + + + `.trim(); + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +})(); export const Default: Story = { args: { - children: 'Hello World', - mb: '4', + width: '64px', + height: '64px', + style: { + background: '#eaf2fd', + borderRadius: '4px', + border: '1px solid #2563eb', + backgroundImage: `url("${diagonalStripePattern}")`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 'bold', + color: '#2563eb', + }, }, }; -export const Preview: Story = { - args: { - children: , - display: 'inline', - }, +export const Margin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const ResponsiveMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomResponsiveMargin: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const Padding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const ResponsivePadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomPadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), +}; + +export const CustomResponsivePadding: Story = { + args: { ...Default.args }, + render: args => ( + + + + + + + + + + + + + + + + + + + + + ), }; const CardDisplay = ({ children }: { children?: ReactNode }) => { @@ -105,151 +347,3 @@ export const Display: Story = { ), }; - -const styleInsideBox = { - background: 'rgb(196, 202, 251)', - color: 'white', - borderRadius: '4px', -}; - -export const Padding: Story = { - args: { - style: { - background: '#1f47ff', - color: 'white', - borderRadius: '4px', - padding: '12px 12px', - }, - }, - render: args => ( - - - - Padding - - - Padding X - - - Padding Y - - - - - Padding Top - - - Padding Right - - - Padding Bottom - - - Padding Left - - - - - Custom Padding - - - Custom Padding X - - - Custom Padding Y - - - - - Custom Padding Top Bottom - - - Custom Padding Left Right - - - - ), -}; - -export const Margin: Story = { - args: { - style: { - background: '#1f47ff', - color: 'white', - borderRadius: '4px', - padding: '12px 12px', - }, - }, - render: args => ( - - - - - Margin - - - - - Margin X - - - - - Margin Y - - - - - - - Margin Top - - - - - Margin Right - - - - - Margin Bottom - - - - - Margin Left - - - - - - - Custom Margin - - - - - Custom Margin X - - - - - Custom Margin Y - - - - - - - Custom Margin Top Bottom - - - - - Custom Margin Left Right - - - - - ), -}; diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index 06d7d1d434..729a0f7cec 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -17,38 +17,27 @@ import { createElement, forwardRef } from 'react'; import { BoxProps } from './types'; import clsx from 'clsx'; -import { extractProps } from '../../utils/extractProps'; -import { spacingPropDefs } from '../../props/spacing.props'; -import { boxPropDefs } from './Box.props'; -import { widthPropDefs } from '../../props/width.props'; -import { heightPropDefs } from '../../props/height.props'; -import { positionPropDefs } from '../../props/position.props'; -import { displayPropDefs } from '../../props/display.props'; import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Box = forwardRef((props, ref) => { - const { children } = props; + const { classNames, utilityClasses, style, cleanedProps } = useStyles( + 'Box', + props, + ); - const propDefs = { - ...spacingPropDefs, - ...widthPropDefs, - ...heightPropDefs, - ...positionPropDefs, - ...displayPropDefs, - ...boxPropDefs, - }; + const { as = 'div', children, ...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'; diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index c70012dd56..c0f4a71035 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -14,24 +14,21 @@ * limitations under the License. */ -import type { HeightProps } from '../../props/height.props'; -import type { WidthProps } from '../../props/width.props'; -import type { PositionProps } from '../../props/position.props'; -import type { DisplayProps } from '../../props/display.props'; -import type { SpaceProps } from '../../types'; -import type { BoxOwnProps } from './Box.props'; +import type { SpaceProps, Responsive } from '../../types'; /** @public */ export interface BoxProps extends SpaceProps { - display?: DisplayProps['display']; - as?: BoxOwnProps['as']; - width?: WidthProps['width']; - minWidth?: WidthProps['minWidth']; - maxWidth?: WidthProps['maxWidth']; - height?: HeightProps['height']; - minHeight?: HeightProps['minHeight']; - maxHeight?: HeightProps['maxHeight']; - position?: PositionProps['position']; + display?: Responsive<'none' | 'flex' | 'block' | 'inline'>; + as?: keyof JSX.IntrinsicElements; + width?: Responsive; + minWidth?: Responsive; + maxWidth?: Responsive; + height?: Responsive; + minHeight?: Responsive; + maxHeight?: Responsive; + position?: Responsive< + 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' + >; children?: React.ReactNode; className?: string; style?: React.CSSProperties; diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index db8e999f98..9669b65d96 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -41,6 +41,7 @@ type Story = StoryObj; export const Default: Story = { args: { children: 'Button', + className: 'caca', }, }; diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 40879e51ed..610f29b9b0 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -23,21 +23,14 @@ import { useStyles } from '../../hooks/useStyles'; /** @public */ export const Button = forwardRef( (props: ButtonProps, ref: Ref) => { - const { - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - ...rest - } = props; - - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); + const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + return ( } />, }; - -const variants = ['primary', 'secondary'] as const; -const sizes = ['small', 'medium'] as const; - -export const Playground: Story = { - render: args => ( - - {variants.map(variant => ( - - {variant} - {sizes.map(size => ( - - } - /> - } - aria-label="Chevron right icon button" - variant={variant} - size={size} - /> - } - aria-label="Chevron right icon button" - variant={variant} - size={size} - /> - - ))} - - ))} - - ), -}; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx index 072ed20963..19ed2de3f3 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx @@ -23,22 +23,16 @@ import { useStyles } from '../../hooks/useStyles'; /** @public */ export const ButtonIcon = forwardRef( (props: ButtonIconProps, ref: Ref) => { - const { - size = 'small', - variant = 'primary', - icon, - className, - style, - ...rest - } = props; - - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); const { classNames: classNamesButtonIcon } = useStyles('ButtonIcon'); + const { className, icon, ...rest } = cleanedProps; + return ( ( - - {variants.map(variant => ( - - {variant} - {sizes.map(size => ( - - - Button - - } - variant={variant} - size={size} - > - Button - - } - variant={variant} - size={size} - > - Button - - } - iconEnd={} - style={{ width: '200px' }} - variant={variant} - size={size} - > - Button - - - Button - - } - variant={variant} - size={size} - isDisabled - > - Button - - } - variant={variant} - size={size} - isDisabled - > - Button - - - ))} - - ))} - - ), -}; diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index b5b5ab208d..5c621bc057 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -26,24 +26,18 @@ import { isExternalLink } from '../../utils/isExternalLink'; export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { const navigate = useNavigate(); - const { - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - href, - ...rest - } = props; - const { classNames, dataAttributes } = useStyles('Button', { - size, - variant, + const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { + size: 'small', + variant: 'primary', + ...props, }); const { classNames: classNamesButtonLink } = useStyles('ButtonLink'); + const { children, className, iconStart, iconEnd, href, ...rest } = + cleanedProps; + const isExternal = isExternalLink(href); // If it's an external link, render RALink without RouterProvider diff --git a/packages/ui/src/css/utilities/mb.css b/packages/ui/src/css/utilities/mb.css index 6394937af4..d2c3df3779 100644 --- a/packages/ui/src/css/utilities/mb.css +++ b/packages/ui/src/css/utilities/mb.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mb { - margin-bottom: var(--pb-xs); + margin-bottom: var(--mb-xs); } .xs\:bui-mb-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mb { - margin-bottom: var(--pb-sm); + margin-bottom: var(--mb-sm); } .sm\:bui-mb-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mb { - margin-bottom: var(--pb-md); + margin-bottom: var(--mb-md); } .md\:bui-mb-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mb { - margin-bottom: var(--pb-lg); + margin-bottom: var(--mb-lg); } .lg\:bui-mb-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mb { - margin-bottom: var(--pb-xl); + margin-bottom: var(--mb-xl); } .xl\:bui-mb-0\.5 { diff --git a/packages/ui/src/css/utilities/ml.css b/packages/ui/src/css/utilities/ml.css index 7b82b73ab8..bfa17b1700 100644 --- a/packages/ui/src/css/utilities/ml.css +++ b/packages/ui/src/css/utilities/ml.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-ml { - margin-left: var(--pl-xs); + margin-left: var(--ml-xs); } .xs\:bui-ml-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-ml { - margin-left: var(--pl-sm); + margin-left: var(--ml-sm); } .sm\:bui-ml-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-ml { - margin-left: var(--pl-md); + margin-left: var(--ml-md); } .md\:bui-ml-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-ml { - margin-left: var(--pl-lg); + margin-left: var(--ml-lg); } .lg\:bui-ml-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-ml { - margin-left: var(--pl-xl); + margin-left: var(--ml-xl); } .xl\:bui-ml-0\.5 { diff --git a/packages/ui/src/css/utilities/mr.css b/packages/ui/src/css/utilities/mr.css index bac16b3ed3..96704994d8 100644 --- a/packages/ui/src/css/utilities/mr.css +++ b/packages/ui/src/css/utilities/mr.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mr { - margin-right: var(--pr-xs); + margin-right: var(--mr-xs); } .xs\:bui-mr-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mr { - margin-right: var(--pr-sm); + margin-right: var(--mr-sm); } .sm\:bui-mr-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mr { - margin-right: var(--pr-md); + margin-right: var(--mr-md); } .md\:bui-mr-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mr { - margin-right: var(--pr-lg); + margin-right: var(--mr-lg); } .lg\:bui-mr-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mr { - margin-right: var(--pr-xl); + margin-right: var(--mr-xl); } .xl\:bui-mr-0\.5 { diff --git a/packages/ui/src/css/utilities/mt.css b/packages/ui/src/css/utilities/mt.css index 2f6ecc4aa8..d72cac5f93 100644 --- a/packages/ui/src/css/utilities/mt.css +++ b/packages/ui/src/css/utilities/mt.css @@ -86,7 +86,7 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mt { - margin-top: var(--pt-xs); + margin-top: var(--mt-xs); } .xs\:bui-mt-0\.5 { @@ -157,7 +157,7 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mt { - margin-top: var(--pt-sm); + margin-top: var(--mt-sm); } .sm\:bui-mt-0\.5 { @@ -228,7 +228,7 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mt { - margin-top: var(--pt-md); + margin-top: var(--mt-md); } .md\:bui-mt-0\.5 { @@ -299,7 +299,7 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mt { - margin-top: var(--pt-lg); + margin-top: var(--mt-lg); } .lg\:bui-mt-0\.5 { @@ -370,7 +370,7 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mt { - margin-top: var(--pt-xl); + margin-top: var(--mt-xl); } .xl\:bui-mt-0\.5 { diff --git a/packages/ui/src/css/utilities/mx.css b/packages/ui/src/css/utilities/mx.css index 4898fc2352..f5c06b8b4c 100644 --- a/packages/ui/src/css/utilities/mx.css +++ b/packages/ui/src/css/utilities/mx.css @@ -103,8 +103,8 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-mx { - margin-left: var(--px-xs); - margin-right: var(--px-xs); + margin-left: var(--mx-xs); + margin-right: var(--mx-xs); } .xs\:bui-mx-0\.5 { @@ -191,8 +191,8 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-mx { - margin-left: var(--px-sm); - margin-right: var(--px-sm); + margin-left: var(--mx-sm); + margin-right: var(--mx-sm); } .sm\:bui-mx-0\.5 { @@ -279,8 +279,8 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-mx { - margin-left: var(--px-md); - margin-right: var(--px-md); + margin-left: var(--mx-md); + margin-right: var(--mx-md); } .md\:bui-mx-0\.5 { @@ -367,8 +367,8 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-mx { - margin-left: var(--px-lg); - margin-right: var(--px-lg); + margin-left: var(--mx-lg); + margin-right: var(--mx-lg); } .lg\:bui-mx-0\.5 { @@ -455,8 +455,8 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-mx { - margin-left: var(--px-xl); - margin-right: var(--px-xl); + margin-left: var(--mx-xl); + margin-right: var(--mx-xl); } .xl\:bui-mx-0\.5 { diff --git a/packages/ui/src/css/utilities/my.css b/packages/ui/src/css/utilities/my.css index a165ad478e..f00167f88e 100644 --- a/packages/ui/src/css/utilities/my.css +++ b/packages/ui/src/css/utilities/my.css @@ -103,8 +103,8 @@ /* Breakpoint xs */ @media (min-width: 640px) { .xs\:bui-my { - margin-top: var(--py-xs); - margin-bottom: var(--py-xs); + margin-top: var(--my-xs); + margin-bottom: var(--my-xs); } .xs\:bui-my-0\.5 { @@ -191,8 +191,8 @@ /* Breakpoint sm */ @media (min-width: 768px) { .sm\:bui-my { - margin-top: var(--py-sm); - margin-bottom: var(--py-sm); + margin-top: var(--my-sm); + margin-bottom: var(--my-sm); } .sm\:bui-my-0\.5 { @@ -279,8 +279,8 @@ /* Breakpoint md */ @media (min-width: 1024px) { .md\:bui-my { - margin-top: var(--py-md); - margin-bottom: var(--py-md); + margin-top: var(--my-md); + margin-bottom: var(--my-md); } .md\:bui-my-0\.5 { @@ -367,8 +367,8 @@ /* Breakpoint lg */ @media (min-width: 1280px) { .lg\:bui-my { - margin-top: var(--py-lg); - margin-bottom: var(--py-lg); + margin-top: var(--my-lg); + margin-bottom: var(--my-lg); } .lg\:bui-my-0\.5 { @@ -455,8 +455,8 @@ /* Breakpoint xl */ @media (min-width: 1536px) { .xl\:bui-my { - margin-top: var(--py-xl); - margin-bottom: var(--py-xl); + margin-top: var(--my-xl); + margin-bottom: var(--my-xl); } .xl\:bui-my-0\.5 { diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts index 19f9823236..8e9259a1a3 100644 --- a/packages/ui/src/hooks/useStyles.ts +++ b/packages/ui/src/hooks/useStyles.ts @@ -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( componentName: T, @@ -64,29 +197,125 @@ export function useStyles( ): { classNames: ComponentClassNames; dataAttributes: Record; - resolvedProps: Record; + utilityClasses: string; + style: React.CSSProperties; + cleanedProps: Record; } { const { breakpoint } = useBreakpoint(); - const classNames = componentDefinitions[componentName] - .classNames as ComponentClassNames; + const componentDefinition = componentDefinitions[componentName]; + const classNames = componentDefinition.classNames as ComponentClassNames; + const utilityPropNames = + ('utilityProps' in componentDefinition + ? componentDefinition.utilityProps + : []) || []; - // Resolve responsive values and generate data attributes + // Extract data attribute names from component definition + const dataAttributeNames = + 'dataAttributes' in componentDefinition + ? Object.keys(componentDefinition.dataAttributes || {}) + : []; + + // Extract existing style from props + const incomingStyle = props.style || {}; + + // Generate data attributes from component definition const dataAttributes: Record = {}; - const resolvedProps: Record = {}; - - for (const [key, value] of Object.entries(props)) { + for (const key of dataAttributeNames) { + const value = props[key]; if (value !== undefined && value !== null) { const resolvedValue = resolveResponsiveValue(value, breakpoint); if (resolvedValue !== undefined) { - resolvedProps[key] = resolvedValue; dataAttributes[`data-${key}`] = resolvedValue; } } } + // 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); + + // Merge incoming style with generated styles (incoming styles take precedence) + const mergedStyle = { + ...generatedStyle, + ...incomingStyle, + }; + return { classNames, dataAttributes, - resolvedProps, // Also return resolved props for convenience + utilityClasses: utilityClassList.join(' '), + style: mergedStyle, + cleanedProps, }; } diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index d0011bfe8a..cde389113d 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -160,6 +160,7 @@ export type DataAttributesMap = Record; export interface ComponentDefinition { classNames: ClassNamesMap; dataAttributes?: DataAttributesMap; + utilityProps?: string[]; } /** diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index fe040d09b1..2516f85a84 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -35,6 +35,30 @@ export const componentDefinitions = { classNames: { root: 'bui-Box', }, + utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'position', + 'display', + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + ], }, Button: { classNames: {