Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-27 22:31:33 +00:00
parent 72c980094b
commit 685ec31f24
19 changed files with 97 additions and 432 deletions
@@ -141,9 +141,9 @@ Here's a view when buttons are full width.
py={4}
open
preview={<ButtonFullWidth />}
code={`<Stack style={{ width: '300px' }}>
code={`<Flex style={{ width: '300px' }}>
<Button fullWidth>Full width</Button>
</Stack>`}
</Flex>`}
/>
### Disabled
@@ -3,29 +3,29 @@ import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
stackFAQ1,
stackSimple,
stackResponsive,
stackAlign,
flexFAQ1,
flexSimple,
flexResponsive,
flexAlign,
} from '@/snippets/_snippets';
import { Snippet } from '@/components/Snippet';
import { StackPreview } from '@/snippets/stack';
import { FlexPreview } from '@/snippets/flex';
# Stack
# Flex
This is the stack container component. It will help to define the number of
This is the flex container component. It will help to define the number of
columns that will be used in the grid. You can also define the gap between the
columns. All values are responsive.
<Snippet
py={4}
align="center"
preview={<StackPreview />}
code={`<Stack>
preview={<FlexPreview />}
code={`<Flex>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Stack>`}
</Flex>`}
/>
<Tabs.Root>
@@ -34,9 +34,9 @@ columns. All values are responsive.
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Stack } from '@backstage/canon';
code={`import { Flex } from '@backstage/canon';
<Stack />
<Flex />
`}
/>
</Tabs.Panel>
@@ -73,29 +73,29 @@ The grid component also accepts all the spacing props from the Box component.
### Can I stack horizontally?
The Stack component only allows for stacking elements vertically. If you want
The Flex component only allows for stacking elements vertically. If you want
to create a column layout, please use the Grid component.
<CodeBlock code={stackFAQ1} />
<CodeBlock code={flexFAQ1} />
## Examples
### Simple
A simple example of how to use the Stack component.
A simple example of how to use the Flex component.
<CodeBlock code={stackSimple} />
<CodeBlock code={flexSimple} />
### Responsive
The Stack component also supports responsive values, making it easy to create
The Flex component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock code={stackResponsive} />
<CodeBlock code={flexResponsive} />
### Align
The Stack component also supports responsive alignment, making it easy to
The Flex component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock code={stackAlign} />
<CodeBlock code={flexAlign} />
@@ -75,13 +75,13 @@ appearance of the heading.
py={2}
open
preview={<HeadingAllVariants />}
code={`<Stack gap="md">
code={`<Flex direction="column" gap="4">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>`}
</Flex>`}
/>
### Responsive
@@ -1,118 +0,0 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { Snippet } from '@/components/Snippet';
import { InlinePreview } from '@/snippets/inline';
# Inline
The Inline component is used to create a horizontal layout of elements.
<Snippet
py={4}
preview={<InlinePreview />}
code={`<Inline>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Inline>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Inline } from '@backstage/canon';
<Inline />
`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
align: {
type: 'start | center | end',
responsive: true,
},
alignY: {
type: 'start | center | end',
responsive: true,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacePropsList} />
## Examples
### Simple
A simple example of how to use the Inline component.
<CodeBlock
code={`<Inline>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Responsive
The Inline component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock
code={`<Inline gap={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Align
The Inline component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock
code={`<Inline align={{ xs: 'left', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Align vertically
The Inline component also supports responsive vertical alignment, making it
easy to create responsive designs.
<CodeBlock
code={`<Inline alignY={{ xs: 'top', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
@@ -83,7 +83,7 @@ appearance of the text.
<Snippet
open
preview={<TextAllVariants />}
code={`<Stack gap="md">
code={`<Flex direction="column" gap="4">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
@@ -104,7 +104,7 @@ appearance of the text.
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
</Flex>`}
/>
### All weights
@@ -115,7 +115,7 @@ appearance of the text.
<Snippet
open
preview={<TextAllWeights />}
code={`<Stack gap="md">
code={`<Flex direction="column" gap="4">
<Text weight="regular" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
@@ -126,7 +126,7 @@ appearance of the text.
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
</Flex>`}
/>
### Responsive
+3 -3
View File
@@ -45,12 +45,12 @@ import { ThemeProvider } from '@backstage/canon';
Now you can start building your plugin using the new design system.
```tsx
import { Stack, Button } from '@backstage/canon';
import { Flex, Button, Text } from '@backstage/canon';
<Stack>
<Flex>
<Text>Hello World</Text>
<Button>Click me</Button>
</Stack>;
</Flex>;
```
## Roadmap
@@ -22,13 +22,13 @@ provide are the ones coming from the theme.
<CodeBlock
title="Layout components"
code={`<Stack direction="column" gap="md">
code={`<Flex direction="column" gap="4">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Stack>
</Flex>
`}
/>
@@ -21,13 +21,13 @@ component](?path=/docs/components-heading--docs).
py={2}
open
preview={<HeadingAllVariants />}
code={`<Stack gap="md">
code={`<Flex direction="column" gap="4">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>`}
</Flex>`}
/>
## Text
@@ -41,7 +41,7 @@ page.
<Snippet
open
preview={<TextAllVariants />}
code={`<Stack gap="md">
code={`<Flex direction="column" gap="4">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
@@ -62,5 +62,5 @@ page.
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
</Flex>`}
/>
@@ -1,7 +1,7 @@
'use client';
import { ReactNode } from 'react';
import { Grid, Stack, Text } from '../../../../../packages/canon';
import { Grid, Flex, Text } from '../../../../../packages/canon';
import { screenSizes } from '@/utils/data';
import { Frame } from '@/components/Frame';
import { usePlayground } from '@/utils/playground-context';
@@ -53,7 +53,7 @@ const Content = () => {
const { selectedComponents } = usePlayground();
return (
<Stack gap="xl">
<Flex direction="column" gap="4">
{selectedComponents.find(c => c === 'button') && (
<Line content={<ButtonPlayground />} title="Button" />
)}
@@ -69,17 +69,17 @@ const Content = () => {
{/* {selectedComponents.find(c => c === 'input') && (
<Line content={<InputPlayground />} title="Input" />
)} */}
</Stack>
</Flex>
);
};
const Line = ({ content, title }: { content: ReactNode; title: string }) => {
return (
<Grid gap={{ xs: 'xs', md: 'xl' }}>
<Grid.Item colSpan={2}>
<Grid gap={{ xs: '2', md: '4' }}>
<Grid.Item colSpan="2">
<Text>{title}</Text>
</Grid.Item>
<Grid.Item colSpan={10}>{content}</Grid.Item>
<Grid.Item colSpan="10">{content}</Grid.Item>
</Grid>
);
};
@@ -1,6 +1,6 @@
import React from 'react';
import { BoxSvg } from './svgs/box';
import { StackSvg } from './svgs/stack';
import { FlexSvg } from './svgs/flex';
import { GridSvg } from './svgs/grid';
import { InlineSvg } from './svgs/inline';
import { ContainerSvg } from './svgs/container';
@@ -20,10 +20,10 @@ export const LayoutComponents = () => {
</div>
</div>
<div className={styles.box}>
<Link className={styles.content} href="/components/stack">
<StackSvg />
<Link className={styles.content} href="/components/flex">
<FlexSvg />
</Link>
<div className={styles.title}>Stack</div>
<div className={styles.title}>Flex</div>
<div className={styles.description}>
Arrange your components vertically
</div>
@@ -15,7 +15,7 @@
*/
import React from 'react';
export const StackSvg = () => {
export const FlexSvg = () => {
return (
<svg
width="100"
+7 -7
View File
@@ -51,26 +51,26 @@ export const buttonVariants = `<Inline alignY="center">
</Inline>
`;
export const stackFAQ1 = `<Grid columns={3} gap="md">
export const flexFAQ1 = `<Grid columns={3} gap="4">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Grid>`;
export const stackSimple = `<Stack>
export const flexSimple = `<Flex>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
</Flex>`;
export const stackResponsive = `<Stack gap={{ xs: 'sm', md: 'md' }}>
export const flexResponsive = `<Flex gap={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
</Flex>`;
export const stackAlign = `<Stack align={{ xs: 'left', md: 'center' }}>
export const flexAlign = `<Flex align={{ xs: 'left', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
</Flex>`;
+15 -21
View File
@@ -1,16 +1,10 @@
'use client';
import {
Inline,
Button,
Stack,
ButtonProps,
Text,
} from '../../../packages/canon';
import { Button, Flex, ButtonProps, Text } from '../../../packages/canon';
export const ButtonPreview = () => {
return (
<Inline alignY="center">
<Flex align="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
@@ -20,40 +14,40 @@ export const ButtonPreview = () => {
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Inline>
</Flex>
);
};
export const ButtonSizes = () => {
return (
<Inline alignY="center">
<Flex align="center">
<Button size="medium">Medium</Button>
<Button size="small">Small</Button>
</Inline>
</Flex>
);
};
export const ButtonWithIcons = () => {
return (
<Inline alignY="center">
<Flex align="center">
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">
Button
</Button>
</Inline>
</Flex>
);
};
export const ButtonFullWidth = () => {
return (
<Stack style={{ width: '300px' }}>
<Flex direction="column" style={{ width: '300px' }}>
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">
Button
</Button>
</Stack>
</Flex>
);
};
@@ -73,12 +67,12 @@ export const ButtonPlayground = () => {
const variants: string[] = ['primary', 'secondary', 'tertiary'];
return (
<Stack>
<Flex direction="column">
{variants.map(variant => (
<Stack key={variant}>
<Flex direction="column" key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
<Inline alignY="center" key={size}>
<Flex align="center" key={size}>
<Button
iconStart="cloud"
variant={variant as ButtonProps['variant']}
@@ -102,10 +96,10 @@ export const ButtonPlayground = () => {
>
Button
</Button>
</Inline>
</Flex>
))}
</Stack>
</Flex>
))}
</Stack>
</Flex>
);
};
+7 -7
View File
@@ -1,6 +1,6 @@
'use client';
import { Inline, Checkbox, Stack, Text } from '../../../packages/canon';
import { Checkbox, Flex, Text } from '../../../packages/canon';
export const CheckboxPreview = () => {
return <Checkbox label="Accept terms and conditions" />;
@@ -8,25 +8,25 @@ export const CheckboxPreview = () => {
export const CheckboxAllVariants = () => {
return (
<Inline alignY="center">
<Flex align="center">
<Checkbox />
<Checkbox checked />
<Checkbox label="Checkbox" />
<Checkbox label="Checkbox" checked />
</Inline>
</Flex>
);
};
export const CheckboxPlayground = () => {
return (
<Stack>
<Flex direction="column">
<Text>All variants</Text>
<Inline alignY="center">
<Flex align="center">
<Checkbox />
<Checkbox checked />
<Checkbox label="Checkbox" />
<Checkbox label="Checkbox" checked />
</Inline>
</Stack>
</Flex>
</Flex>
);
};
@@ -1,6 +1,6 @@
'use client';
import { Stack } from '../../../packages/canon';
import { Flex } from '../../../packages/canon';
const DecorativeBox = () => {
return (
@@ -18,14 +18,14 @@ const DecorativeBox = () => {
);
};
export const StackPreview = () => {
export const FlexPreview = () => {
return (
<div style={{ width: '320px' }}>
<Stack>
<Flex>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Stack>
</Flex>
</div>
);
};
+7 -7
View File
@@ -1,6 +1,6 @@
'use client';
import { Heading, Stack, Text } from '../../../packages/canon';
import { Heading, Flex, Text } from '../../../packages/canon';
export const HeadingPreview = () => {
return <Heading>Look mum, no hands!</Heading>;
@@ -8,35 +8,35 @@ export const HeadingPreview = () => {
export const HeadingAllVariants = () => {
return (
<Stack>
<Flex direction="column">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>
</Flex>
);
};
export const HeadingResponsive = () => {
return (
<Stack>
<Flex direction="column">
<Heading variant={{ initial: 'title2', lg: 'title1' }}>
Responsive heading
</Heading>
</Stack>
</Flex>
);
};
export const HeadingPlayground = () => {
return (
<Stack>
<Flex direction="column">
<Text>All variants</Text>
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>
</Flex>
);
};
+7 -7
View File
@@ -1,6 +1,6 @@
'use client';
import { Stack, Text } from '../../../packages/canon';
import { Flex, Text } from '../../../packages/canon';
export const TextPreview = () => {
return (
@@ -14,7 +14,7 @@ export const TextPreview = () => {
export const TextAllVariants = () => {
return (
<Stack gap="md">
<Flex direction="column">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son. Who is
@@ -35,13 +35,13 @@ export const TextAllVariants = () => {
have none, but that man&apos;s father is my father&apos;s son. Who is
in the painting?
</Text>
</Stack>
</Flex>
);
};
export const TextAllWeights = () => {
return (
<Stack gap="md">
<Flex direction="column">
<Text weight="regular" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son. Who is
@@ -52,7 +52,7 @@ export const TextAllWeights = () => {
have none, but that man&apos;s father is my father&apos;s son. Who is
in the painting?
</Text>
</Stack>
</Flex>
);
};
@@ -64,7 +64,7 @@ export const TextResponsive = () => {
export const TextPlayground = () => {
return (
<Stack>
<Flex direction="column">
<Text>Subtitle</Text>
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, Brothers and sisters I
@@ -89,6 +89,6 @@ export const TextPlayground = () => {
have none, but that man&apos;s father is my father&apos;s son. Who is
in the painting?
</Text>
</Stack>
</Flex>
);
};
+2 -7
View File
@@ -55,13 +55,8 @@ export const layoutComponents: Page[] = [
status: 'alpha',
},
{
title: 'Inline',
slug: 'inline',
status: 'alpha',
},
{
title: 'Stack',
slug: 'stack',
title: 'Flex',
slug: 'flex',
status: 'alpha',
},
];
-206
View File
@@ -1,206 +0,0 @@
/*
* Copyright 2024 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 { Breakpoint, UtilityProps } from '../types';
const spaceMap = (type: string) => ({
none: `${type}-none`,
'2xs': `${type}-2xs`,
xs: `${type}-xs`,
sm: `${type}-sm`,
md: `${type}-md`,
lg: `${type}-lg`,
xl: `${type}-xl`,
'2xl': `${type}-2xl`,
});
const valueMap: Record<string, Record<string, string>> = {
alignItems: {
stretch: 'items-stretch',
start: 'items-start',
center: 'items-center',
end: 'items-end',
},
border: {
none: 'border-none',
base: 'border-base',
error: 'border-error',
warning: 'border-warning',
selected: 'border-selected',
},
borderRadius: {
none: 'rounded-none',
'2xs': 'rounded-2xs',
xs: 'rounded-xs',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
xl: 'rounded-xl',
'2xl': 'rounded-2xl',
},
colEnd: {
1: 'col-end-1',
2: 'col-end-2',
3: 'col-end-3',
4: 'col-end-4',
5: 'col-end-5',
6: 'col-end-6',
7: 'col-end-7',
8: 'col-end-8',
9: 'col-end-9',
10: 'col-end-10',
11: 'col-end-11',
12: 'col-end-12',
auto: 'col-end-auto',
},
colSpan: {
1: 'col-span-1',
2: 'col-span-2',
3: 'col-span-3',
4: 'col-span-4',
5: 'col-span-5',
6: 'col-span-6',
7: 'col-span-7',
8: 'col-span-8',
9: 'col-span-9',
10: 'col-span-10',
11: 'col-span-11',
12: 'col-span-12',
auto: 'col-span-auto',
},
colStart: {
1: 'col-start-1',
2: 'col-start-2',
3: 'col-start-3',
4: 'col-start-4',
5: 'col-start-5',
6: 'col-start-6',
7: 'col-start-7',
8: 'col-start-8',
9: 'col-start-9',
10: 'col-start-10',
11: 'col-start-11',
12: 'col-start-12',
auto: 'col-start-auto',
},
columns: {
1: 'grid-cols-1',
2: 'grid-cols-2',
3: 'grid-cols-3',
4: 'grid-cols-4',
5: 'grid-cols-5',
6: 'grid-cols-6',
7: 'grid-cols-7',
8: 'grid-cols-8',
9: 'grid-cols-9',
10: 'grid-cols-10',
11: 'grid-cols-11',
12: 'grid-cols-12',
auto: 'grid-cols-auto',
},
display: {
none: 'hidden',
flex: 'flex',
block: 'block',
inline: 'inline',
},
flexDirection: {
row: 'flex-row',
column: 'flex-col',
},
flexWrap: {
wrap: 'flex-wrap',
nowrap: 'flex-nowrap',
'wrap-reverse': 'flex-wrap-reverse',
},
gap: spaceMap('gap'),
justifyContent: {
stretch: 'justify-stretch',
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
around: 'justify-around',
between: 'justify-between',
},
margin: spaceMap('m'),
marginBottom: spaceMap('mb'),
marginLeft: spaceMap('ml'),
marginRight: spaceMap('mr'),
marginTop: spaceMap('mt'),
marginX: spaceMap('mx'),
marginY: spaceMap('my'),
padding: spaceMap('p'),
paddingBottom: spaceMap('pb'),
paddingLeft: spaceMap('pl'),
paddingRight: spaceMap('pr'),
paddingTop: spaceMap('pt'),
paddingX: spaceMap('px'),
paddingY: spaceMap('py'),
rowSpan: {
1: 'row-span-1',
2: 'row-span-2',
3: 'row-span-3',
4: 'row-span-4',
5: 'row-span-5',
6: 'row-span-6',
7: 'row-span-7',
8: 'row-span-8',
9: 'row-span-9',
10: 'row-span-10',
11: 'row-span-11',
12: 'row-span-12',
full: 'row-span-full',
},
};
const generateClassNames = (propName: string, propValue: any) => {
const classNames: string[] = [];
// If the property name is not in the valueMap, return an empty array
if (!valueMap.hasOwnProperty(propName)) {
return classNames;
}
if (typeof propValue === 'string' || typeof propValue === 'number') {
// If the property value is a string, map it to the valueMap
const value = valueMap[propName]?.[propValue] || propValue;
classNames.push(`cu-${value}`);
} else if (typeof propValue === 'object') {
// If the property value is an object, map each key-value pair
Object.entries(propValue as Record<Breakpoint, string>).forEach(
([breakpoint, value]) => {
const mappedValue = valueMap[propName]?.[value] || value;
if (breakpoint === 'xs') {
classNames.push(`cu-${mappedValue}`);
} else {
classNames.push(`cu-${breakpoint}-${mappedValue}`);
}
},
);
}
return classNames;
};
export const getClassNames = (props: UtilityProps) => {
const classNames: string[] = [];
Object.entries(props).forEach(([propName, propValue]) => {
classNames.push(...generateClassNames(propName, propValue));
});
return classNames.filter(Boolean).join(' ');
};