From cfc84d7e70d483bb3ad548e22f26447fe313a25d Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Thu, 30 Jul 2026 17:02:52 +0300 Subject: [PATCH 1/5] fix(flex): ensure utility styles override component styles --- .../src/components/FlexBox/FlexBox.test.tsx | 54 +++++++- .../src/components/FlexBox/FlexBox.tsx | 4 +- .../src/components/layout/flex/classNames.ts | 38 ++++++ .../layout/flex/{flex.module.css => flex.css} | 100 +++++++-------- .../components/layout/flex/flex.stories.tsx | 2 +- .../src/components/layout/flex/flex.test.ts | 43 +++++++ .../src/components/layout/flex/flex.ts | 115 +----------------- .../src/components/layout/flex/index.ts | 1 + .../src/components/layout/flex/types.ts | 75 ++++++++++++ .../public_api_guard/components/layout.api.md | 2 +- 10 files changed, 268 insertions(+), 166 deletions(-) create mode 100644 packages/components/src/components/layout/flex/classNames.ts rename packages/components/src/components/layout/flex/{flex.module.css => flex.css} (63%) create mode 100644 packages/components/src/components/layout/flex/flex.test.ts create mode 100644 packages/components/src/components/layout/flex/types.ts diff --git a/packages/components/src/components/FlexBox/FlexBox.test.tsx b/packages/components/src/components/FlexBox/FlexBox.test.tsx index f4332914f..a32aec84c 100644 --- a/packages/components/src/components/FlexBox/FlexBox.test.tsx +++ b/packages/components/src/components/FlexBox/FlexBox.test.tsx @@ -1,8 +1,10 @@ import { createRef } from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; +import { BreakpointsContext, type BreakpointsContextType } from '../Provider'; + import { FlexBox } from './index'; import type { FlexBoxProps } from './index'; @@ -27,4 +29,54 @@ describe('FlexBox', () => { const flexBox = container.querySelector('div'); expect(ref.current).toBe(flexBox); }); + + it('should apply flex classes from props', () => { + render( + + ); + + expect(screen.getByTestId('flex-box')).toHaveClass( + 'kbq-flex', + 'kbq-flex-flex_inline-flex', + 'kbq-flex-wrap_wrap-reverse', + 'kbq-flex-gap_row_l', + 'kbq-flex-gap_column_xl', + 'kbq-flex-direction_column-reverse', + 'kbq-flex-alignItems_center', + 'kbq-flex-justifyContent_space-between' + ); + }); + + it('should apply flex classes from responsive props', () => { + const breakpoints = { + xs: true, + l: true, + } as BreakpointsContextType; + + render( + + + + ); + + expect(screen.getByTestId('flex-box')).toHaveClass( + 'kbq-flex-gap_row_m', + 'kbq-flex-gap_column_m', + 'kbq-flex-direction_row' + ); + }); }); diff --git a/packages/components/src/components/FlexBox/FlexBox.tsx b/packages/components/src/components/FlexBox/FlexBox.tsx index 6e21a3e01..7c023feb8 100644 --- a/packages/components/src/components/FlexBox/FlexBox.tsx +++ b/packages/components/src/components/FlexBox/FlexBox.tsx @@ -5,7 +5,7 @@ import type { ComponentPropsWithRef, ElementType } from 'react'; import { clsx, polymorphicForwardRef } from '@koobiq/react-core'; import { getResponsiveValue } from '../../utils'; -import { flex as flexBox } from '../layout'; +import { getFlexClassNames } from '../layout/flex/classNames'; import { useMatchedBreakpoints } from '../Provider'; import type { FlexBoxBaseProps } from './index'; @@ -45,7 +45,7 @@ export const FlexBox = polymorphicForwardRef<'div', FlexBoxBaseProps>( const direction = getResponsiveValue(directionProp, breakpoints); const justifyContent = getResponsiveValue(justifyContentProp, breakpoints); - const flexCn = flexBox({ + const flexCn = getFlexClassNames({ gap, flex, wrap, diff --git a/packages/components/src/components/layout/flex/classNames.ts b/packages/components/src/components/layout/flex/classNames.ts new file mode 100644 index 000000000..73d2b5e80 --- /dev/null +++ b/packages/components/src/components/layout/flex/classNames.ts @@ -0,0 +1,38 @@ +import { clsx } from '@koobiq/react-core'; + +import type { FlexProps } from './types'; + +const baseClassName = 'kbq-flex'; + +export const getFlexClassNames = ( + props: FlexProps, + className?: string +): string => { + const { + alignItems, + justifyContent, + flex, + wrap, + direction, + gap, + rowGap: rowGapProp, + colGap: colGapProp, + order, + } = props; + + const colGap = colGapProp ?? gap; + const rowGap = rowGapProp ?? gap; + + return clsx( + baseClassName, + flex && `${baseClassName}-flex_${flex}`, + wrap && `${baseClassName}-wrap_${wrap}`, + order !== undefined && `${baseClassName}-order_${order}`, + rowGap && `${baseClassName}-gap_row_${rowGap}`, + colGap && `${baseClassName}-gap_column_${colGap}`, + direction && `${baseClassName}-direction_${direction}`, + alignItems && `${baseClassName}-alignItems_${alignItems}`, + justifyContent && `${baseClassName}-justifyContent_${justifyContent}`, + className + ); +}; diff --git a/packages/components/src/components/layout/flex/flex.module.css b/packages/components/src/components/layout/flex/flex.css similarity index 63% rename from packages/components/src/components/layout/flex/flex.module.css rename to packages/components/src/components/layout/flex/flex.css index 26e65f6b9..0acbfac93 100644 --- a/packages/components/src/components/layout/flex/flex.module.css +++ b/packages/components/src/components/layout/flex/flex.css @@ -1,4 +1,4 @@ -.base { +.kbq-flex { --flex-gap: 0; --flex-order: 0; --flex-type: flex; @@ -15,200 +15,200 @@ flex-flow: var(--flex-direction) var(--flex-wrap); } -.alignItems_flex-start { +.kbq-flex-alignItems_flex-start { --flex-align-items: flex-start; } -.alignItems_flex-end { +.kbq-flex-alignItems_flex-end { --flex-align-items: flex-end; } -.alignItems_center { +.kbq-flex-alignItems_center { --flex-align-items: center; } -.alignItems_baseline { +.kbq-flex-alignItems_baseline { --flex-align-items: baseline; } -.alignItems_stretch { +.kbq-flex-alignItems_stretch { --flex-align-items: stretch; } -.justifyContent_flex-start { +.kbq-flex-justifyContent_flex-start { --flex-justify-content: flex-start; } -.justifyContent_flex-end { +.kbq-flex-justifyContent_flex-end { --flex-justify-content: flex-end; } -.justifyContent_center { +.kbq-flex-justifyContent_center { --flex-justify-content: center; } -.justifyContent_space-between { +.kbq-flex-justifyContent_space-between { --flex-justify-content: space-between; } -.justifyContent_space-around { +.kbq-flex-justifyContent_space-around { --flex-justify-content: space-around; } -.justifyContent_space-evenly { +.kbq-flex-justifyContent_space-evenly { --flex-justify-content: space-evenly; } -.flex_flex { +.kbq-flex-flex_flex { --flex-type: flex; } -.flex_inline-flex { +.kbq-flex-flex_inline-flex { --flex-type: inline-flex; } -.wrap_wrap { +.kbq-flex-wrap_wrap { --flex-wrap: wrap; } -.wrap_nowrap { +.kbq-flex-wrap_nowrap { --flex-wrap: nowrap; } -.wrap_wrap-reverse { +.kbq-flex-wrap_wrap-reverse { --flex-wrap: wrap-reverse; } -.direction_row { +.kbq-flex-direction_row { --flex-direction: row; } -.direction_row-reverse { +.kbq-flex-direction_row-reverse { --flex-direction: row-reverse; } -.direction_column { +.kbq-flex-direction_column { --flex-direction: column; } -.direction_column-reverse { +.kbq-flex-direction_column-reverse { --flex-direction: column-reverse; } -.order_-1 { +.kbq-flex-order_-1 { --flex-order: -1; } -.order_0 { +.kbq-flex-order_0 { --flex-order: 0; } -.order_1 { +.kbq-flex-order_1 { --flex-order: 1; } /* column-gap */ -.gap_column_3xs { +.kbq-flex-gap_column_3xs { --flex-column-gap: var(--kbq-size-3xs); } -.gap_column_xxs { +.kbq-flex-gap_column_xxs { --flex-column-gap: var(--kbq-size-xxs); } -.gap_column_xs { +.kbq-flex-gap_column_xs { --flex-column-gap: var(--kbq-size-xs); } -.gap_column_s { +.kbq-flex-gap_column_s { --flex-column-gap: var(--kbq-size-s); } -.gap_column_m { +.kbq-flex-gap_column_m { --flex-column-gap: var(--kbq-size-m); } -.gap_column_l { +.kbq-flex-gap_column_l { --flex-column-gap: var(--kbq-size-l); } -.gap_column_xl { +.kbq-flex-gap_column_xl { --flex-column-gap: var(--kbq-size-xl); } -.gap_column_xxl { +.kbq-flex-gap_column_xxl { --flex-column-gap: var(--kbq-size-xxl); } -.gap_column_3xl { +.kbq-flex-gap_column_3xl { --flex-column-gap: var(--kbq-size-3xl); } -.gap_column_4xl { +.kbq-flex-gap_column_4xl { --flex-column-gap: var(--kbq-size-4xl); } -.gap_column_5xl { +.kbq-flex-gap_column_5xl { --flex-column-gap: var(--kbq-size-5xl); } -.gap_column_6xl { +.kbq-flex-gap_column_6xl { --flex-column-gap: var(--kbq-size-6xl); } -.gap_column_7xl { +.kbq-flex-gap_column_7xl { --flex-column-gap: var(--kbq-size-7xl); } /* row-gap */ -.gap_row_3xs { +.kbq-flex-gap_row_3xs { --flex-row-gap: var(--kbq-size-3xs); } -.gap_row_xxs { +.kbq-flex-gap_row_xxs { --flex-row-gap: var(--kbq-size-xxs); } -.gap_row_xs { +.kbq-flex-gap_row_xs { --flex-row-gap: var(--kbq-size-xs); } -.gap_row_s { +.kbq-flex-gap_row_s { --flex-row-gap: var(--kbq-size-s); } -.gap_row_m { +.kbq-flex-gap_row_m { --flex-row-gap: var(--kbq-size-m); } -.gap_row_l { +.kbq-flex-gap_row_l { --flex-row-gap: var(--kbq-size-l); } -.gap_row_xl { +.kbq-flex-gap_row_xl { --flex-row-gap: var(--kbq-size-xl); } -.gap_row_xxl { +.kbq-flex-gap_row_xxl { --flex-row-gap: var(--kbq-size-xxl); } -.gap_row_3xl { +.kbq-flex-gap_row_3xl { --flex-row-gap: var(--kbq-size-3xl); } -.gap_row_4xl { +.kbq-flex-gap_row_4xl { --flex-row-gap: var(--kbq-size-4xl); } -.gap_row_5xl { +.kbq-flex-gap_row_5xl { --flex-row-gap: var(--kbq-size-5xl); } -.gap_row_6xl { +.kbq-flex-gap_row_6xl { --flex-row-gap: var(--kbq-size-6xl); } -.gap_row_7xl { +.kbq-flex-gap_row_7xl { --flex-row-gap: var(--kbq-size-7xl); } diff --git a/packages/components/src/components/layout/flex/flex.stories.tsx b/packages/components/src/components/layout/flex/flex.stories.tsx index 3b50be176..3153b4f4c 100644 --- a/packages/components/src/components/layout/flex/flex.stories.tsx +++ b/packages/components/src/components/layout/flex/flex.stories.tsx @@ -12,7 +12,7 @@ import { flexPropOrder, type FlexProps, flexPropWrap, -} from './flex'; +} from './index'; const meta = { title: 'Mixins/flex', diff --git a/packages/components/src/components/layout/flex/flex.test.ts b/packages/components/src/components/layout/flex/flex.test.ts new file mode 100644 index 000000000..04e2f01ae --- /dev/null +++ b/packages/components/src/components/layout/flex/flex.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { flex } from './flex'; + +describe('flex', () => { + it('should generate classes for every flex property', () => { + expect( + flex( + { + flex: 'inline-flex', + wrap: 'wrap-reverse', + order: -1, + gap: 'm', + rowGap: 'l', + colGap: 'xl', + direction: 'column-reverse', + alignItems: 'center', + justifyContent: 'space-between', + }, + 'custom-class' + ) + ).toBe( + [ + 'kbq-flex', + 'kbq-flex-flex_inline-flex', + 'kbq-flex-wrap_wrap-reverse', + 'kbq-flex-order_-1', + 'kbq-flex-gap_row_l', + 'kbq-flex-gap_column_xl', + 'kbq-flex-direction_column-reverse', + 'kbq-flex-alignItems_center', + 'kbq-flex-justifyContent_space-between', + 'custom-class', + ].join(' ') + ); + }); + + it('should use gap as the row and column gap fallback', () => { + expect(flex({ gap: 'm' })).toBe( + ['kbq-flex', 'kbq-flex-gap_row_m', 'kbq-flex-gap_column_m'].join(' ') + ); + }); +}); diff --git a/packages/components/src/components/layout/flex/flex.ts b/packages/components/src/components/layout/flex/flex.ts index d94551c04..c30c983b0 100644 --- a/packages/components/src/components/layout/flex/flex.ts +++ b/packages/components/src/components/layout/flex/flex.ts @@ -1,112 +1,5 @@ -import { clsx } from '@koobiq/react-core'; +import './flex.css'; +import { getFlexClassNames } from './classNames'; +import type { FlexParams } from './types'; -import s from './flex.module.css'; - -export const flexPropAlignItems = [ - 'flex-start', - 'flex-end', - 'center', - 'baseline', - 'stretch', -] as const; -export type FlexPropAlignItems = (typeof flexPropAlignItems)[number]; - -export const flexPropGap = [ - '3xs', - 'xxs', - 'xs', - 's', - 'm', - 'l', - 'xl', - 'xxl', - '3xl', - '4xl', - '5xl', - '6xl', - '7xl', -] as const; -export type FlexPropGap = (typeof flexPropGap)[number]; - -export const flexPropJustifyContent = [ - 'flex-start', - 'flex-end', - 'center', - 'space-between', - 'space-around', - 'space-evenly', -] as const; -export type FlexPropJustifyContent = (typeof flexPropJustifyContent)[number]; - -export const flexPropFlex = ['flex', 'inline-flex'] as const; -export type FlexPropFlex = (typeof flexPropFlex)[number]; - -export const flexPropWrap = ['nowrap', 'wrap', 'wrap-reverse'] as const; -export type FlexPropWrap = (typeof flexPropWrap)[number]; - -export const flexPropDirection = [ - 'row', - 'row-reverse', - 'column', - 'column-reverse', -] as const; -export type FlexPropDirection = (typeof flexPropDirection)[number]; - -export const flexPropOrder = [-1, 0, 1] as const; -export type FlexPropOrder = (typeof flexPropOrder)[number]; - -export type FlexProps = { - /** Defines the `gap` property. */ - gap?: FlexPropGap; - /** Defines the `column-gap` property. */ - colGap?: FlexPropGap; - /** Defines the `row-gap` property. */ - rowGap?: FlexPropGap; - /** Defines the `display` property with `flex` or `inline-flex` value. */ - flex?: FlexPropFlex; - /** Defines the `flex-wrap` property. */ - wrap?: FlexPropWrap; - /** Defines the `order` property. */ - order?: FlexPropOrder; - /** Defines the `flex-direction` property. */ - direction?: FlexPropDirection; - /** Defines the `align-items` property. */ - alignItems?: FlexPropAlignItems; - /** Defines the `justify-content` property. */ - justifyContent?: FlexPropJustifyContent; -}; - -export type FlexParams = (props: FlexProps, className?: string) => string; - -/** The flex mixin turns the element it’s applied to into a flex container. */ -export const flex: FlexParams = (props, className) => { - const { - alignItems, - justifyContent, - flex, - wrap, - direction, - gap, - rowGap: rowGapProp, - colGap: colGapProp, - order: orderProp, - } = props; - - const order = String(orderProp); - - const colGap = colGapProp ?? gap; - const rowGap = rowGapProp ?? gap; - - return clsx( - s.base, - flex && s[`flex_${flex}`], - wrap && s[`wrap_${wrap}`], - order && s[`order_${order}`], - rowGap && s[`gap_row_${rowGap}`], - colGap && s[`gap_column_${colGap}`], - direction && s[`direction_${direction}`], - alignItems && s[`alignItems_${alignItems}`], - justifyContent && s[`justifyContent_${justifyContent}`], - className - ); -}; +export const flex: FlexParams = getFlexClassNames; diff --git a/packages/components/src/components/layout/flex/index.ts b/packages/components/src/components/layout/flex/index.ts index ece6890a3..6ef28bf01 100644 --- a/packages/components/src/components/layout/flex/index.ts +++ b/packages/components/src/components/layout/flex/index.ts @@ -1 +1,2 @@ export * from './flex'; +export * from './types'; diff --git a/packages/components/src/components/layout/flex/types.ts b/packages/components/src/components/layout/flex/types.ts new file mode 100644 index 000000000..80bbe14a9 --- /dev/null +++ b/packages/components/src/components/layout/flex/types.ts @@ -0,0 +1,75 @@ +export const flexPropAlignItems = [ + 'flex-start', + 'flex-end', + 'center', + 'baseline', + 'stretch', +] as const; +export type FlexPropAlignItems = (typeof flexPropAlignItems)[number]; + +export const flexPropGap = [ + '3xs', + 'xxs', + 'xs', + 's', + 'm', + 'l', + 'xl', + 'xxl', + '3xl', + '4xl', + '5xl', + '6xl', + '7xl', +] as const; +export type FlexPropGap = (typeof flexPropGap)[number]; + +export const flexPropJustifyContent = [ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + 'space-evenly', +] as const; +export type FlexPropJustifyContent = (typeof flexPropJustifyContent)[number]; + +export const flexPropFlex = ['flex', 'inline-flex'] as const; +export type FlexPropFlex = (typeof flexPropFlex)[number]; + +export const flexPropWrap = ['nowrap', 'wrap', 'wrap-reverse'] as const; +export type FlexPropWrap = (typeof flexPropWrap)[number]; + +export const flexPropDirection = [ + 'row', + 'row-reverse', + 'column', + 'column-reverse', +] as const; +export type FlexPropDirection = (typeof flexPropDirection)[number]; + +export const flexPropOrder = [-1, 0, 1] as const; +export type FlexPropOrder = (typeof flexPropOrder)[number]; + +export type FlexProps = { + /** Defines the `gap` property. */ + gap?: FlexPropGap; + /** Defines the `column-gap` property. */ + colGap?: FlexPropGap; + /** Defines the `row-gap` property. */ + rowGap?: FlexPropGap; + /** Defines the `display` property with `flex` or `inline-flex` value. */ + flex?: FlexPropFlex; + /** Defines the `flex-wrap` property. */ + wrap?: FlexPropWrap; + /** Defines the `order` property. */ + order?: FlexPropOrder; + /** Defines the `flex-direction` property. */ + direction?: FlexPropDirection; + /** Defines the `align-items` property. */ + alignItems?: FlexPropAlignItems; + /** Defines the `justify-content` property. */ + justifyContent?: FlexPropJustifyContent; +}; + +export type FlexParams = (props: FlexProps, className?: string) => string; diff --git a/tools/public_api_guard/components/layout.api.md b/tools/public_api_guard/components/layout.api.md index ddf09f4c0..69cb1a261 100644 --- a/tools/public_api_guard/components/layout.api.md +++ b/tools/public_api_guard/components/layout.api.md @@ -4,7 +4,7 @@ ```ts -// @public +// @public (undocumented) export const flex: FlexParams; // @public (undocumented) From acf74f79d01241d73af912448954cef5d486c395 Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Thu, 30 Jul 2026 17:37:51 +0300 Subject: [PATCH 2/5] fix(flex): ensure utility styles override component styles (round 2) --- .../src/components/FlexBox/FlexBox.module.css | 199 ++++++++++++++++++ .../src/components/FlexBox/FlexBox.test.tsx | 23 +- .../src/components/FlexBox/FlexBox.tsx | 33 +-- .../src/components/layout/flex/classNames.ts | 38 ---- .../layout/flex/{flex.css => flex.module.css} | 100 ++++----- .../src/components/layout/flex/flex.test.ts | 21 +- .../src/components/layout/flex/flex.ts | 36 +++- 7 files changed, 323 insertions(+), 127 deletions(-) create mode 100644 packages/components/src/components/FlexBox/FlexBox.module.css delete mode 100644 packages/components/src/components/layout/flex/classNames.ts rename packages/components/src/components/layout/flex/{flex.css => flex.module.css} (63%) diff --git a/packages/components/src/components/FlexBox/FlexBox.module.css b/packages/components/src/components/FlexBox/FlexBox.module.css new file mode 100644 index 000000000..b99f00063 --- /dev/null +++ b/packages/components/src/components/FlexBox/FlexBox.module.css @@ -0,0 +1,199 @@ +.base { + --flex-type: flex; + --flex-wrap: nowrap; + --flex-direction: row; + --flex-align-items: flex-start; + --flex-justify-content: flex-start; + + gap: var(--flex-row-gap) var(--flex-column-gap); + display: var(--flex-type); + align-items: var(--flex-align-items); + justify-content: var(--flex-justify-content); + flex-flow: var(--flex-direction) var(--flex-wrap); +} + +.alignItems_flex-start { + --flex-align-items: flex-start; +} + +.alignItems_flex-end { + --flex-align-items: flex-end; +} + +.alignItems_center { + --flex-align-items: center; +} + +.alignItems_baseline { + --flex-align-items: baseline; +} + +.alignItems_stretch { + --flex-align-items: stretch; +} + +.justifyContent_flex-start { + --flex-justify-content: flex-start; +} + +.justifyContent_flex-end { + --flex-justify-content: flex-end; +} + +.justifyContent_center { + --flex-justify-content: center; +} + +.justifyContent_space-between { + --flex-justify-content: space-between; +} + +.justifyContent_space-around { + --flex-justify-content: space-around; +} + +.justifyContent_space-evenly { + --flex-justify-content: space-evenly; +} + +.flex_flex { + --flex-type: flex; +} + +.flex_inline-flex { + --flex-type: inline-flex; +} + +.wrap_wrap { + --flex-wrap: wrap; +} + +.wrap_nowrap { + --flex-wrap: nowrap; +} + +.wrap_wrap-reverse { + --flex-wrap: wrap-reverse; +} + +.direction_row { + --flex-direction: row; +} + +.direction_row-reverse { + --flex-direction: row-reverse; +} + +.direction_column { + --flex-direction: column; +} + +.direction_column-reverse { + --flex-direction: column-reverse; +} + +/* column-gap */ +.gap_column_3xs { + --flex-column-gap: var(--kbq-size-3xs); +} + +.gap_column_xxs { + --flex-column-gap: var(--kbq-size-xxs); +} + +.gap_column_xs { + --flex-column-gap: var(--kbq-size-xs); +} + +.gap_column_s { + --flex-column-gap: var(--kbq-size-s); +} + +.gap_column_m { + --flex-column-gap: var(--kbq-size-m); +} + +.gap_column_l { + --flex-column-gap: var(--kbq-size-l); +} + +.gap_column_xl { + --flex-column-gap: var(--kbq-size-xl); +} + +.gap_column_xxl { + --flex-column-gap: var(--kbq-size-xxl); +} + +.gap_column_3xl { + --flex-column-gap: var(--kbq-size-3xl); +} + +.gap_column_4xl { + --flex-column-gap: var(--kbq-size-4xl); +} + +.gap_column_5xl { + --flex-column-gap: var(--kbq-size-5xl); +} + +.gap_column_6xl { + --flex-column-gap: var(--kbq-size-6xl); +} + +.gap_column_7xl { + --flex-column-gap: var(--kbq-size-7xl); +} + +/* row-gap */ +.gap_row_3xs { + --flex-row-gap: var(--kbq-size-3xs); +} + +.gap_row_xxs { + --flex-row-gap: var(--kbq-size-xxs); +} + +.gap_row_xs { + --flex-row-gap: var(--kbq-size-xs); +} + +.gap_row_s { + --flex-row-gap: var(--kbq-size-s); +} + +.gap_row_m { + --flex-row-gap: var(--kbq-size-m); +} + +.gap_row_l { + --flex-row-gap: var(--kbq-size-l); +} + +.gap_row_xl { + --flex-row-gap: var(--kbq-size-xl); +} + +.gap_row_xxl { + --flex-row-gap: var(--kbq-size-xxl); +} + +.gap_row_3xl { + --flex-row-gap: var(--kbq-size-3xl); +} + +.gap_row_4xl { + --flex-row-gap: var(--kbq-size-4xl); +} + +.gap_row_5xl { + --flex-row-gap: var(--kbq-size-5xl); +} + +.gap_row_6xl { + --flex-row-gap: var(--kbq-size-6xl); +} + +.gap_row_7xl { + --flex-row-gap: var(--kbq-size-7xl); +} diff --git a/packages/components/src/components/FlexBox/FlexBox.test.tsx b/packages/components/src/components/FlexBox/FlexBox.test.tsx index a32aec84c..cb801a9d8 100644 --- a/packages/components/src/components/FlexBox/FlexBox.test.tsx +++ b/packages/components/src/components/FlexBox/FlexBox.test.tsx @@ -5,6 +5,7 @@ import { describe, it, expect } from 'vitest'; import { BreakpointsContext, type BreakpointsContextType } from '../Provider'; +import s from './FlexBox.module.css'; import { FlexBox } from './index'; import type { FlexBoxProps } from './index'; @@ -46,14 +47,14 @@ describe('FlexBox', () => { ); expect(screen.getByTestId('flex-box')).toHaveClass( - 'kbq-flex', - 'kbq-flex-flex_inline-flex', - 'kbq-flex-wrap_wrap-reverse', - 'kbq-flex-gap_row_l', - 'kbq-flex-gap_column_xl', - 'kbq-flex-direction_column-reverse', - 'kbq-flex-alignItems_center', - 'kbq-flex-justifyContent_space-between' + s.base, + s['flex_inline-flex'], + s['wrap_wrap-reverse'], + s.gap_row_l, + s.gap_column_xl, + s['direction_column-reverse'], + s.alignItems_center, + s['justifyContent_space-between'] ); }); @@ -74,9 +75,9 @@ describe('FlexBox', () => { ); expect(screen.getByTestId('flex-box')).toHaveClass( - 'kbq-flex-gap_row_m', - 'kbq-flex-gap_column_m', - 'kbq-flex-direction_row' + s.gap_row_m, + s.gap_column_m, + s.direction_row ); }); }); diff --git a/packages/components/src/components/FlexBox/FlexBox.tsx b/packages/components/src/components/FlexBox/FlexBox.tsx index 7c023feb8..2aa7e8961 100644 --- a/packages/components/src/components/FlexBox/FlexBox.tsx +++ b/packages/components/src/components/FlexBox/FlexBox.tsx @@ -5,9 +5,9 @@ import type { ComponentPropsWithRef, ElementType } from 'react'; import { clsx, polymorphicForwardRef } from '@koobiq/react-core'; import { getResponsiveValue } from '../../utils'; -import { getFlexClassNames } from '../layout/flex/classNames'; import { useMatchedBreakpoints } from '../Provider'; +import s from './FlexBox.module.css'; import type { FlexBoxBaseProps } from './index'; /** @@ -38,26 +38,29 @@ export const FlexBox = polymorphicForwardRef<'div', FlexBoxBaseProps>( const flex = getResponsiveValue(flexProp, breakpoints); const gap = getResponsiveValue(gapProp, breakpoints); - const colGap = getResponsiveValue(colGapProp, breakpoints); - const rowGap = getResponsiveValue(rowGapProp, breakpoints); + const colGap = getResponsiveValue(colGapProp, breakpoints) ?? gap; + const rowGap = getResponsiveValue(rowGapProp, breakpoints) ?? gap; const wrap = getResponsiveValue(wrapProp, breakpoints); const alignItems = getResponsiveValue(alignItemsProp, breakpoints); const direction = getResponsiveValue(directionProp, breakpoints); const justifyContent = getResponsiveValue(justifyContentProp, breakpoints); - const flexCn = getFlexClassNames({ - gap, - flex, - wrap, - colGap, - rowGap, - direction, - alignItems, - justifyContent, - }); - return ( - + {children} ); diff --git a/packages/components/src/components/layout/flex/classNames.ts b/packages/components/src/components/layout/flex/classNames.ts deleted file mode 100644 index 73d2b5e80..000000000 --- a/packages/components/src/components/layout/flex/classNames.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { clsx } from '@koobiq/react-core'; - -import type { FlexProps } from './types'; - -const baseClassName = 'kbq-flex'; - -export const getFlexClassNames = ( - props: FlexProps, - className?: string -): string => { - const { - alignItems, - justifyContent, - flex, - wrap, - direction, - gap, - rowGap: rowGapProp, - colGap: colGapProp, - order, - } = props; - - const colGap = colGapProp ?? gap; - const rowGap = rowGapProp ?? gap; - - return clsx( - baseClassName, - flex && `${baseClassName}-flex_${flex}`, - wrap && `${baseClassName}-wrap_${wrap}`, - order !== undefined && `${baseClassName}-order_${order}`, - rowGap && `${baseClassName}-gap_row_${rowGap}`, - colGap && `${baseClassName}-gap_column_${colGap}`, - direction && `${baseClassName}-direction_${direction}`, - alignItems && `${baseClassName}-alignItems_${alignItems}`, - justifyContent && `${baseClassName}-justifyContent_${justifyContent}`, - className - ); -}; diff --git a/packages/components/src/components/layout/flex/flex.css b/packages/components/src/components/layout/flex/flex.module.css similarity index 63% rename from packages/components/src/components/layout/flex/flex.css rename to packages/components/src/components/layout/flex/flex.module.css index 0acbfac93..26e65f6b9 100644 --- a/packages/components/src/components/layout/flex/flex.css +++ b/packages/components/src/components/layout/flex/flex.module.css @@ -1,4 +1,4 @@ -.kbq-flex { +.base { --flex-gap: 0; --flex-order: 0; --flex-type: flex; @@ -15,200 +15,200 @@ flex-flow: var(--flex-direction) var(--flex-wrap); } -.kbq-flex-alignItems_flex-start { +.alignItems_flex-start { --flex-align-items: flex-start; } -.kbq-flex-alignItems_flex-end { +.alignItems_flex-end { --flex-align-items: flex-end; } -.kbq-flex-alignItems_center { +.alignItems_center { --flex-align-items: center; } -.kbq-flex-alignItems_baseline { +.alignItems_baseline { --flex-align-items: baseline; } -.kbq-flex-alignItems_stretch { +.alignItems_stretch { --flex-align-items: stretch; } -.kbq-flex-justifyContent_flex-start { +.justifyContent_flex-start { --flex-justify-content: flex-start; } -.kbq-flex-justifyContent_flex-end { +.justifyContent_flex-end { --flex-justify-content: flex-end; } -.kbq-flex-justifyContent_center { +.justifyContent_center { --flex-justify-content: center; } -.kbq-flex-justifyContent_space-between { +.justifyContent_space-between { --flex-justify-content: space-between; } -.kbq-flex-justifyContent_space-around { +.justifyContent_space-around { --flex-justify-content: space-around; } -.kbq-flex-justifyContent_space-evenly { +.justifyContent_space-evenly { --flex-justify-content: space-evenly; } -.kbq-flex-flex_flex { +.flex_flex { --flex-type: flex; } -.kbq-flex-flex_inline-flex { +.flex_inline-flex { --flex-type: inline-flex; } -.kbq-flex-wrap_wrap { +.wrap_wrap { --flex-wrap: wrap; } -.kbq-flex-wrap_nowrap { +.wrap_nowrap { --flex-wrap: nowrap; } -.kbq-flex-wrap_wrap-reverse { +.wrap_wrap-reverse { --flex-wrap: wrap-reverse; } -.kbq-flex-direction_row { +.direction_row { --flex-direction: row; } -.kbq-flex-direction_row-reverse { +.direction_row-reverse { --flex-direction: row-reverse; } -.kbq-flex-direction_column { +.direction_column { --flex-direction: column; } -.kbq-flex-direction_column-reverse { +.direction_column-reverse { --flex-direction: column-reverse; } -.kbq-flex-order_-1 { +.order_-1 { --flex-order: -1; } -.kbq-flex-order_0 { +.order_0 { --flex-order: 0; } -.kbq-flex-order_1 { +.order_1 { --flex-order: 1; } /* column-gap */ -.kbq-flex-gap_column_3xs { +.gap_column_3xs { --flex-column-gap: var(--kbq-size-3xs); } -.kbq-flex-gap_column_xxs { +.gap_column_xxs { --flex-column-gap: var(--kbq-size-xxs); } -.kbq-flex-gap_column_xs { +.gap_column_xs { --flex-column-gap: var(--kbq-size-xs); } -.kbq-flex-gap_column_s { +.gap_column_s { --flex-column-gap: var(--kbq-size-s); } -.kbq-flex-gap_column_m { +.gap_column_m { --flex-column-gap: var(--kbq-size-m); } -.kbq-flex-gap_column_l { +.gap_column_l { --flex-column-gap: var(--kbq-size-l); } -.kbq-flex-gap_column_xl { +.gap_column_xl { --flex-column-gap: var(--kbq-size-xl); } -.kbq-flex-gap_column_xxl { +.gap_column_xxl { --flex-column-gap: var(--kbq-size-xxl); } -.kbq-flex-gap_column_3xl { +.gap_column_3xl { --flex-column-gap: var(--kbq-size-3xl); } -.kbq-flex-gap_column_4xl { +.gap_column_4xl { --flex-column-gap: var(--kbq-size-4xl); } -.kbq-flex-gap_column_5xl { +.gap_column_5xl { --flex-column-gap: var(--kbq-size-5xl); } -.kbq-flex-gap_column_6xl { +.gap_column_6xl { --flex-column-gap: var(--kbq-size-6xl); } -.kbq-flex-gap_column_7xl { +.gap_column_7xl { --flex-column-gap: var(--kbq-size-7xl); } /* row-gap */ -.kbq-flex-gap_row_3xs { +.gap_row_3xs { --flex-row-gap: var(--kbq-size-3xs); } -.kbq-flex-gap_row_xxs { +.gap_row_xxs { --flex-row-gap: var(--kbq-size-xxs); } -.kbq-flex-gap_row_xs { +.gap_row_xs { --flex-row-gap: var(--kbq-size-xs); } -.kbq-flex-gap_row_s { +.gap_row_s { --flex-row-gap: var(--kbq-size-s); } -.kbq-flex-gap_row_m { +.gap_row_m { --flex-row-gap: var(--kbq-size-m); } -.kbq-flex-gap_row_l { +.gap_row_l { --flex-row-gap: var(--kbq-size-l); } -.kbq-flex-gap_row_xl { +.gap_row_xl { --flex-row-gap: var(--kbq-size-xl); } -.kbq-flex-gap_row_xxl { +.gap_row_xxl { --flex-row-gap: var(--kbq-size-xxl); } -.kbq-flex-gap_row_3xl { +.gap_row_3xl { --flex-row-gap: var(--kbq-size-3xl); } -.kbq-flex-gap_row_4xl { +.gap_row_4xl { --flex-row-gap: var(--kbq-size-4xl); } -.kbq-flex-gap_row_5xl { +.gap_row_5xl { --flex-row-gap: var(--kbq-size-5xl); } -.kbq-flex-gap_row_6xl { +.gap_row_6xl { --flex-row-gap: var(--kbq-size-6xl); } -.kbq-flex-gap_row_7xl { +.gap_row_7xl { --flex-row-gap: var(--kbq-size-7xl); } diff --git a/packages/components/src/components/layout/flex/flex.test.ts b/packages/components/src/components/layout/flex/flex.test.ts index 04e2f01ae..d30ced1a7 100644 --- a/packages/components/src/components/layout/flex/flex.test.ts +++ b/packages/components/src/components/layout/flex/flex.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { flex } from './flex'; +import s from './flex.module.css'; describe('flex', () => { it('should generate classes for every flex property', () => { @@ -21,15 +22,15 @@ describe('flex', () => { ) ).toBe( [ - 'kbq-flex', - 'kbq-flex-flex_inline-flex', - 'kbq-flex-wrap_wrap-reverse', - 'kbq-flex-order_-1', - 'kbq-flex-gap_row_l', - 'kbq-flex-gap_column_xl', - 'kbq-flex-direction_column-reverse', - 'kbq-flex-alignItems_center', - 'kbq-flex-justifyContent_space-between', + s.base, + s['flex_inline-flex'], + s['wrap_wrap-reverse'], + s['order_-1'], + s.gap_row_l, + s.gap_column_xl, + s['direction_column-reverse'], + s.alignItems_center, + s['justifyContent_space-between'], 'custom-class', ].join(' ') ); @@ -37,7 +38,7 @@ describe('flex', () => { it('should use gap as the row and column gap fallback', () => { expect(flex({ gap: 'm' })).toBe( - ['kbq-flex', 'kbq-flex-gap_row_m', 'kbq-flex-gap_column_m'].join(' ') + [s.base, s.gap_row_m, s.gap_column_m].join(' ') ); }); }); diff --git a/packages/components/src/components/layout/flex/flex.ts b/packages/components/src/components/layout/flex/flex.ts index c30c983b0..1f9146d17 100644 --- a/packages/components/src/components/layout/flex/flex.ts +++ b/packages/components/src/components/layout/flex/flex.ts @@ -1,5 +1,35 @@ -import './flex.css'; -import { getFlexClassNames } from './classNames'; +import { clsx } from '@koobiq/react-core'; + +import s from './flex.module.css'; import type { FlexParams } from './types'; -export const flex: FlexParams = getFlexClassNames; +/** The flex mixin turns the element it’s applied to into a flex container. */ +export const flex: FlexParams = (props, className) => { + const { + alignItems, + justifyContent, + flex, + wrap, + direction, + gap, + rowGap: rowGapProp, + colGap: colGapProp, + order, + } = props; + + const colGap = colGapProp ?? gap; + const rowGap = rowGapProp ?? gap; + + return clsx( + s.base, + flex && s[`flex_${flex}`], + wrap && s[`wrap_${wrap}`], + order !== undefined && s[`order_${order}`], + rowGap && s[`gap_row_${rowGap}`], + colGap && s[`gap_column_${colGap}`], + direction && s[`direction_${direction}`], + alignItems && s[`alignItems_${alignItems}`], + justifyContent && s[`justifyContent_${justifyContent}`], + className + ); +}; From ddea296072164b3c18e50d9af5679e33364a21bd Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Thu, 30 Jul 2026 17:45:24 +0300 Subject: [PATCH 3/5] chore: approve api --- tools/public_api_guard/components/layout.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/public_api_guard/components/layout.api.md b/tools/public_api_guard/components/layout.api.md index 69cb1a261..ddf09f4c0 100644 --- a/tools/public_api_guard/components/layout.api.md +++ b/tools/public_api_guard/components/layout.api.md @@ -4,7 +4,7 @@ ```ts -// @public (undocumented) +// @public export const flex: FlexParams; // @public (undocumented) From 7da6e58600fdc24ace57b014e405dd1f11196bb9 Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Mon, 3 Aug 2026 12:25:07 +0300 Subject: [PATCH 4/5] test(flex): cover zero order value --- packages/components/src/components/layout/flex/flex.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/src/components/layout/flex/flex.test.ts b/packages/components/src/components/layout/flex/flex.test.ts index d30ced1a7..469b309e6 100644 --- a/packages/components/src/components/layout/flex/flex.test.ts +++ b/packages/components/src/components/layout/flex/flex.test.ts @@ -41,4 +41,8 @@ describe('flex', () => { [s.base, s.gap_row_m, s.gap_column_m].join(' ') ); }); + + it('should generate class for zero order', () => { + expect(flex({ order: 0 })).toBe([s.base, s.order_0].join(' ')); + }); }); From e38993e0b1966e623d136a07e8df3723c5058443 Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Mon, 3 Aug 2026 12:27:47 +0300 Subject: [PATCH 5/5] style(flex): use type-only import for FlexProps --- packages/components/src/components/layout/flex/flex.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/layout/flex/flex.stories.tsx b/packages/components/src/components/layout/flex/flex.stories.tsx index 3153b4f4c..07d1a9df0 100644 --- a/packages/components/src/components/layout/flex/flex.stories.tsx +++ b/packages/components/src/components/layout/flex/flex.stories.tsx @@ -10,9 +10,9 @@ import { flexPropGap, flexPropJustifyContent, flexPropOrder, - type FlexProps, flexPropWrap, } from './index'; +import type { FlexProps } from './index'; const meta = { title: 'Mixins/flex',