From bfb7bee2f08709d0b8354313cfffc93bef89f1d4 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Tue, 14 Oct 2025 17:16:46 +0200 Subject: [PATCH 1/2] feat: layout components --- src/components/Card/Card.tsx | 8 +- .../NumericArrows/NumericArrows.tsx | 2 +- src/components/layout/Box/Box.scss | 17 +- src/components/layout/Box/Box.tsx | 92 ++---- src/components/layout/Flex/Flex.tsx | 201 +++++++------ src/components/layout/Grid/Grid.scss | 7 + src/components/layout/Grid/Grid.tsx | 104 +++++++ src/components/layout/hooks/useStyleProps.ts | 192 +++++++++++++ src/components/layout/index.ts | 1 + src/components/layout/types.ts | 264 +++++++++++++++++- src/components/layout/utils/index.ts | 36 ++- .../ListContainerView/ListContainerView.tsx | 2 +- 12 files changed, 732 insertions(+), 194 deletions(-) create mode 100644 src/components/layout/Grid/Grid.scss create mode 100644 src/components/layout/Grid/Grid.tsx create mode 100644 src/components/layout/hooks/useStyleProps.ts diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 2ae313fe20..3a6f66574e 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -19,7 +19,9 @@ export type CardTheme = 'normal' | 'info' | 'success' | 'warning' | 'danger' | ' export type CardView = SelectionCardView | ContainerCardView; export type CardSize = 'm' | 'l'; -export interface CardProps extends Omit, 'as' | 'onClick'> { +export interface CardProps + extends Omit, + Omit, 'style'> { children: React.ReactNode; /** Card click handler. Available for type: 'selection', 'action' */ onClick?: (event: React.MouseEvent) => void; @@ -69,7 +71,6 @@ export const Card = React.forwardRef(function Card(pr return ( (function Card(pr }, className, )} - onClick={handleClick as BoxProps['onClick']} + onClick={handleClick} onKeyDown={isClickable ? onKeyDown : undefined} tabIndex={isClickable ? 0 : undefined} {...restProps} + ref={ref} > {children} diff --git a/src/components/NumberInput/NumericArrows/NumericArrows.tsx b/src/components/NumberInput/NumericArrows/NumericArrows.tsx index 9b3b4ff8d2..c5dee4efcf 100644 --- a/src/components/NumberInput/NumericArrows/NumericArrows.tsx +++ b/src/components/NumberInput/NumericArrows/NumericArrows.tsx @@ -15,7 +15,7 @@ import './NumericArrows.scss'; const b = block('numeric-arrows'); -interface NumericArrowsProps extends React.HTMLAttributes<'div'> { +interface NumericArrowsProps extends React.HTMLAttributes { className?: string; size: InputControlSize; disabled?: boolean; diff --git a/src/components/layout/Box/Box.scss b/src/components/layout/Box/Box.scss index 2e71c7e78b..9c74f0533c 100644 --- a/src/components/layout/Box/Box.scss +++ b/src/components/layout/Box/Box.scss @@ -1,21 +1,6 @@ @use '../variables.scss' as v; #{v.$boxBlock} { + /** TODO: remove box-sizing */ box-sizing: border-box; - - &_overflow_hidden { - overflow: hidden; - } - - &_overflow_auto { - overflow: auto; - } - - &_overflow_x { - overflow: hidden auto; - } - - &_overflow_y { - overflow: auto hidden; - } } diff --git a/src/components/layout/Box/Box.tsx b/src/components/layout/Box/Box.tsx index f28c3dc840..d25c524a2f 100644 --- a/src/components/layout/Box/Box.tsx +++ b/src/components/layout/Box/Box.tsx @@ -1,9 +1,20 @@ +'use client'; + import * as React from 'react'; -import type {QAProps} from '../../types'; +import type {DOMProps, QAProps} from '../../types'; import {block} from '../../utils/cn'; -import type {SpacingProps} from '../spacing/spacing'; +import {useStyleProps} from '../hooks/useStyleProps'; +import type {SpacingProps as SpacingPropsDeprecated} from '../spacing/spacing'; import {sp} from '../spacing/spacing'; +import type { + LayoutComponentProps, + LayoutProps, + PositioningProps, + SizingProps, + SpacingProps, + StylingProps, +} from '../types'; import './Box.scss'; @@ -11,25 +22,13 @@ const b = block('box'); export interface BoxProps extends QAProps, - React.HTMLAttributes, - React.PropsWithChildren< - Pick< - React.CSSProperties, - | 'width' - | 'height' - | 'maxHeight' - | 'maxWidth' - | 'minHeight' - | 'minWidth' - | 'position' - > - > { + DOMProps, + LayoutProps, + SpacingProps, + SizingProps, + PositioningProps, + StylingProps { as?: T; - /** - * Add overflow css properties to container - */ - overflow?: 'hidden' | 'x' | 'y' | 'auto'; - className?: string; /** * All spacing shortcut properties available here. * ```tsx @@ -37,15 +36,12 @@ export interface BoxProps * // margin-right: 12px * // padding-bottom: 8px * ``` + * @deprecated use `...` instead */ - spacing?: SpacingProps; + spacing?: SpacingPropsDeprecated; + children?: React.ReactNode; } -type BoxRef = React.ComponentPropsWithRef['ref']; - -type BoxPropsWithTypedAttrs = BoxProps & - Omit, keyof BoxProps>; - /** * Basic block to build other components and for standalone usage as a smart block with build in support of most usable css properties and shortcut `spacing` properties. * ```tsx @@ -57,50 +53,22 @@ type BoxPropsWithTypedAttrs = BoxProps & * * ``` */ -export const Box = React.forwardRef(function Box( - { - as, - children, - qa, - className, - width, - height, - minWidth, - minHeight, - maxHeight, - maxWidth, - position, - style: outerStyle, - spacing, - overflow, - ...props - }: BoxProps, - ref?: BoxRef, +export const Box = React.forwardRef(function Box( + {as, qa, spacing, ...props}, + ref, ) { const Tag: React.ElementType = as || 'div'; - const style: React.CSSProperties = { - width, - height, - minWidth, - minHeight, - maxHeight, - maxWidth, - position, - ...outerStyle, - }; + const {className, ...otherProps} = useStyleProps(props); return ( - {children} - + /> ); }) as (( - props: BoxPropsWithTypedAttrs & {ref?: BoxRef}, + props: LayoutComponentProps>, ) => React.ReactElement) & {displayName: string}; diff --git a/src/components/layout/Flex/Flex.tsx b/src/components/layout/Flex/Flex.tsx index a1355fe49f..214cf4521b 100644 --- a/src/components/layout/Flex/Flex.tsx +++ b/src/components/layout/Flex/Flex.tsx @@ -6,42 +6,37 @@ import {block} from '../../utils/cn'; import {Box} from '../Box/Box'; import type {BoxProps} from '../Box/Box'; import {useLayoutContext} from '../hooks/useLayoutContext'; -import type {AdaptiveProp, MediaPartial, Space} from '../types'; +import {getSpacingValue, useStyleProps} from '../hooks/useStyleProps'; +import type {StyleHandlers} from '../hooks/useStyleProps'; +import type { + AdaptiveProp, + BoxAlignmentStyleProps, + LayoutComponentProps, + Space, + SpacingValue, +} from '../types'; import {makeCssMod} from '../utils'; import './Flex.scss'; const b = block('flex'); -export interface FlexProps extends BoxProps { - /** - * `flex-direction` property - */ - direction?: AdaptiveProp<'flexDirection'>; +interface DeprecatedProps { /** * `flex-grow` property + * @deprecated use flexGrow instead */ grow?: true | React.CSSProperties['flexGrow']; /** * `flex-basis` property + * @deprecated use flexBasis instead */ basis?: React.CSSProperties['flexBasis']; /** * `flex-shrink` property + * @deprecated use flexShrink instead */ shrink?: React.CSSProperties['flexShrink']; - /** - * `align-` properties - */ - alignContent?: AdaptiveProp<'justifyContent'>; - alignItems?: AdaptiveProp<'alignItems'>; - alignSelf?: AdaptiveProp<'alignSelf'>; - /** - * `justify-` properties - */ - justifyContent?: AdaptiveProp<'justifyContent'>; - justifyItems?: AdaptiveProp<'justifyItems'>; - justifySelf?: AdaptiveProp<'justifySelf'>; /** * Shortcut for: * @@ -49,20 +44,13 @@ export interface FlexProps extends BoxProps * justify-content: center; align-items: center; * ``` + * @deprecated */ centerContent?: true; /** - * `flex-wrap` property - * - * If value equals `true`, add css property `flex-wrap: wrap`; + * @deprecated use rowGap instead */ - wrap?: true | React.CSSProperties['flexWrap']; - /** - * display: inline-flex; - */ - inline?: boolean; - gap?: Space | MediaPartial; - gapRow?: Space | MediaPartial; + gapRow?: AdaptiveProp; /** * @deprecated - use native gap property * Space between children. Works like gap but supports in old browsers. Under the hoods uses negative margins. Vertical and horizontal directions are also supported @@ -88,13 +76,62 @@ export interface FlexProps extends BoxProps * * ``` */ - space?: Space | MediaPartial; + space?: AdaptiveProp; +} + +interface FlexStyleProps + extends Omit { + /** The direction in which to layout children. */ + direction?: AdaptiveProp; + /** Whether to wrap items onto multiple lines. */ + wrap?: AdaptiveProp; + /** + * TODO: use gap from BoxAlignmentStyleProps + */ + gap?: AdaptiveProp; } -type FlexRef = React.ComponentPropsWithRef['ref']; +export interface FlexProps + extends BoxProps, + FlexStyleProps, + DeprecatedProps { + /** + * display: inline-flex; + */ + inline?: boolean; +} -type FlexPropsWithTypedAttrs = FlexProps & - Omit, keyof FlexProps>; +const flexStyleHandlers: StyleHandlers = { + direction: ['flexDirection'], + wrap: [ + 'flexWrap', + (v) => { + if (typeof v === 'boolean') { + return v ? 'wrap' : 'nowrap'; + } + return `${v}`; + }, + ], + justifyContent: ['justifyContent'], + alignItems: ['alignItems', flexAlignValue], + alignContent: ['alignContent', flexAlignValue], + placeContent: ['placeContent', (v) => `${v}`.trim().split(/\s+/).map(flexAlignValue).join(' ')], + gap: [ + 'gap', + (v) => { + const n = Number(v); + if (Number.isInteger(n)) { + return getSpacingValue(`spacing-${n}`); + } + if (n === 0.5) { + return getSpacingValue('spacing-half'); + } + return getSpacingValue(v); + }, + ], + columnGap: ['columnGap', getSpacingValue], + rowGap: ['rowGap', getSpacingValue], +}; /** * Flexbox model utility component. @@ -130,69 +167,51 @@ type FlexPropsWithTypedAttrs = FlexProps & * --- * Storybook - https://preview.gravity-ui.com/uikit/?path=/docs/layout--playground#flex */ -export const Flex = React.forwardRef(function Flex( - props: FlexProps, - ref: FlexRef, -) { +export const Flex = React.forwardRef(function Flex(props, ref) { + const {style, ...otherProps} = useStyleProps(props, flexStyleHandlers); + const { - as: propsAs, - direction, grow, basis, children, - style, - alignContent, - alignItems, - alignSelf, - justifyContent, - justifyItems, - justifySelf, shrink, - wrap, inline, - gap, gapRow, className, space, centerContent, ...restProps - } = props; + } = otherProps; - const as: React.ElementType = propsAs || 'div'; + const {getClosestMediaProps} = useLayoutContext(); - const { - getClosestMediaProps, - theme: {spaceBaseSize}, - } = useLayoutContext(); - - const applyMediaProps = ( - property?: P | MediaPartial

? V : P>, - ): P | (P extends MediaPartial ? V : P) | undefined => - typeof property === 'object' && property !== null - ? getClosestMediaProps(property) - : property; - - const gapSpaceSize = applyMediaProps(gap); - const columnGap = - typeof gapSpaceSize === 'undefined' ? undefined : spaceBaseSize * Number(gapSpaceSize); - - const gapRowSpaceSize = applyMediaProps(gapRow) || gapSpaceSize; - const rowGap = - typeof gapRowSpaceSize === 'undefined' - ? undefined - : spaceBaseSize * Number(gapRowSpaceSize); - - const spaceSize = applyMediaProps(space); + const gapRowSpaceSize = getClosestMediaProps(gapRow); + if (gapRowSpaceSize !== undefined && props.rowGap === undefined) { + style.rowGap = `calc(var(--g-spacing-base) * ${Number(gapRowSpaceSize)})`; + } + + const spaceSize = getClosestMediaProps(space); const s = - typeof gap === 'undefined' && - typeof gapRow === 'undefined' && + style.gap === undefined && + style.columnGap === undefined && + style.rowGap === undefined && typeof spaceSize !== 'undefined' ? makeCssMod(spaceSize) : undefined; + if (props.flexGrow === undefined && grow !== undefined) { + style.flexGrow = grow === true ? '1' : grow; + } + if (props.flexBasis === undefined && basis !== undefined) { + style.flexBasis = basis; + } + if (props.flexShrink === undefined && shrink !== undefined) { + style.flexShrink = shrink; + } + return ( {space ? React.Children.map(children, (child) => @@ -229,5 +232,17 @@ export const Flex = React.forwardRef(function Flex ); }) as (( - props: FlexPropsWithTypedAttrs & {ref?: FlexRef}, + props: LayoutComponentProps>, ) => React.ReactElement) & {displayName: string}; + +function flexAlignValue(value: unknown) { + if (value === 'start') { + return 'flex-start'; + } + + if (value === 'end') { + return 'flex-end'; + } + + return `${value}`; +} diff --git a/src/components/layout/Grid/Grid.scss b/src/components/layout/Grid/Grid.scss new file mode 100644 index 0000000000..58426790a0 --- /dev/null +++ b/src/components/layout/Grid/Grid.scss @@ -0,0 +1,7 @@ +@use '../../variables'; + +$block: '.#{variables.$ns}grid'; + +#{$block} { + display: grid; +} diff --git a/src/components/layout/Grid/Grid.tsx b/src/components/layout/Grid/Grid.tsx new file mode 100644 index 0000000000..c6bc2e42be --- /dev/null +++ b/src/components/layout/Grid/Grid.tsx @@ -0,0 +1,104 @@ +'use client'; + +import * as React from 'react'; + +import type {DOMProps} from '../../types'; +import {block} from '../../utils/cn'; +import {Box} from '../Box/Box'; +import type {BoxProps} from '../Box/Box'; +import {getSpacingValue, useStyleProps} from '../hooks/useStyleProps'; +import type {StyleHandlers} from '../hooks/useStyleProps'; +import type { + AdaptiveProp, + BoxAlignmentStyleProps, + LayoutComponentProps, + SpacingValue, +} from '../types'; + +import './Grid.scss'; + +const b = block('grid'); + +interface GridStyleProps extends BoxAlignmentStyleProps { + /** Defines named grid areas. */ + areas?: AdaptiveProp; + /** Defines the sizes of each row in the grid. */ + rows?: AdaptiveProp; + /** Defines the sizes of each column in the grid. */ + columns?: AdaptiveProp; + /** Defines the size of implicitly generated columns. */ + autoColumns?: AdaptiveProp; + /** Defines the size of implicitly generated rows. */ + autoRows?: AdaptiveProp; + /** Controls how auto-placed items are flowed into the grid. */ + autoFlow?: AdaptiveProp; +} + +const gridStyleHandlers: StyleHandlers = { + areas: [ + 'gridArea', + (value: unknown) => + Array.isArray(value) ? value.map((v) => `"${v}"`).join('\n') : undefined, + ], + rows: ['gridTemplateRows', gridTemplateValue], + columns: ['gridTemplateColumns', gridTemplateValue], + autoColumns: ['gridAutoColumns', getSpacingValue], + autoRows: ['gridAutoRows', getSpacingValue], + autoFlow: ['gridAutoFlow'], + + justifyContent: ['justifyContent'], + justifyItems: ['justifyItems'], + alignItems: ['alignItems'], + alignContent: ['alignContent'], + placeContent: ['placeContent'], + placeItems: ['placeItems'], + gap: ['gap', getSpacingValue], + columnGap: ['columnGap', getSpacingValue], + rowGap: ['rowGap', getSpacingValue], +}; + +function gridTemplateValue(value: unknown) { + if (Array.isArray(value)) { + return value.map(getSpacingValue).join(' '); + } + return getSpacingValue(value); +} +/** + * Can be used to make a repeating fragment of the columns or rows list. + * @param count - The number of times to repeat the fragment. + * @param fragment - The fragment to repeat. + */ +export function repeat( + count: number | 'auto-fill' | 'auto-fit', + fragment: SpacingValue | SpacingValue[], +): string { + return `repeat(${count}, ${gridTemplateValue(fragment)})`; +} + +/** + * Defines a size range greater than or equal to min and less than or equal to max. + * @param min - The minimum size. + * @param max - The maximum size. + */ +export function minmax(min: SpacingValue, max: SpacingValue): string { + return `minmax(${getSpacingValue(min)}, ${getSpacingValue(max)})`; +} + +/** + * Clamps a given size to an available size. + * @param dimension - The size to clamp. + */ +export function fitContent(dimension: SpacingValue): string { + return `fit-content(${getSpacingValue(dimension)})`; +} + +export interface GridProps = 'div'> + extends BoxProps, + GridStyleProps {} + +export const Grid = React.forwardRef(function Grid(props, ref) { + const boxProps = useStyleProps(props, gridStyleHandlers); + return ; +}) as (( + props: LayoutComponentProps>, +) => React.ReactElement) & {displayName: string}; diff --git a/src/components/layout/hooks/useStyleProps.ts b/src/components/layout/hooks/useStyleProps.ts new file mode 100644 index 0000000000..ebe5f3b548 --- /dev/null +++ b/src/components/layout/hooks/useStyleProps.ts @@ -0,0 +1,192 @@ +import type {CSSProperties, DOMProps} from '../../types'; +import type { + LayoutProps, + PositioningProps, + SizingProps, + SpacingProps, + StylingProps, +} from '../types'; + +import {useLayoutContext} from './useLayoutContext'; + +type CSSKey = keyof React.CSSProperties; + +type StyleName = CSSKey | CSSKey[]; +type StyleHandler = (value: unknown) => string | undefined; +export type StyleHandlers = { + [key in T]: [StyleName, StyleHandler?]; +}; + +type BaseStyleProperties = + | keyof LayoutProps + | keyof SizingProps + | keyof SpacingProps + | keyof PositioningProps + | keyof StylingProps; + +const baseStyleHandlers: StyleHandlers = { + flex: ['flex'], + flexGrow: ['flexGrow'], + flexBasis: ['flexBasis'], + flexShrink: ['flexShrink'], + alignSelf: ['alignSelf'], + justifySelf: ['justifySelf'], + placeSelf: ['placeSelf'], + order: ['order'], + gridArea: ['gridArea'], + gridColumn: ['gridColumn'], + gridRow: ['gridRow'], + gridColumnStart: ['gridColumnStart'], + gridColumnEnd: ['gridColumnEnd'], + gridRowStart: ['gridRowStart'], + gridRowEnd: ['gridRowEnd'], + overflow: ['overflow', getOverflowValue], + width: ['width', getSpacingValue], + minWidth: ['minWidth', getSpacingValue], + maxWidth: ['maxWidth', getSpacingValue], + height: ['height', getSpacingValue], + minHeight: ['minHeight', getSpacingValue], + maxHeight: ['maxHeight', getSpacingValue], + position: ['position'], + inset: ['inset', getSpacingValue], + top: ['insetBlockStart', getSpacingValue], + bottom: ['insetBlockEnd', getSpacingValue], + start: ['insetInlineStart', getSpacingValue], + end: ['insetInlineEnd', getSpacingValue], + zIndex: ['zIndex'], + margin: ['margin', getSpacingValue], + marginInline: ['marginInline', getSpacingValue], + marginStart: ['marginInlineStart', getSpacingValue], + marginEnd: ['marginInlineEnd', getSpacingValue], + marginBlock: ['marginBlock', getSpacingValue], + marginTop: ['marginBlockStart', getSpacingValue], + marginBottom: ['marginBlockEnd', getSpacingValue], + padding: ['padding', getSpacingValue], + paddingInline: ['paddingInline', getSpacingValue], + paddingStart: ['paddingInlineStart', getSpacingValue], + paddingEnd: ['paddingInlineEnd', getSpacingValue], + paddingBlock: ['paddingBlock', getSpacingValue], + paddingTop: ['paddingBlockStart', getSpacingValue], + paddingBottom: ['paddingBlockEnd', getSpacingValue], + + backgroundColor: ['backgroundColor', getBackgroundColorValue], + borderWidth: ['borderWidth'], + borderInlineWidth: ['borderInlineWidth'], + borderBlockWidth: ['borderBlockWidth'], + borderStartWidth: ['borderInlineStartWidth'], + borderEndWidth: ['borderInlineEndWidth'], + borderTopWidth: ['borderBlockStartWidth'], + borderBottomWidth: ['borderBlockEndWidth'], + borderColor: ['borderColor', getLineColorValue], + borderInlineColor: ['borderInlineColor', getLineColorValue], + borderBlockColor: ['borderBlockColor', getLineColorValue], + borderStartColor: ['borderInlineStartColor', getLineColorValue], + borderEndColor: ['borderInlineEndColor', getLineColorValue], + borderTopColor: ['borderTopColor', getLineColorValue], + borderBottomColor: ['borderBottomColor', getLineColorValue], + borderRadius: ['borderRadius', getRadiusValue], + borderTopStartRadius: ['borderStartStartRadius', getRadiusValue], + borderTopEndRadius: ['borderStartEndRadius', getRadiusValue], + borderBottomStartRadius: ['borderEndStartRadius', getRadiusValue], + borderBottomEndRadius: ['borderEndEndRadius', getRadiusValue], +}; + +const borderStyleProps = { + borderWidth: 'borderStyle', + borderStartWidth: 'borderInlineStartStyle', + borderEndWidth: 'borderInlineEndStyle', + borderTopWidth: 'borderBlockStartStyle', + borderBottomWidth: 'borderBlockEndStyle', +} as const; + +export function useStyleProps< + T extends DOMProps, + S extends StyleHandlers = typeof baseStyleHandlers, +>(props: T, handlers?: S): {style: CSSProperties} & Omit { + const {getClosestMediaProps} = useLayoutContext(); + const styleHandlers = handlers ?? baseStyleHandlers; + + const style: CSSProperties = {}; + const otherProps: Record = {}; + for (const [key, value] of Object.entries(props)) { + const styleProp = styleHandlers[key as keyof typeof styleHandlers]; + if (!styleProp) { + otherProps[key] = value; + } + if (!styleProp || value === undefined || value === null) { + continue; + } + + const prop = getClosestMediaProps(value); + const [name, convert] = styleProp; + const v = typeof convert === 'function' ? convert(prop) : prop; + if (Array.isArray(name)) { + for (const n of name) { + style[n] = v; + } + } else { + style[name] = v; + } + } + + for (const [key, value] of Object.entries(borderStyleProps)) { + if (style[key as CSSKey]) { + style[value] = 'solid'; + style.boxSizing = 'border-box'; + } + } + + return { + ...(otherProps as Omit), + style: {...props.style, ...style}, + }; +} + +function getOverflowValue(v: unknown) { + switch (v) { + case 'hidden': { + return 'hidden'; + } + case 'x': { + return 'hidden auto'; + } + case 'y': { + return 'auto hidden'; + } + case 'auto': { + return 'auto'; + } + } + + return `${v}`; +} + +function getBackgroundColorValue(value: unknown) { + return `var(--g-color-base-${value})`; +} + +function getLineColorValue(value: unknown) { + return `var(--g-color-line-${value})`; +} + +function getRadiusValue(value: unknown) { + if (value === 'focus') { + return 'var(--g-focus-border-radius)'; + } + return `var(--g-border-radius-${value})`; +} + +const spacingRe = /^spacing-(\d+|half)$/; +export function getSpacingValue(value: unknown) { + if (typeof value === 'string' && spacingRe.test(value)) { + const v = value.slice('spacing-'.length); + const n = v === 'half' ? 0.5 : Number(v); + return `calc(var(--g-spacing-base) * ${n})`; + } + + if (typeof value === 'number') { + return `${value}px`; + } + + return `${value}`; +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 11eef83fc5..bae02e28ea 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -3,6 +3,7 @@ export * from './Col/Col'; export * from './Row/Row'; export * from './Flex/Flex'; export * from './Box/Box'; +export * from './Grid/Grid'; export * from './Container/Container'; export * from './spacing/spacing'; diff --git a/src/components/layout/types.ts b/src/components/layout/types.ts index c6e0ee1998..f5c3d7f6d4 100644 --- a/src/components/layout/types.ts +++ b/src/components/layout/types.ts @@ -50,14 +50,20 @@ export type Space = | 9 | 10; -// TODO BREAKING CHANGE: xxl -> 2xl, xxxl -> 3xl -export type MediaType = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl'; +export type MediaProps = { + xs: T; + s: T; + m: T; + l: T; + xl: T; + xxl: T; + xxxl: T; +}; -export type MediaProps = Record; +// TODO BREAKING CHANGE: xxl -> 2xl, xxxl -> 3xl +export type MediaType = keyof MediaProps; -export type AdaptiveProp = - | React.CSSProperties[T] - | MediaPartial; +export type AdaptiveProp = T | MediaPartial; export type MediaPartial = Partial>; @@ -102,3 +108,249 @@ export interface LayoutTheme { export interface IsMediaActive { (toCheck: MediaType): boolean; } + +export interface LayoutProps { + /** When used in a flex layout, specifies how the element will grow or shrink to fit the space available. */ + flex?: AdaptiveProp; + /** When used in a flex layout, specifies how the element will grow to fit the space available. */ + flexGrow?: AdaptiveProp; + /** When used in a flex layout, specifies the initial main size of the element. */ + flexBasis?: AdaptiveProp; + /** When used in a flex layout, specifies how the element will shrink to fit the space available. */ + flexShrink?: AdaptiveProp; + /** Specifies how the element is aligned inside a flex or grid container. */ + alignSelf?: AdaptiveProp; + /** Specifies how the element is justified inside a flex or grid container. */ + justifySelf?: AdaptiveProp; + /** Specifies how the element is aligned in both direction inside a flex or grid container. */ + placeSelf?: AdaptiveProp; + /** The layout order for the element within a flex or grid container. */ + order?: AdaptiveProp; + + /** When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. */ + gridArea?: AdaptiveProp; + /** When used in a grid layout, specifies the column the element should be placed in within the grid. */ + gridColumn?: AdaptiveProp; + /** When used in a grid layout, specifies the row the element should be placed in within the grid. */ + gridRow?: AdaptiveProp; + /** When used in a grid layout, specifies the starting column to span within the grid. */ + gridColumnStart?: AdaptiveProp; + /** When used in a grid layout, specifies the ending column to span within the grid. */ + gridColumnEnd?: AdaptiveProp; + /** When used in a grid layout, specifies the starting row to span within the grid. */ + gridRowStart?: AdaptiveProp; + /** When used in a grid layout, specifies the ending row to span within the grid. */ + gridRowEnd?: AdaptiveProp; + + /** Species what to do when the element's content is too long to fit its size. */ + overflow?: AdaptiveProp<'hidden' | 'x' | 'y' | 'auto'>; +} + +export interface SizingProps { + /** The width of the element */ + width?: AdaptiveProp; + /** The minimum width of the element */ + minWidth?: AdaptiveProp; + /** The maximum width of the element */ + maxWidth?: AdaptiveProp; + /** The height of the element */ + height?: AdaptiveProp; + /** The minimum height of the element */ + minHeight?: AdaptiveProp; + /** The maximum height of the element */ + maxHeight?: AdaptiveProp; +} + +export type SpacingValue = + | `spacing-${'half' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}` + | (string & {}) + | (number & {}); + +export interface SpacingProps { + /** The margin for all four sides of the element. */ + margin?: AdaptiveProp; + /** The margin for both the left and right sides of the element. */ + marginInline?: AdaptiveProp; + /** The margin for the logical start side of the element. */ + marginStart?: AdaptiveProp; + /** The margin for the logical end side of the element. */ + marginEnd?: AdaptiveProp; + /** The margin for both the top and bottom sides of the element. */ + marginBlock?: AdaptiveProp; + /** The margin for the logical top side of the element. */ + marginTop?: AdaptiveProp; + /** The margin for for the logical bottom side of the element. */ + marginBottom?: AdaptiveProp; + /** The padding for all four sides of the element. */ + padding?: AdaptiveProp; + /** The padding for both the left and right sides of the element. */ + paddingInline?: AdaptiveProp; + /** The padding for the logical start side of the element. */ + paddingStart?: AdaptiveProp; + /** The padding for the logical end side of the element. */ + paddingEnd?: AdaptiveProp; + /** The padding for both the top and bottom sides of the element. */ + paddingBlock?: AdaptiveProp; + /** The padding for the logical top side of the element. */ + paddingTop?: AdaptiveProp; + /** The padding for for the logical bottom side of the element. */ + paddingBottom?: AdaptiveProp; +} + +export interface PositioningProps { + /** Specifies how the element is positioned. */ + position?: AdaptiveProp; + /** The position for all four sides of the element. */ + inset?: AdaptiveProp; + /** The top position for the element. */ + top?: AdaptiveProp; + /** The bottom position for the element. */ + bottom?: AdaptiveProp; + /** The logical start position for the element. */ + start?: AdaptiveProp; + /** The logical end position for the element. */ + end?: AdaptiveProp; + /** The stacking order for the element. */ + zIndex?: AdaptiveProp; +} + +export interface BoxAlignmentStyleProps { + /** The distribution of space around child items along the cross axis. */ + alignContent?: AdaptiveProp; + /** The alignment of children within their container. */ + alignItems?: AdaptiveProp; + /** The distribution of space around items along the main axis. */ + justifyContent?: AdaptiveProp; + /** Defines the default `justifySelf` for all items. */ + justifyItems?: AdaptiveProp; + /** Allows to align content along both the block and inline directions at once. */ + placeContent?: AdaptiveProp; + /** Allows to align items along both the block and inline directions at once */ + placeItems?: AdaptiveProp; + /** The space to display between both rows and columns. */ + gap?: AdaptiveProp; + /** The space to display between columns. */ + columnGap?: AdaptiveProp; + /** The space to display between rows. */ + rowGap?: AdaptiveProp; +} + +export type BackgroundColorValue = + | 'background' + | 'generic' + | 'generic-hover' + | 'generic-medium' + | 'generic-medium-hover' + | 'generic-accent' + | 'generic-accent-disabled' + | 'generic-ultralight' + | 'simple-hover' + | 'simple-hover-solid' + | 'brand' + | 'brand-hover' + | 'selection' + | 'selection-hover' + | 'info-light' + | 'info-light-hover' + | 'info-medium' + | 'info-medium-hover' + | 'info-heavy' + | 'info-heavy-hover' + | 'positive-light' + | 'positive-light-hover' + | 'positive-medium' + | 'positive-medium-hover' + | 'positive-heavy' + | 'positive-heavy-hover' + | 'warning-light' + | 'warning-light-hover' + | 'warning-medium' + | 'warning-medium-hover' + | 'warning-heavy' + | 'warning-heavy-hover' + | 'danger-light' + | 'danger-light-hover' + | 'danger-medium' + | 'danger-medium-hover' + | 'danger-heavy' + | 'danger-heavy-hover' + | 'utility-light' + | 'utility-light-hover' + | 'utility-medium' + | 'utility-medium-hover' + | 'utility-heavy' + | 'utility-heavy-hover' + | 'neutral-light' + | 'neutral-light-hover' + | 'neutral-medium' + | 'neutral-medium-hover' + | 'neutral-heavy' + | 'neutral-heavy-hover' + | 'misc-light' + | 'misc-light-hover' + | 'misc-medium' + | 'misc-medium-hover' + | 'misc-heavy' + | 'misc-heavy-hover' + | 'light' + | 'light-hover' + | 'light-simple-hover' + | 'light-disabled' + | 'light-accent-disabled' + | 'float' + | 'float-hover' + | 'float-medium' + | 'float-heavy' + | 'float-accent' + | 'float-accent-hover' + | 'float-announcement' + | 'modal'; + +export type LineColorValue = + | 'generic' + | 'generic-hover' + | 'generic-active' + | 'generic-accent' + | 'generic-accent-hover' + | 'generic-solid' + | 'brand' + | 'focus' + | 'light' + | 'info' + | 'positive' + | 'warning' + | 'danger' + | 'utility' + | 'misc'; + +export type BorderRadiusValue = 'xs' | 's' | 'm' | 'l' | 'xl' | 'focus'; + +export interface StylingProps { + /** The background color for the element. */ + backgroundColor?: AdaptiveProp; + + borderWidth?: AdaptiveProp; + borderInlineWidth?: AdaptiveProp; + borderBlockWidth?: AdaptiveProp; + borderStartWidth?: AdaptiveProp; + borderEndWidth?: AdaptiveProp; + borderTopWidth?: AdaptiveProp; + borderBottomWidth?: AdaptiveProp; + + borderColor?: AdaptiveProp; + borderInlineColor?: AdaptiveProp; + borderBlockColor?: AdaptiveProp; + borderStartColor?: AdaptiveProp; + borderEndColor?: AdaptiveProp; + borderTopColor?: AdaptiveProp; + borderBottomColor?: AdaptiveProp; + + borderRadius?: AdaptiveProp; + borderTopStartRadius?: AdaptiveProp; + borderTopEndRadius?: AdaptiveProp; + borderBottomStartRadius?: AdaptiveProp; + borderBottomEndRadius?: AdaptiveProp; +} + +export type LayoutComponentProps = P & + Omit, keyof P>; diff --git a/src/components/layout/utils/index.ts b/src/components/layout/utils/index.ts index 4751bcc929..813cbe95e8 100644 --- a/src/components/layout/utils/index.ts +++ b/src/components/layout/utils/index.ts @@ -1,5 +1,13 @@ import {CSS_SIZE_EXCEPTION} from '../constants'; -import type {ColSize, IsMediaActive, MediaPartial, MediaProps, MediaType, Space} from '../types'; +import type { + AdaptiveProp, + ColSize, + IsMediaActive, + MediaPartial, + MediaProps, + MediaType, + Space, +} from '../types'; const mediaByOrder: MediaProps = { xs: 0, @@ -23,22 +31,26 @@ const mediaOrder = ['xs', 's', 'm', 'l', 'xl', 'xxl', 'xxxl'] as const; export const getClosestMediaPropsFactory = (currentActive: MediaType) => - (medias: MediaPartial = {}): T | undefined => { - if (!currentActive) { - return undefined; - } + (prop?: AdaptiveProp | undefined): T | undefined => { + if (prop && typeof prop === 'object' && !Array.isArray(prop)) { + if (!currentActive) { + return undefined; + } - let candidate = currentActive; + const medias = prop as MediaPartial; + let candidate = currentActive; - while (candidate) { - if (typeof medias[candidate] !== 'undefined') { - return medias[candidate]; + while (candidate) { + if (medias[candidate] !== undefined) { + return medias[candidate]; + } + + candidate = mediaOrder[mediaByOrder[candidate] - 1]; } - candidate = mediaOrder[mediaByOrder[candidate] - 1]; + return undefined; } - - return undefined; + return prop; }; export const makeCssMod = (space: Space | ColSize): string => { diff --git a/src/components/useList/components/ListContainerView/ListContainerView.tsx b/src/components/useList/components/ListContainerView/ListContainerView.tsx index b083bafe04..beea8982cb 100644 --- a/src/components/useList/components/ListContainerView/ListContainerView.tsx +++ b/src/components/useList/components/ListContainerView/ListContainerView.tsx @@ -22,7 +22,7 @@ export interface ListContainerViewProps extends QAProps { */ fixedHeight?: boolean; children: React.ReactNode; - extraProps?: React.HTMLAttributes<'div'>; + extraProps?: React.HTMLAttributes; } export const ListContainerView = React.forwardRef( From fc09da5712dbf321f993485c24cf7c6c19e14e8a Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Tue, 18 Nov 2025 17:28:19 +0100 Subject: [PATCH 2/2] fix: use full names --- src/components/layout/hooks/useStyleProps.ts | 50 +++++++++--------- src/components/layout/types.ts | 54 +++++++++++--------- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/components/layout/hooks/useStyleProps.ts b/src/components/layout/hooks/useStyleProps.ts index ebe5f3b548..730974e855 100644 --- a/src/components/layout/hooks/useStyleProps.ts +++ b/src/components/layout/hooks/useStyleProps.ts @@ -49,46 +49,48 @@ const baseStyleHandlers: StyleHandlers = { maxHeight: ['maxHeight', getSpacingValue], position: ['position'], inset: ['inset', getSpacingValue], - top: ['insetBlockStart', getSpacingValue], - bottom: ['insetBlockEnd', getSpacingValue], - start: ['insetInlineStart', getSpacingValue], - end: ['insetInlineEnd', getSpacingValue], + insetBlock: ['insetBlock', getSpacingValue], + insetBlockStart: ['insetBlockStart', getSpacingValue], + insetBlockEnd: ['insetBlockEnd', getSpacingValue], + insetInline: ['insetInline', getSpacingValue], + insetInlineStart: ['insetInlineStart', getSpacingValue], + insetInlineEnd: ['insetInlineEnd', getSpacingValue], zIndex: ['zIndex'], margin: ['margin', getSpacingValue], marginInline: ['marginInline', getSpacingValue], - marginStart: ['marginInlineStart', getSpacingValue], - marginEnd: ['marginInlineEnd', getSpacingValue], + marginInlineStart: ['marginInlineStart', getSpacingValue], + marginInlineEnd: ['marginInlineEnd', getSpacingValue], marginBlock: ['marginBlock', getSpacingValue], - marginTop: ['marginBlockStart', getSpacingValue], - marginBottom: ['marginBlockEnd', getSpacingValue], + marginBlockStart: ['marginBlockStart', getSpacingValue], + marginBlockEnd: ['marginBlockEnd', getSpacingValue], padding: ['padding', getSpacingValue], paddingInline: ['paddingInline', getSpacingValue], - paddingStart: ['paddingInlineStart', getSpacingValue], - paddingEnd: ['paddingInlineEnd', getSpacingValue], + paddingInlineStart: ['paddingInlineStart', getSpacingValue], + paddingInlineEnd: ['paddingInlineEnd', getSpacingValue], paddingBlock: ['paddingBlock', getSpacingValue], - paddingTop: ['paddingBlockStart', getSpacingValue], - paddingBottom: ['paddingBlockEnd', getSpacingValue], + paddingBlockStart: ['paddingBlockStart', getSpacingValue], + paddingBlockEnd: ['paddingBlockEnd', getSpacingValue], backgroundColor: ['backgroundColor', getBackgroundColorValue], borderWidth: ['borderWidth'], borderInlineWidth: ['borderInlineWidth'], borderBlockWidth: ['borderBlockWidth'], - borderStartWidth: ['borderInlineStartWidth'], - borderEndWidth: ['borderInlineEndWidth'], - borderTopWidth: ['borderBlockStartWidth'], - borderBottomWidth: ['borderBlockEndWidth'], + borderInlineStartWidth: ['borderInlineStartWidth'], + borderInlineEndWidth: ['borderInlineEndWidth'], + borderBlockStartWidth: ['borderBlockStartWidth'], + borderBlockEndWidth: ['borderBlockEndWidth'], borderColor: ['borderColor', getLineColorValue], borderInlineColor: ['borderInlineColor', getLineColorValue], borderBlockColor: ['borderBlockColor', getLineColorValue], - borderStartColor: ['borderInlineStartColor', getLineColorValue], - borderEndColor: ['borderInlineEndColor', getLineColorValue], - borderTopColor: ['borderTopColor', getLineColorValue], - borderBottomColor: ['borderBottomColor', getLineColorValue], + borderInlineStartColor: ['borderInlineStartColor', getLineColorValue], + borderInlineEndColor: ['borderInlineEndColor', getLineColorValue], + borderBlockStartColor: ['borderBlockStartColor', getLineColorValue], + borderBlockEndColor: ['borderBlockEndColor', getLineColorValue], borderRadius: ['borderRadius', getRadiusValue], - borderTopStartRadius: ['borderStartStartRadius', getRadiusValue], - borderTopEndRadius: ['borderStartEndRadius', getRadiusValue], - borderBottomStartRadius: ['borderEndStartRadius', getRadiusValue], - borderBottomEndRadius: ['borderEndEndRadius', getRadiusValue], + borderStartStartRadius: ['borderStartStartRadius', getRadiusValue], + borderStartEndRadius: ['borderStartEndRadius', getRadiusValue], + borderEndStartRadius: ['borderEndStartRadius', getRadiusValue], + borderEndEndRadius: ['borderEndEndRadius', getRadiusValue], }; const borderStyleProps = { diff --git a/src/components/layout/types.ts b/src/components/layout/types.ts index f5c3d7f6d4..f03a6184f3 100644 --- a/src/components/layout/types.ts +++ b/src/components/layout/types.ts @@ -172,29 +172,29 @@ export interface SpacingProps { /** The margin for both the left and right sides of the element. */ marginInline?: AdaptiveProp; /** The margin for the logical start side of the element. */ - marginStart?: AdaptiveProp; + marginInlineStart?: AdaptiveProp; /** The margin for the logical end side of the element. */ - marginEnd?: AdaptiveProp; + marginInlineEnd?: AdaptiveProp; /** The margin for both the top and bottom sides of the element. */ marginBlock?: AdaptiveProp; /** The margin for the logical top side of the element. */ - marginTop?: AdaptiveProp; + marginBlockStart?: AdaptiveProp; /** The margin for for the logical bottom side of the element. */ - marginBottom?: AdaptiveProp; + marginBlockEnd?: AdaptiveProp; /** The padding for all four sides of the element. */ padding?: AdaptiveProp; /** The padding for both the left and right sides of the element. */ paddingInline?: AdaptiveProp; /** The padding for the logical start side of the element. */ - paddingStart?: AdaptiveProp; + paddingInlineStart?: AdaptiveProp; /** The padding for the logical end side of the element. */ - paddingEnd?: AdaptiveProp; + paddingInlineEnd?: AdaptiveProp; /** The padding for both the top and bottom sides of the element. */ - paddingBlock?: AdaptiveProp; + paddingBlock?: AdaptiveProp; /** The padding for the logical top side of the element. */ - paddingTop?: AdaptiveProp; + paddingBlockStart?: AdaptiveProp; /** The padding for for the logical bottom side of the element. */ - paddingBottom?: AdaptiveProp; + paddingBlockEnd?: AdaptiveProp; } export interface PositioningProps { @@ -202,14 +202,18 @@ export interface PositioningProps { position?: AdaptiveProp; /** The position for all four sides of the element. */ inset?: AdaptiveProp; + /** The logical vertical position for the element. */ + insetBlock?: AdaptiveProp; /** The top position for the element. */ - top?: AdaptiveProp; + insetBlockStart?: AdaptiveProp; /** The bottom position for the element. */ - bottom?: AdaptiveProp; + insetBlockEnd?: AdaptiveProp; + /** The logical horizontal position for the element. */ + insetInline?: AdaptiveProp; /** The logical start position for the element. */ - start?: AdaptiveProp; + insetInlineStart?: AdaptiveProp; /** The logical end position for the element. */ - end?: AdaptiveProp; + insetInlineEnd?: AdaptiveProp; /** The stacking order for the element. */ zIndex?: AdaptiveProp; } @@ -332,24 +336,24 @@ export interface StylingProps { borderWidth?: AdaptiveProp; borderInlineWidth?: AdaptiveProp; borderBlockWidth?: AdaptiveProp; - borderStartWidth?: AdaptiveProp; - borderEndWidth?: AdaptiveProp; - borderTopWidth?: AdaptiveProp; - borderBottomWidth?: AdaptiveProp; + borderInlineStartWidth?: AdaptiveProp; + borderInlineEndWidth?: AdaptiveProp; + borderBlockStartWidth?: AdaptiveProp; + borderBlockEndWidth?: AdaptiveProp; borderColor?: AdaptiveProp; borderInlineColor?: AdaptiveProp; borderBlockColor?: AdaptiveProp; - borderStartColor?: AdaptiveProp; - borderEndColor?: AdaptiveProp; - borderTopColor?: AdaptiveProp; - borderBottomColor?: AdaptiveProp; + borderInlineStartColor?: AdaptiveProp; + borderInlineEndColor?: AdaptiveProp; + borderBlockStartColor?: AdaptiveProp; + borderBlockEndColor?: AdaptiveProp; borderRadius?: AdaptiveProp; - borderTopStartRadius?: AdaptiveProp; - borderTopEndRadius?: AdaptiveProp; - borderBottomStartRadius?: AdaptiveProp; - borderBottomEndRadius?: AdaptiveProp; + borderStartStartRadius?: AdaptiveProp; + borderStartEndRadius?: AdaptiveProp; + borderEndStartRadius?: AdaptiveProp; + borderEndEndRadius?: AdaptiveProp; } export type LayoutComponentProps = P &