diff --git a/.changeset/afraid-eels-drive.md b/.changeset/afraid-eels-drive.md new file mode 100644 index 000000000..a907798a0 --- /dev/null +++ b/.changeset/afraid-eels-drive.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": patch +--- + +Improve style state application for padding and margin styles making it predictable. diff --git a/src/tasty/styles/margin.test.ts b/src/tasty/styles/margin.test.ts new file mode 100644 index 000000000..d905b5b8f --- /dev/null +++ b/src/tasty/styles/margin.test.ts @@ -0,0 +1,345 @@ +import { marginStyle } from './margin'; + +describe('marginStyle', () => { + describe('basic functionality', () => { + it('returns empty object when no margin properties are provided', () => { + expect(marginStyle({})).toEqual({}); + }); + + it('handles boolean true value', () => { + const result = marginStyle({ margin: true }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': 'var(--gap)', + 'margin-bottom': 'var(--gap)', + 'margin-left': 'var(--gap)', + }); + }); + + it('handles number value', () => { + const result = marginStyle({ margin: 16 }); + expect(result).toEqual({ + 'margin-top': '16px', + 'margin-right': '16px', + 'margin-bottom': '16px', + 'margin-left': '16px', + }); + }); + + it('handles single string value', () => { + const result = marginStyle({ margin: '2x' }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', + 'margin-right': 'calc(2 * var(--gap))', + 'margin-bottom': 'calc(2 * var(--gap))', + 'margin-left': 'calc(2 * var(--gap))', + }); + }); + + it('handles two-value string (vertical horizontal)', () => { + const result = marginStyle({ margin: '2x 3x' }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', + 'margin-right': 'calc(3 * var(--gap))', + 'margin-bottom': 'calc(2 * var(--gap))', + 'margin-left': 'calc(3 * var(--gap))', + }); + }); + + it('handles four-value string (top right bottom left)', () => { + const result = marginStyle({ margin: '1x 2x 3x 4x' }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': 'calc(2 * var(--gap))', + 'margin-bottom': 'calc(3 * var(--gap))', + 'margin-left': 'calc(4 * var(--gap))', + }); + }); + }); + + describe('directional margin', () => { + it('handles directional margin - top only', () => { + const result = marginStyle({ margin: '2x top' }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', + }); + }); + + it('handles directional margin - left and right', () => { + const result = marginStyle({ margin: '3x left right' }); + expect(result).toEqual({ + 'margin-left': 'calc(3 * var(--gap))', + 'margin-right': 'calc(3 * var(--gap))', + }); + }); + + it('handles directional margin - bottom only', () => { + const result = marginStyle({ margin: '1x bottom' }); + expect(result).toEqual({ + 'margin-bottom': 'var(--gap)', + }); + }); + }); + + describe('marginBlock and marginInline', () => { + it('handles marginBlock (top and bottom)', () => { + const result = marginStyle({ marginBlock: '2x' }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', + 'margin-bottom': 'calc(2 * var(--gap))', + }); + }); + + it('handles marginBlock with two values', () => { + const result = marginStyle({ marginBlock: '1x 3x' }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-bottom': 'calc(3 * var(--gap))', + }); + }); + + it('handles marginInline (left and right)', () => { + const result = marginStyle({ marginInline: '4x' }); + expect(result).toEqual({ + 'margin-left': 'calc(4 * var(--gap))', + 'margin-right': 'calc(4 * var(--gap))', + }); + }); + + it('handles marginInline with two values', () => { + const result = marginStyle({ marginInline: '2x 5x' }); + expect(result).toEqual({ + 'margin-left': 'calc(2 * var(--gap))', + 'margin-right': 'calc(5 * var(--gap))', + }); + }); + + it('handles boolean and number values for logical properties', () => { + const result = marginStyle({ + marginBlock: true, + marginInline: 8, + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-bottom': 'var(--gap)', + 'margin-left': '8px', + 'margin-right': '8px', + }); + }); + }); + + describe('individual direction properties', () => { + it('handles individual direction properties', () => { + const result = marginStyle({ + marginTop: '1x', + marginRight: '2x', + marginBottom: '3x', + marginLeft: '4x', + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': 'calc(2 * var(--gap))', + 'margin-bottom': 'calc(3 * var(--gap))', + 'margin-left': 'calc(4 * var(--gap))', + }); + }); + + it('handles boolean and number values for individual directions', () => { + const result = marginStyle({ + marginTop: true, + marginRight: 12, + marginBottom: '2x', + marginLeft: false, + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': '12px', + 'margin-bottom': 'calc(2 * var(--gap))', + }); + }); + }); + + describe('priority system', () => { + it('margin (low) < marginBlock/marginInline (medium)', () => { + const result = marginStyle({ + margin: '1x', + marginBlock: '2x', + marginInline: '3x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', // overridden by marginBlock + 'margin-right': 'calc(3 * var(--gap))', // overridden by marginInline + 'margin-bottom': 'calc(2 * var(--gap))', // overridden by marginBlock + 'margin-left': 'calc(3 * var(--gap))', // overridden by marginInline + }); + }); + + it('marginBlock/marginInline (medium) < individual directions (high)', () => { + const result = marginStyle({ + marginBlock: '2x', + marginInline: '3x', + marginTop: '4x', + marginRight: '5x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(4 * var(--gap))', // overridden by marginTop + 'margin-right': 'calc(5 * var(--gap))', // overridden by marginRight + 'margin-bottom': 'calc(2 * var(--gap))', // from marginBlock + 'margin-left': 'calc(3 * var(--gap))', // from marginInline + }); + }); + + it('complete priority chain: margin < marginBlock/Inline < individual', () => { + const result = marginStyle({ + margin: '1x', + marginBlock: '2x', + marginInline: '3x', + marginTop: '4x', + marginRight: '5x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(4 * var(--gap))', // highest: individual direction + 'margin-right': 'calc(5 * var(--gap))', // highest: individual direction + 'margin-bottom': 'calc(2 * var(--gap))', // medium: marginBlock + 'margin-left': 'calc(3 * var(--gap))', // medium: marginInline + }); + }); + + it('example: margin="1x" marginRight="2x"', () => { + const result = marginStyle({ + margin: '1x', + marginRight: '2x', + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': 'calc(2 * var(--gap))', // overridden by marginRight + 'margin-bottom': 'var(--gap)', + 'margin-left': 'var(--gap)', + }); + }); + + it('example: margin="1x" marginBlock="2x"', () => { + const result = marginStyle({ + margin: '1x', + marginBlock: '2x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', // overridden by marginBlock + 'margin-right': 'var(--gap)', + 'margin-bottom': 'calc(2 * var(--gap))', // overridden by marginBlock + 'margin-left': 'var(--gap)', + }); + }); + }); + + describe('edge cases', () => { + it('handles null and undefined values', () => { + const result = marginStyle({ + margin: undefined, + marginBlock: undefined, + marginTop: undefined, + }); + expect(result).toEqual({}); + }); + + it('handles empty string values', () => { + const result = marginStyle({ + margin: '', + marginBlock: '', + marginTop: '2x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(2 * var(--gap))', + }); + }); + + it('handles zero values', () => { + const result = marginStyle({ + margin: 0, + marginTop: '1x', + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', // overridden by marginTop + 'margin-right': '0px', + 'margin-bottom': '0px', + 'margin-left': '0px', + }); + }); + + it('handles mixed types', () => { + const result = marginStyle({ + margin: true, + marginBlock: 16, + marginLeft: '3x', + }); + expect(result).toEqual({ + 'margin-top': '16px', // overridden by marginBlock + 'margin-right': 'var(--gap)', // from margin + 'margin-bottom': '16px', // overridden by marginBlock + 'margin-left': 'calc(3 * var(--gap))', // overridden by marginLeft + }); + }); + + it('handles negative values', () => { + const result = marginStyle({ + margin: '-1x', + marginTop: -8, + }); + expect(result).toEqual({ + 'margin-top': '-8px', // overridden by marginTop + 'margin-right': 'calc(-1 * var(--gap))', + 'margin-bottom': 'calc(-1 * var(--gap))', + 'margin-left': 'calc(-1 * var(--gap))', + }); + }); + }); + + describe('directional margin with priority', () => { + it('respects individual directions over directional margin', () => { + const result = marginStyle({ + margin: '2x top bottom', + marginTop: '5x', + }); + expect(result).toEqual({ + 'margin-top': 'calc(5 * var(--gap))', // overridden by marginTop + 'margin-bottom': 'calc(2 * var(--gap))', // from directional margin + }); + }); + + it('combines directional margin with logical properties', () => { + const result = marginStyle({ + margin: '1x top', + marginInline: '3x', + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', // from directional margin + 'margin-left': 'calc(3 * var(--gap))', // from marginInline + 'margin-right': 'calc(3 * var(--gap))', // from marginInline + }); + }); + }); + + describe('auto values', () => { + it('handles auto values for centering', () => { + const result = marginStyle({ + marginInline: 'auto', + }); + expect(result).toEqual({ + 'margin-left': 'auto', + 'margin-right': 'auto', + }); + }); + + it('handles mixed auto and specific values', () => { + const result = marginStyle({ + margin: '1x auto', + }); + expect(result).toEqual({ + 'margin-top': 'var(--gap)', + 'margin-right': 'auto', + 'margin-bottom': 'var(--gap)', + 'margin-left': 'auto', + }); + }); + }); +}); diff --git a/src/tasty/styles/margin.ts b/src/tasty/styles/margin.ts index 979b5e993..a0f65d43c 100644 --- a/src/tasty/styles/margin.ts +++ b/src/tasty/styles/margin.ts @@ -1,5 +1,55 @@ import { DIRECTIONS, filterMods, parseStyle } from '../utils/styles'; +/** + * Parse a margin value and return processed values + */ +function parseMarginValue(value: string | number | boolean): string[] { + if (typeof value === 'number') { + return [`${value}px`]; + } + + if (!value) return []; + + if (value === true) value = '1x'; + + const processed = parseStyle(value); + let { values } = processed.groups[0] ?? ({ values: [] } as any); + + if (!values.length) { + values = ['var(--gap)']; + } + + return values; +} + +/** + * Parse directional margin value (like "1x top" or "2x left right") + */ +function parseDirectionalMargin(value: string | number | boolean): { + values: string[]; + directions: string[]; +} { + if (typeof value === 'number') { + return { values: [`${value}px`], directions: [] }; + } + + if (!value) return { values: [], directions: [] }; + + if (value === true) value = '1x'; + + const processed = parseStyle(value); + let { values, mods } = + processed.groups[0] ?? ({ values: [], mods: [] } as any); + + if (!values.length) { + values = ['var(--gap)']; + } + + const directions = filterMods(mods, DIRECTIONS); + + return { values, directions }; +} + export function marginStyle({ margin, marginBlock, @@ -10,50 +60,72 @@ export function marginStyle({ marginLeft, }: { margin?: string | number | boolean; - marginBlock?: string; - marginInline?: string; - marginTop?: string; - marginRight?: string; - marginBottom?: string; - marginLeft?: string; + marginBlock?: string | number | boolean; + marginInline?: string | number | boolean; + marginTop?: string | number | boolean; + marginRight?: string | number | boolean; + marginBottom?: string | number | boolean; + marginLeft?: string | number | boolean; }) { - if (typeof margin === 'number') { - margin = `${margin}px`; - } - - if (!margin) return ''; + const styles: { [key: string]: string } = {}; - if (margin === true) margin = '1x'; + // Priority 1 (lowest): margin - apply to all directions if no other properties are specified + if (margin != null) { + // Parse directional margin (e.g., "1x top" or "2x left right") + const { values, directions } = parseDirectionalMargin(margin); - const processed = parseStyle(margin); - let { values, mods } = - processed.groups[0] ?? ({ values: [], mods: [] } as any); + if (directions.length === 0) { + // No directions specified, apply to all sides + styles['margin-top'] = values[0]; + styles['margin-right'] = values[1] || values[0]; + styles['margin-bottom'] = values[2] || values[0]; + styles['margin-left'] = values[3] || values[1] || values[0]; + } else { + // Apply only to specified directions + directions.forEach((dir) => { + const index = DIRECTIONS.indexOf(dir); + styles[`margin-${dir}`] = + values[index] || values[index % 2] || values[0]; + }); + } + } - let directions = filterMods(mods, DIRECTIONS); + // Priority 2 (medium): marginBlock - override top and bottom + if (marginBlock != null) { + const values = parseMarginValue(marginBlock); + styles['margin-top'] = values[0]; + styles['margin-bottom'] = values[1] || values[0]; + } - if (!values.length) { - values = ['var(--gap)']; + // Priority 2 (medium): marginInline - override left and right + if (marginInline != null) { + const values = parseMarginValue(marginInline); + styles['margin-left'] = values[0]; + styles['margin-right'] = values[1] || values[0]; } - if (!directions.length) { - directions = DIRECTIONS; + // Priority 3 (highest): individual directions - override specific sides + if (marginTop != null) { + const values = parseMarginValue(marginTop); + styles['margin-top'] = values[0]; } - const marginDirs = [marginTop, marginRight, marginBottom, marginLeft]; + if (marginRight != null) { + const values = parseMarginValue(marginRight); + styles['margin-right'] = values[0]; + } - return directions.reduce((styles, dir) => { - const index = DIRECTIONS.indexOf(dir); + if (marginBottom != null) { + const values = parseMarginValue(marginBottom); + styles['margin-bottom'] = values[0]; + } - if ( - ((!!(index % 2) && marginInline == null) || - (!(index % 2) && marginBlock == null)) && - marginDirs[index] == null - ) { - styles[`margin-${dir}`] = values[index] || values[index % 2] || values[0]; - } + if (marginLeft != null) { + const values = parseMarginValue(marginLeft); + styles['margin-left'] = values[0]; + } - return styles; - }, {}); + return styles; } marginStyle.__lookupStyles = [ diff --git a/src/tasty/styles/marginBlock.ts b/src/tasty/styles/marginBlock.ts deleted file mode 100644 index baf1ecb14..000000000 --- a/src/tasty/styles/marginBlock.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { parseStyle } from '../utils/styles'; - -export function marginBlockStyle({ - marginBlock: margin, - marginTop, - marginBottom, -}) { - if (typeof margin === 'number') { - margin = `${margin}px`; - } - - if (!margin) return ''; - - if (margin === true) margin = '1x'; - - const processed = parseStyle(margin); - let { values } = processed.groups[0] ?? ({ values: [] } as any); - - if (!values.length) { - values = ['var(--gap)']; - } - - const styles = {}; - - if (marginTop == null) { - styles['margin-top'] = values[0]; - } - - if (marginBottom == null) { - styles['margin-bottom'] = values[1] || values[0]; - } - - return styles; -} - -marginBlockStyle.__lookupStyles = ['marginBlock', 'marginTop', 'marginBottom']; diff --git a/src/tasty/styles/marginInline.ts b/src/tasty/styles/marginInline.ts deleted file mode 100644 index 6c27bec99..000000000 --- a/src/tasty/styles/marginInline.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { parseStyle } from '../utils/styles'; - -export function marginInlineStyle({ - marginInline: margin, - marginLeft, - marginRight, -}) { - if (typeof margin === 'number') { - margin = `${margin}px`; - } - - if (!margin) return ''; - - if (margin === true) margin = '1x'; - - const processed = parseStyle(margin); - let { values } = processed.groups[0] ?? ({ values: [] } as any); - - if (!values.length) { - values = ['var(--gap)']; - } - - const styles = {}; - - if (marginLeft == null) { - styles['margin-left'] = values[0]; - } - - if (marginRight == null) { - styles['margin-right'] = values[1] || values[0]; - } - - return styles; -} - -marginInlineStyle.__lookupStyles = [ - 'marginInline', - 'marginLeft', - 'marginRight', -]; diff --git a/src/tasty/styles/padding.test.ts b/src/tasty/styles/padding.test.ts new file mode 100644 index 000000000..3bf63f6c4 --- /dev/null +++ b/src/tasty/styles/padding.test.ts @@ -0,0 +1,308 @@ +import { paddingStyle } from './padding'; + +describe('paddingStyle', () => { + describe('basic functionality', () => { + it('returns empty object when no padding properties are provided', () => { + expect(paddingStyle({})).toEqual({}); + }); + + it('handles boolean true value', () => { + const result = paddingStyle({ padding: true }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-right': 'var(--gap)', + 'padding-bottom': 'var(--gap)', + 'padding-left': 'var(--gap)', + }); + }); + + it('handles number value', () => { + const result = paddingStyle({ padding: 16 }); + expect(result).toEqual({ + 'padding-top': '16px', + 'padding-right': '16px', + 'padding-bottom': '16px', + 'padding-left': '16px', + }); + }); + + it('handles single string value', () => { + const result = paddingStyle({ padding: '2x' }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', + 'padding-right': 'calc(2 * var(--gap))', + 'padding-bottom': 'calc(2 * var(--gap))', + 'padding-left': 'calc(2 * var(--gap))', + }); + }); + + it('handles two-value string (vertical horizontal)', () => { + const result = paddingStyle({ padding: '2x 3x' }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', + 'padding-right': 'calc(3 * var(--gap))', + 'padding-bottom': 'calc(2 * var(--gap))', + 'padding-left': 'calc(3 * var(--gap))', + }); + }); + + it('handles four-value string (top right bottom left)', () => { + const result = paddingStyle({ padding: '1x 2x 3x 4x' }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-right': 'calc(2 * var(--gap))', + 'padding-bottom': 'calc(3 * var(--gap))', + 'padding-left': 'calc(4 * var(--gap))', + }); + }); + }); + + describe('directional padding', () => { + it('handles directional padding - top only', () => { + const result = paddingStyle({ padding: '2x top' }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', + }); + }); + + it('handles directional padding - left and right', () => { + const result = paddingStyle({ padding: '3x left right' }); + expect(result).toEqual({ + 'padding-left': 'calc(3 * var(--gap))', + 'padding-right': 'calc(3 * var(--gap))', + }); + }); + + it('handles directional padding - bottom only', () => { + const result = paddingStyle({ padding: '1x bottom' }); + expect(result).toEqual({ + 'padding-bottom': 'var(--gap)', + }); + }); + }); + + describe('paddingBlock and paddingInline', () => { + it('handles paddingBlock (top and bottom)', () => { + const result = paddingStyle({ paddingBlock: '2x' }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', + 'padding-bottom': 'calc(2 * var(--gap))', + }); + }); + + it('handles paddingBlock with two values', () => { + const result = paddingStyle({ paddingBlock: '1x 3x' }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-bottom': 'calc(3 * var(--gap))', + }); + }); + + it('handles paddingInline (left and right)', () => { + const result = paddingStyle({ paddingInline: '4x' }); + expect(result).toEqual({ + 'padding-left': 'calc(4 * var(--gap))', + 'padding-right': 'calc(4 * var(--gap))', + }); + }); + + it('handles paddingInline with two values', () => { + const result = paddingStyle({ paddingInline: '2x 5x' }); + expect(result).toEqual({ + 'padding-left': 'calc(2 * var(--gap))', + 'padding-right': 'calc(5 * var(--gap))', + }); + }); + + it('handles boolean and number values for logical properties', () => { + const result = paddingStyle({ + paddingBlock: true, + paddingInline: 8, + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-bottom': 'var(--gap)', + 'padding-left': '8px', + 'padding-right': '8px', + }); + }); + }); + + describe('individual direction properties', () => { + it('handles individual direction properties', () => { + const result = paddingStyle({ + paddingTop: '1x', + paddingRight: '2x', + paddingBottom: '3x', + paddingLeft: '4x', + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-right': 'calc(2 * var(--gap))', + 'padding-bottom': 'calc(3 * var(--gap))', + 'padding-left': 'calc(4 * var(--gap))', + }); + }); + + it('handles boolean and number values for individual directions', () => { + const result = paddingStyle({ + paddingTop: true, + paddingRight: 12, + paddingBottom: '2x', + paddingLeft: false, + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-right': '12px', + 'padding-bottom': 'calc(2 * var(--gap))', + }); + }); + }); + + describe('priority system', () => { + it('padding (low) < paddingBlock/paddingInline (medium)', () => { + const result = paddingStyle({ + padding: '1x', + paddingBlock: '2x', + paddingInline: '3x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', // overridden by paddingBlock + 'padding-right': 'calc(3 * var(--gap))', // overridden by paddingInline + 'padding-bottom': 'calc(2 * var(--gap))', // overridden by paddingBlock + 'padding-left': 'calc(3 * var(--gap))', // overridden by paddingInline + }); + }); + + it('paddingBlock/paddingInline (medium) < individual directions (high)', () => { + const result = paddingStyle({ + paddingBlock: '2x', + paddingInline: '3x', + paddingTop: '4x', + paddingRight: '5x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(4 * var(--gap))', // overridden by paddingTop + 'padding-right': 'calc(5 * var(--gap))', // overridden by paddingRight + 'padding-bottom': 'calc(2 * var(--gap))', // from paddingBlock + 'padding-left': 'calc(3 * var(--gap))', // from paddingInline + }); + }); + + it('complete priority chain: padding < paddingBlock/Inline < individual', () => { + const result = paddingStyle({ + padding: '1x', + paddingBlock: '2x', + paddingInline: '3x', + paddingTop: '4x', + paddingRight: '5x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(4 * var(--gap))', // highest: individual direction + 'padding-right': 'calc(5 * var(--gap))', // highest: individual direction + 'padding-bottom': 'calc(2 * var(--gap))', // medium: paddingBlock + 'padding-left': 'calc(3 * var(--gap))', // medium: paddingInline + }); + }); + + it('example from requirements: padding="1x" paddingRight="2x"', () => { + const result = paddingStyle({ + padding: '1x', + paddingRight: '2x', + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', + 'padding-right': 'calc(2 * var(--gap))', // overridden by paddingRight + 'padding-bottom': 'var(--gap)', + 'padding-left': 'var(--gap)', + }); + }); + + it('example from requirements: padding="1x" paddingBlock="2x"', () => { + const result = paddingStyle({ + padding: '1x', + paddingBlock: '2x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', // overridden by paddingBlock + 'padding-right': 'var(--gap)', + 'padding-bottom': 'calc(2 * var(--gap))', // overridden by paddingBlock + 'padding-left': 'var(--gap)', + }); + }); + }); + + describe('edge cases', () => { + it('handles null and undefined values', () => { + const result = paddingStyle({ + padding: undefined, + paddingBlock: undefined, + paddingTop: undefined, + }); + expect(result).toEqual({}); + }); + + it('handles empty string values', () => { + const result = paddingStyle({ + padding: '', + paddingBlock: '', + paddingTop: '2x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(2 * var(--gap))', + }); + }); + + it('handles zero values', () => { + const result = paddingStyle({ + padding: 0, + paddingTop: '1x', + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', // overridden by paddingTop + 'padding-right': '0px', + 'padding-bottom': '0px', + 'padding-left': '0px', + }); + }); + + it('handles mixed types', () => { + const result = paddingStyle({ + padding: true, + paddingBlock: 16, + paddingLeft: '3x', + }); + expect(result).toEqual({ + 'padding-top': '16px', // overridden by paddingBlock + 'padding-right': 'var(--gap)', // from padding + 'padding-bottom': '16px', // overridden by paddingBlock + 'padding-left': 'calc(3 * var(--gap))', // overridden by paddingLeft + }); + }); + }); + + describe('directional padding with priority', () => { + it('respects individual directions over directional padding', () => { + const result = paddingStyle({ + padding: '2x top bottom', + paddingTop: '5x', + }); + expect(result).toEqual({ + 'padding-top': 'calc(5 * var(--gap))', // overridden by paddingTop + 'padding-bottom': 'calc(2 * var(--gap))', // from directional padding + }); + }); + + it('combines directional padding with logical properties', () => { + const result = paddingStyle({ + padding: '1x top', + paddingInline: '3x', + }); + expect(result).toEqual({ + 'padding-top': 'var(--gap)', // from directional padding + 'padding-left': 'calc(3 * var(--gap))', // from paddingInline + 'padding-right': 'calc(3 * var(--gap))', // from paddingInline + }); + }); + }); +}); diff --git a/src/tasty/styles/padding.ts b/src/tasty/styles/padding.ts index 36e77d9ae..919db67da 100644 --- a/src/tasty/styles/padding.ts +++ b/src/tasty/styles/padding.ts @@ -1,5 +1,55 @@ import { DIRECTIONS, filterMods, parseStyle } from '../utils/styles'; +/** + * Parse a padding value and return processed values + */ +function parsePaddingValue(value: string | number | boolean): string[] { + if (typeof value === 'number') { + return [`${value}px`]; + } + + if (!value) return []; + + if (value === true) value = '1x'; + + const processed = parseStyle(value); + let { values } = processed.groups[0] ?? ({ values: [] } as any); + + if (!values.length) { + values = ['var(--gap)']; + } + + return values; +} + +/** + * Parse directional padding value (like "1x top" or "2x left right") + */ +function parseDirectionalPadding(value: string | number | boolean): { + values: string[]; + directions: string[]; +} { + if (typeof value === 'number') { + return { values: [`${value}px`], directions: [] }; + } + + if (!value) return { values: [], directions: [] }; + + if (value === true) value = '1x'; + + const processed = parseStyle(value); + let { values, mods } = + processed.groups[0] ?? ({ values: [], mods: [] } as any); + + if (!values.length) { + values = ['var(--gap)']; + } + + const directions = filterMods(mods, DIRECTIONS); + + return { values, directions }; +} + export function paddingStyle({ padding, paddingBlock, @@ -9,61 +59,73 @@ export function paddingStyle({ paddingBottom, paddingLeft, }: { - padding?: string | number | boolean | string[]; - paddingBlock?: string; - paddingInline?: string; - paddingTop?: string; - paddingRight?: string; - paddingBottom?: string; - paddingLeft?: string; + padding?: string | number | boolean; + paddingBlock?: string | number | boolean; + paddingInline?: string | number | boolean; + paddingTop?: string | number | boolean; + paddingRight?: string | number | boolean; + paddingBottom?: string | number | boolean; + paddingLeft?: string | number | boolean; }) { - if (Array.isArray(padding)) { - return { - 'padding-top': padding[0], - 'padding-right': padding[1] || padding[0], - 'padding-bottom': padding[2] || padding[0], - 'padding-left': padding[3] || padding[1] || padding[0], - }; - } - - if (typeof padding === 'number') { - padding = `${padding}px`; - } + const styles: { [key: string]: string } = {}; - if (!padding) return ''; + // Priority 1 (lowest): padding - apply to all directions if no other properties are specified + if (padding != null) { + // Parse directional padding (e.g., "1x top" or "2x left right") + const { values, directions } = parseDirectionalPadding(padding); - if (padding === true) padding = '1x'; - - const processed = parseStyle(padding); - let { values, mods } = - processed.groups[0] ?? ({ values: [], mods: [] } as any); + if (directions.length === 0) { + // No directions specified, apply to all sides + styles['padding-top'] = values[0]; + styles['padding-right'] = values[1] || values[0]; + styles['padding-bottom'] = values[2] || values[0]; + styles['padding-left'] = values[3] || values[1] || values[0]; + } else { + // Apply only to specified directions + directions.forEach((dir) => { + const index = DIRECTIONS.indexOf(dir); + styles[`padding-${dir}`] = + values[index] || values[index % 2] || values[0]; + }); + } + } - let directions = filterMods(mods, DIRECTIONS); + // Priority 2 (medium): paddingBlock - override top and bottom + if (paddingBlock != null) { + const values = parsePaddingValue(paddingBlock); + styles['padding-top'] = values[0]; + styles['padding-bottom'] = values[1] || values[0]; + } - if (!values.length) { - values = ['var(--gap)']; + // Priority 2 (medium): paddingInline - override left and right + if (paddingInline != null) { + const values = parsePaddingValue(paddingInline); + styles['padding-left'] = values[0]; + styles['padding-right'] = values[1] || values[0]; } - if (!directions.length) { - directions = DIRECTIONS; + // Priority 3 (highest): individual directions - override specific sides + if (paddingTop != null) { + const values = parsePaddingValue(paddingTop); + styles['padding-top'] = values[0]; } - const paddingDirs = [paddingTop, paddingRight, paddingBottom, paddingLeft]; + if (paddingRight != null) { + const values = parsePaddingValue(paddingRight); + styles['padding-right'] = values[0]; + } - return directions.reduce((styles, dir) => { - const index = DIRECTIONS.indexOf(dir); + if (paddingBottom != null) { + const values = parsePaddingValue(paddingBottom); + styles['padding-bottom'] = values[0]; + } - if ( - ((!!(index % 2) && paddingInline == null) || - (!(index % 2) && paddingBlock == null)) && - paddingDirs[index] == null - ) { - styles[`padding-${dir}`] = - values[index] || values[index % 2] || values[0]; - } + if (paddingLeft != null) { + const values = parsePaddingValue(paddingLeft); + styles['padding-left'] = values[0]; + } - return styles; - }, {}); + return styles; } paddingStyle.__lookupStyles = [ diff --git a/src/tasty/styles/paddingBlock.ts b/src/tasty/styles/paddingBlock.ts deleted file mode 100644 index 49a1fbbad..000000000 --- a/src/tasty/styles/paddingBlock.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { parseStyle } from '../utils/styles'; - -export function paddingBlockStyle({ - paddingBlock: padding, - paddingTop, - paddingBottom, -}) { - if (typeof padding === 'number') { - padding = `${padding}px`; - } - - if (!padding) return ''; - - if (padding === true) padding = '1x'; - - const processed = parseStyle(padding); - let { values } = processed.groups[0] ?? ({ values: [] } as any); - - if (!values.length) { - values = ['var(--gap)']; - } - - const styles = {}; - - if (paddingTop == null) { - styles['padding-top'] = values[0]; - } - - if (paddingBottom == null) { - styles['padding-bottom'] = values[1] || values[0]; - } - - return styles; -} - -paddingBlockStyle.__lookupStyles = [ - 'paddingBlock', - 'paddingTop', - 'paddingBottom', -]; diff --git a/src/tasty/styles/paddingInline.ts b/src/tasty/styles/paddingInline.ts deleted file mode 100644 index a825b5285..000000000 --- a/src/tasty/styles/paddingInline.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { parseStyle } from '../utils/styles'; - -export function paddingInlineStyle({ - paddingInline: padding, - paddingLeft, - paddingRight, -}) { - if (typeof padding === 'number') { - padding = `${padding}px`; - } - - if (!padding) return ''; - - if (padding === true) padding = '1x'; - - const processed = parseStyle(padding); - let { values } = processed.groups[0] ?? ({ values: [] } as any); - - if (!values.length) { - values = ['var(--gap)']; - } - - const styles = {}; - - if (paddingLeft == null) { - styles['padding-left'] = values[0]; - } - - if (paddingRight == null) { - styles['padding-right'] = values[1] || values[0]; - } - - return styles; -} - -paddingInlineStyle.__lookupStyles = [ - 'paddingInline', - 'paddingLeft', - 'paddingRight', -]; diff --git a/src/tasty/styles/predefined.ts b/src/tasty/styles/predefined.ts index 6df1f3300..3d5397427 100644 --- a/src/tasty/styles/predefined.ts +++ b/src/tasty/styles/predefined.ts @@ -16,12 +16,8 @@ import { heightStyle } from './height'; import { insetStyle } from './inset'; import { justifyStyle } from './justify'; import { marginStyle } from './margin'; -import { marginBlockStyle } from './marginBlock'; -import { marginInlineStyle } from './marginInline'; import { outlineStyle } from './outline'; import { paddingStyle } from './padding'; -import { paddingBlockStyle } from './paddingBlock'; -import { paddingInlineStyle } from './paddingInline'; import { presetStyle } from './preset'; import { radiusStyle } from './radius'; import { resetStyle } from './reset'; @@ -154,8 +150,6 @@ export function predefine() { fillStyle, widthStyle, marginStyle, - marginBlockStyle, - marginInlineStyle, gapStyle, flowStyle, colorStyle, @@ -164,8 +158,6 @@ export function predefine() { borderStyle, shadowStyle, paddingStyle, - paddingBlockStyle, - paddingInlineStyle, alignStyle, justifyStyle, presetStyle,