diff --git a/.gitignore b/.gitignore index 7c3ead5c..fd43ef01 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage node_modules/ .DS_Store *.local +*.local.md .idea diff --git a/AGENTS.md b/AGENTS.md index dc724aa0..06658204 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This repository is `tailwind-merge`, a TypeScript library that merges Tailwind c ## Primary Goals -- Keep merge behavior correct for supported Tailwind versions (currently v4.0-v4.1). +- Keep merge behavior correct for supported Tailwind versions (currently v4.0-v4.2). - Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache). - Keep API and type contracts stable. diff --git a/docs/README.md b/docs/README.md index 2c0a284b..27b5cb4f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') // → 'hover:bg-dark-red p-3 bg-[#B91C1C]' ``` -- Supports Tailwind v4.0 up to v4.1 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0)) +- Supports Tailwind v4.0 up to v4.2 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0)) - Works in all modern browsers and maintained Node versions - Fully typed - [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge) diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index 19c97632..ced00268 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -137,6 +137,33 @@ export const getDefaultConfig = () => { 'fit', ...scaleUnambiguousSpacing(), ] as const + const scaleSizingInline = () => + [ + isFraction, + 'screen', + 'full', + 'dvw', + 'lvw', + 'svw', + 'min', + 'max', + 'fit', + ...scaleUnambiguousSpacing(), + ] as const + const scaleSizingBlock = () => + [ + isFraction, + 'screen', + 'full', + 'lh', + 'dvh', + 'lvh', + 'svh', + 'min', + 'max', + 'fit', + ...scaleUnambiguousSpacing(), + ] as const const scaleColor = () => [themeColor, isArbitraryVariable, isArbitraryValue] as const const scaleBgPosition = () => [ @@ -390,30 +417,60 @@ export const getDefaultConfig = () => { */ position: ['static', 'fixed', 'absolute', 'relative', 'sticky'], /** - * Top / Right / Bottom / Left + * Inset * @see https://tailwindcss.com/docs/top-right-bottom-left */ inset: [{ inset: scaleInset() }], /** - * Right / Left + * Inset Inline * @see https://tailwindcss.com/docs/top-right-bottom-left */ 'inset-x': [{ 'inset-x': scaleInset() }], /** - * Top / Bottom + * Inset Block * @see https://tailwindcss.com/docs/top-right-bottom-left */ 'inset-y': [{ 'inset-y': scaleInset() }], /** - * Start + * Inset Inline Start * @see https://tailwindcss.com/docs/top-right-bottom-left + * @todo class group will be renamed to `inset-s` in next major release */ - start: [{ start: scaleInset() }], + start: [ + { + 'inset-s': scaleInset(), + /** + * @deprecated since Tailwind CSS v4.2.0 in favor of `inset-s-*` utilities. + * @see https://github.com/tailwindlabs/tailwindcss/pull/19613 + */ + start: scaleInset(), + }, + ], + /** + * Inset Inline End + * @see https://tailwindcss.com/docs/top-right-bottom-left + * @todo class group will be renamed to `inset-e` in next major release + */ + end: [ + { + 'inset-e': scaleInset(), + /** + * @deprecated since Tailwind CSS v4.2.0 in favor of `inset-e-*` utilities. + * @see https://github.com/tailwindlabs/tailwindcss/pull/19613 + */ + end: scaleInset(), + }, + ], + /** + * Inset Block Start + * @see https://tailwindcss.com/docs/top-right-bottom-left + */ + 'inset-bs': [{ 'inset-bs': scaleInset() }], /** - * End + * Inset Block End * @see https://tailwindcss.com/docs/top-right-bottom-left */ - end: [{ end: scaleInset() }], + 'inset-be': [{ 'inset-be': scaleInset() }], /** * Top * @see https://tailwindcss.com/docs/top-right-bottom-left @@ -629,25 +686,35 @@ export const getDefaultConfig = () => { */ p: [{ p: scaleUnambiguousSpacing() }], /** - * Padding X + * Padding Inline * @see https://tailwindcss.com/docs/padding */ px: [{ px: scaleUnambiguousSpacing() }], /** - * Padding Y + * Padding Block * @see https://tailwindcss.com/docs/padding */ py: [{ py: scaleUnambiguousSpacing() }], /** - * Padding Start + * Padding Inline Start * @see https://tailwindcss.com/docs/padding */ ps: [{ ps: scaleUnambiguousSpacing() }], /** - * Padding End + * Padding Inline End * @see https://tailwindcss.com/docs/padding */ pe: [{ pe: scaleUnambiguousSpacing() }], + /** + * Padding Block Start + * @see https://tailwindcss.com/docs/padding + */ + pbs: [{ pbs: scaleUnambiguousSpacing() }], + /** + * Padding Block End + * @see https://tailwindcss.com/docs/padding + */ + pbe: [{ pbe: scaleUnambiguousSpacing() }], /** * Padding Top * @see https://tailwindcss.com/docs/padding @@ -674,25 +741,35 @@ export const getDefaultConfig = () => { */ m: [{ m: scaleMargin() }], /** - * Margin X + * Margin Inline * @see https://tailwindcss.com/docs/margin */ mx: [{ mx: scaleMargin() }], /** - * Margin Y + * Margin Block * @see https://tailwindcss.com/docs/margin */ my: [{ my: scaleMargin() }], /** - * Margin Start + * Margin Inline Start * @see https://tailwindcss.com/docs/margin */ ms: [{ ms: scaleMargin() }], /** - * Margin End + * Margin Inline End * @see https://tailwindcss.com/docs/margin */ me: [{ me: scaleMargin() }], + /** + * Margin Block Start + * @see https://tailwindcss.com/docs/margin + */ + mbs: [{ mbs: scaleMargin() }], + /** + * Margin Block End + * @see https://tailwindcss.com/docs/margin + */ + mbe: [{ mbe: scaleMargin() }], /** * Margin Top * @see https://tailwindcss.com/docs/margin @@ -743,6 +820,36 @@ export const getDefaultConfig = () => { * @see https://tailwindcss.com/docs/width#setting-both-width-and-height */ size: [{ size: scaleSizing() }], + /** + * Inline Size + * @see https://tailwindcss.com/docs/width + */ + 'inline-size': [{ inline: ['auto', ...scaleSizingInline()] }], + /** + * Min-Inline Size + * @see https://tailwindcss.com/docs/min-width + */ + 'min-inline-size': [{ 'min-inline': ['auto', ...scaleSizingInline()] }], + /** + * Max-Inline Size + * @see https://tailwindcss.com/docs/max-width + */ + 'max-inline-size': [{ 'max-inline': ['none', ...scaleSizingInline()] }], + /** + * Block Size + * @see https://tailwindcss.com/docs/height + */ + 'block-size': [{ block: ['auto', ...scaleSizingBlock()] }], + /** + * Min-Block Size + * @see https://tailwindcss.com/docs/min-height + */ + 'min-block-size': [{ 'min-block': ['auto', ...scaleSizingBlock()] }], + /** + * Max-Block Size + * @see https://tailwindcss.com/docs/max-height + */ + 'max-block-size': [{ 'max-block': ['none', ...scaleSizingBlock()] }], /** * Width * @see https://tailwindcss.com/docs/width @@ -855,6 +962,11 @@ export const getDefaultConfig = () => { 'font-family': [ { font: [isArbitraryVariableFamilyName, isArbitraryFamilyName, themeFont] }, ], + /** + * Font Feature Settings + * @see https://tailwindcss.com/docs/font-feature-settings + */ + 'font-features': [{ 'font-features': [isArbitraryValue] }], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric @@ -1226,25 +1338,35 @@ export const getDefaultConfig = () => { */ 'border-w': [{ border: scaleBorderWidth() }], /** - * Border Width X + * Border Width Inline * @see https://tailwindcss.com/docs/border-width */ 'border-w-x': [{ 'border-x': scaleBorderWidth() }], /** - * Border Width Y + * Border Width Block * @see https://tailwindcss.com/docs/border-width */ 'border-w-y': [{ 'border-y': scaleBorderWidth() }], /** - * Border Width Start + * Border Width Inline Start * @see https://tailwindcss.com/docs/border-width */ 'border-w-s': [{ 'border-s': scaleBorderWidth() }], /** - * Border Width End + * Border Width Inline End * @see https://tailwindcss.com/docs/border-width */ 'border-w-e': [{ 'border-e': scaleBorderWidth() }], + /** + * Border Width Block Start + * @see https://tailwindcss.com/docs/border-width + */ + 'border-w-bs': [{ 'border-bs': scaleBorderWidth() }], + /** + * Border Width Block End + * @see https://tailwindcss.com/docs/border-width + */ + 'border-w-be': [{ 'border-be': scaleBorderWidth() }], /** * Border Width Top * @see https://tailwindcss.com/docs/border-width @@ -1301,25 +1423,35 @@ export const getDefaultConfig = () => { */ 'border-color': [{ border: scaleColor() }], /** - * Border Color X + * Border Color Inline * @see https://tailwindcss.com/docs/border-color */ 'border-color-x': [{ 'border-x': scaleColor() }], /** - * Border Color Y + * Border Color Block * @see https://tailwindcss.com/docs/border-color */ 'border-color-y': [{ 'border-y': scaleColor() }], /** - * Border Color S + * Border Color Inline Start * @see https://tailwindcss.com/docs/border-color */ 'border-color-s': [{ 'border-s': scaleColor() }], /** - * Border Color E + * Border Color Inline End * @see https://tailwindcss.com/docs/border-color */ 'border-color-e': [{ 'border-e': scaleColor() }], + /** + * Border Color Block Start + * @see https://tailwindcss.com/docs/border-color + */ + 'border-color-bs': [{ 'border-bs': scaleColor() }], + /** + * Border Color Block End + * @see https://tailwindcss.com/docs/border-color + */ + 'border-color-be': [{ 'border-be': scaleColor() }], /** * Border Color Top * @see https://tailwindcss.com/docs/border-color @@ -2055,25 +2187,35 @@ export const getDefaultConfig = () => { */ 'scroll-m': [{ 'scroll-m': scaleUnambiguousSpacing() }], /** - * Scroll Margin X + * Scroll Margin Inline * @see https://tailwindcss.com/docs/scroll-margin */ 'scroll-mx': [{ 'scroll-mx': scaleUnambiguousSpacing() }], /** - * Scroll Margin Y + * Scroll Margin Block * @see https://tailwindcss.com/docs/scroll-margin */ 'scroll-my': [{ 'scroll-my': scaleUnambiguousSpacing() }], /** - * Scroll Margin Start + * Scroll Margin Inline Start * @see https://tailwindcss.com/docs/scroll-margin */ 'scroll-ms': [{ 'scroll-ms': scaleUnambiguousSpacing() }], /** - * Scroll Margin End + * Scroll Margin Inline End * @see https://tailwindcss.com/docs/scroll-margin */ 'scroll-me': [{ 'scroll-me': scaleUnambiguousSpacing() }], + /** + * Scroll Margin Block Start + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mbs': [{ 'scroll-mbs': scaleUnambiguousSpacing() }], + /** + * Scroll Margin Block End + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mbe': [{ 'scroll-mbe': scaleUnambiguousSpacing() }], /** * Scroll Margin Top * @see https://tailwindcss.com/docs/scroll-margin @@ -2100,25 +2242,35 @@ export const getDefaultConfig = () => { */ 'scroll-p': [{ 'scroll-p': scaleUnambiguousSpacing() }], /** - * Scroll Padding X + * Scroll Padding Inline * @see https://tailwindcss.com/docs/scroll-padding */ 'scroll-px': [{ 'scroll-px': scaleUnambiguousSpacing() }], /** - * Scroll Padding Y + * Scroll Padding Block * @see https://tailwindcss.com/docs/scroll-padding */ 'scroll-py': [{ 'scroll-py': scaleUnambiguousSpacing() }], /** - * Scroll Padding Start + * Scroll Padding Inline Start * @see https://tailwindcss.com/docs/scroll-padding */ 'scroll-ps': [{ 'scroll-ps': scaleUnambiguousSpacing() }], /** - * Scroll Padding End + * Scroll Padding Inline End * @see https://tailwindcss.com/docs/scroll-padding */ 'scroll-pe': [{ 'scroll-pe': scaleUnambiguousSpacing() }], + /** + * Scroll Padding Block Start + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pbs': [{ 'scroll-pbs': scaleUnambiguousSpacing() }], + /** + * Scroll Padding Block End + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pbe': [{ 'scroll-pbe': scaleUnambiguousSpacing() }], /** * Scroll Padding Top * @see https://tailwindcss.com/docs/scroll-padding @@ -2243,15 +2395,26 @@ export const getDefaultConfig = () => { conflictingClassGroups: { overflow: ['overflow-x', 'overflow-y'], overscroll: ['overscroll-x', 'overscroll-y'], - inset: ['inset-x', 'inset-y', 'start', 'end', 'top', 'right', 'bottom', 'left'], + inset: [ + 'inset-x', + 'inset-y', + 'inset-bs', + 'inset-be', + 'start', + 'end', + 'top', + 'right', + 'bottom', + 'left', + ], 'inset-x': ['right', 'left'], 'inset-y': ['top', 'bottom'], flex: ['basis', 'grow', 'shrink'], gap: ['gap-x', 'gap-y'], - p: ['px', 'py', 'ps', 'pe', 'pt', 'pr', 'pb', 'pl'], + p: ['px', 'py', 'ps', 'pe', 'pbs', 'pbe', 'pt', 'pr', 'pb', 'pl'], px: ['pr', 'pl'], py: ['pt', 'pb'], - m: ['mx', 'my', 'ms', 'me', 'mt', 'mr', 'mb', 'ml'], + m: ['mx', 'my', 'ms', 'me', 'mbs', 'mbe', 'mt', 'mr', 'mb', 'ml'], mx: ['mr', 'ml'], my: ['mt', 'mb'], size: ['w', 'h'], @@ -2297,6 +2460,8 @@ export const getDefaultConfig = () => { 'border-w-y', 'border-w-s', 'border-w-e', + 'border-w-bs', + 'border-w-be', 'border-w-t', 'border-w-r', 'border-w-b', @@ -2309,6 +2474,8 @@ export const getDefaultConfig = () => { 'border-color-y', 'border-color-s', 'border-color-e', + 'border-color-bs', + 'border-color-be', 'border-color-t', 'border-color-r', 'border-color-b', @@ -2323,6 +2490,8 @@ export const getDefaultConfig = () => { 'scroll-my', 'scroll-ms', 'scroll-me', + 'scroll-mbs', + 'scroll-mbe', 'scroll-mt', 'scroll-mr', 'scroll-mb', @@ -2335,6 +2504,8 @@ export const getDefaultConfig = () => { 'scroll-py', 'scroll-ps', 'scroll-pe', + 'scroll-pbs', + 'scroll-pbe', 'scroll-pt', 'scroll-pr', 'scroll-pb', diff --git a/src/lib/types.ts b/src/lib/types.ts index 274c95a0..1cf30f2a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -236,9 +236,12 @@ export type DefaultClassGroupIds = | 'bg-position' | 'bg-repeat' | 'bg-size' + | 'block-size' | 'blur' | 'border-collapse' | 'border-color-b' + | 'border-color-be' + | 'border-color-bs' | 'border-color-e' | 'border-color-l' | 'border-color-r' @@ -252,6 +255,8 @@ export type DefaultClassGroupIds = | 'border-spacing' | 'border-style' | 'border-w-b' + | 'border-w-be' + | 'border-w-bs' | 'border-w-e' | 'border-w-l' | 'border-w-r' @@ -301,6 +306,7 @@ export type DefaultClassGroupIds = | 'flex' | 'float' | 'font-family' + | 'font-features' | 'font-size' | 'font-smoothing' | 'font-stretch' @@ -331,10 +337,13 @@ export type DefaultClassGroupIds = | 'hue-rotate' | 'hyphens' | 'indent' + | 'inline-size' | 'inset-ring-color' | 'inset-ring-w' | 'inset-shadow-color' | 'inset-shadow' + | 'inset-be' + | 'inset-bs' | 'inset-x' | 'inset-y' | 'inset' @@ -401,11 +410,17 @@ export type DefaultClassGroupIds = | 'mask-repeat' | 'mask-size' | 'mask-type' + | 'max-block-size' | 'max-h' + | 'max-inline-size' | 'max-w' | 'mb' + | 'mbe' + | 'mbs' | 'me' + | 'min-block-size' | 'min-h' + | 'min-inline-size' | 'min-w' | 'mix-blend' | 'ml' @@ -430,6 +445,7 @@ export type DefaultClassGroupIds = | 'overscroll' | 'p' | 'pb' + | 'pbe' | 'pe' | 'perspective-origin' | 'perspective' @@ -442,6 +458,7 @@ export type DefaultClassGroupIds = | 'position' | 'pr' | 'ps' + | 'pbs' | 'pt' | 'px' | 'py' @@ -483,18 +500,22 @@ export type DefaultClassGroupIds = | 'scroll-behavior' | 'scroll-m' | 'scroll-mb' + | 'scroll-mbe' | 'scroll-me' | 'scroll-ml' | 'scroll-mr' + | 'scroll-mbs' | 'scroll-ms' | 'scroll-mt' | 'scroll-mx' | 'scroll-my' | 'scroll-p' | 'scroll-pb' + | 'scroll-pbe' | 'scroll-pe' | 'scroll-pl' | 'scroll-pr' + | 'scroll-pbs' | 'scroll-ps' | 'scroll-pt' | 'scroll-px' diff --git a/src/lib/validators.ts b/src/lib/validators.ts index ef239539..fd2d9406 100644 --- a/src/lib/validators.ts +++ b/src/lib/validators.ts @@ -1,6 +1,6 @@ const arbitraryValueRegex = /^\[(?:(\w[\w-]*):)?(.+)\]$/i const arbitraryVariableRegex = /^\((?:(\w[\w-]*):)?(.+)\)$/i -const fractionRegex = /^\d+\/\d+$/ +const fractionRegex = /^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/ const tshirtUnitRegex = /^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/ const lengthUnitRegex = /\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/ diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index bfdee9fd..ab0e352f 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -49,12 +49,14 @@ test('class map has correct class groups at first part', () => { 'bg-repeat', 'bg-size', ], - block: ['display'], + block: ['block-size', 'display'], blur: ['blur'], border: [ 'border-collapse', 'border-color', 'border-color-b', + 'border-color-be', + 'border-color-bs', 'border-color-e', 'border-color-l', 'border-color-r', @@ -68,6 +70,8 @@ test('class map has correct class groups at first part', () => { 'border-style', 'border-w', 'border-w-b', + 'border-w-be', + 'border-w-bs', 'border-w-e', 'border-w-l', 'border-w-r', @@ -114,7 +118,7 @@ test('class map has correct class groups at first part', () => { flex: ['display', 'flex', 'flex-direction', 'flex-wrap'], float: ['float'], flow: ['display'], - font: ['font-family', 'font-stretch', 'font-weight'], + font: ['font-family', 'font-features', 'font-stretch', 'font-weight'], forced: ['forced-color-adjust'], from: ['gradient-from', 'gradient-from-pos'], gap: ['gap', 'gap-x', 'gap-y'], @@ -126,15 +130,19 @@ test('class map has correct class groups at first part', () => { hue: ['hue-rotate'], hyphens: ['hyphens'], indent: ['indent'], - inline: ['display'], + inline: ['display', 'inline-size'], inset: [ + 'end', 'inset', + 'inset-be', + 'inset-bs', 'inset-ring-color', 'inset-ring-w', 'inset-shadow', 'inset-shadow-color', 'inset-x', 'inset-y', + 'start', ], invert: ['invert'], invisible: ['visibility'], @@ -203,10 +211,12 @@ test('class map has correct class groups at first part', () => { 'mask-size', 'mask-type', ], - max: ['max-h', 'max-w'], + max: ['max-block-size', 'max-h', 'max-inline-size', 'max-w'], mb: ['mb'], + mbe: ['mbe'], + mbs: ['mbs'], me: ['me'], - min: ['min-h', 'min-w'], + min: ['min-block-size', 'min-h', 'min-inline-size', 'min-w'], mix: ['mix-blend'], ml: ['ml'], mr: ['mr'], @@ -229,6 +239,8 @@ test('class map has correct class groups at first part', () => { overscroll: ['overscroll', 'overscroll-x', 'overscroll-y'], p: ['p'], pb: ['pb'], + pbe: ['pbe'], + pbs: ['pbs'], pe: ['pe'], perspective: ['perspective', 'perspective-origin'], pl: ['pl'], @@ -271,6 +283,8 @@ test('class map has correct class groups at first part', () => { 'scroll-behavior', 'scroll-m', 'scroll-mb', + 'scroll-mbe', + 'scroll-mbs', 'scroll-me', 'scroll-ml', 'scroll-mr', @@ -280,6 +294,8 @@ test('class map has correct class groups at first part', () => { 'scroll-my', 'scroll-p', 'scroll-pb', + 'scroll-pbe', + 'scroll-pbs', 'scroll-pe', 'scroll-pl', 'scroll-pr', diff --git a/tests/tailwind-css-versions.test.ts b/tests/tailwind-css-versions.test.ts index 68d0dc98..76d4ccc6 100644 --- a/tests/tailwind-css-versions.test.ts +++ b/tests/tailwind-css-versions.test.ts @@ -167,3 +167,128 @@ test('supports Tailwind CSS v4.1.5 features', () => { expect(twMerge('min-h-12 min-h-lh')).toBe('min-h-lh') expect(twMerge('max-h-12 max-h-lh')).toBe('max-h-lh') }) + +test('supports Tailwind CSS v4.2 features', () => { + // Logical inset utilities + + expect(twMerge('inset-s-1 inset-s-2')).toBe('inset-s-2') + expect(twMerge('inset-e-1 inset-e-2')).toBe('inset-e-2') + expect(twMerge('inset-bs-1 inset-bs-2')).toBe('inset-bs-2') + expect(twMerge('inset-be-1 inset-be-2')).toBe('inset-be-2') + + expect(twMerge('start-1 inset-s-2')).toBe('inset-s-2') + expect(twMerge('inset-s-1 start-2')).toBe('start-2') + expect(twMerge('end-1 inset-e-2')).toBe('inset-e-2') + expect(twMerge('inset-e-1 end-2')).toBe('end-2') + + expect(twMerge('inset-s-1 inset-e-2 inset-bs-3 inset-be-4 inset-0')).toBe('inset-0') + expect(twMerge('inset-0 inset-s-1 inset-bs-1')).toBe('inset-0 inset-s-1 inset-bs-1') + + expect(twMerge('inset-y-1 inset-bs-2 inset-be-3')).toBe('inset-y-1 inset-bs-2 inset-be-3') + expect(twMerge('top-1 inset-bs-2 bottom-3 inset-be-4')).toBe( + 'top-1 inset-bs-2 bottom-3 inset-be-4', + ) + + // Logical spacing utilities + + expect(twMerge('pbs-1 pbs-2')).toBe('pbs-2') + expect(twMerge('pbe-1 pbe-2')).toBe('pbe-2') + expect(twMerge('mbs-1 mbs-2')).toBe('mbs-2') + expect(twMerge('mbe-1 mbe-2')).toBe('mbe-2') + + expect(twMerge('pt-1 pbs-2')).toBe('pt-1 pbs-2') + expect(twMerge('pb-1 pbe-2')).toBe('pb-1 pbe-2') + expect(twMerge('mt-1 mbs-2')).toBe('mt-1 mbs-2') + expect(twMerge('mb-1 mbe-2')).toBe('mb-1 mbe-2') + + expect(twMerge('p-0 pbs-1 pbe-1')).toBe('p-0 pbs-1 pbe-1') + expect(twMerge('pbs-1 pbe-1 p-0')).toBe('p-0') + expect(twMerge('m-0 mbs-1 mbe-1')).toBe('m-0 mbs-1 mbe-1') + expect(twMerge('mbs-1 mbe-1 m-0')).toBe('m-0') + + expect(twMerge('py-1 pbs-2 pbe-3')).toBe('py-1 pbs-2 pbe-3') + expect(twMerge('my-1 mbs-2 mbe-3')).toBe('my-1 mbs-2 mbe-3') + + // Logical scroll spacing utilities + + expect(twMerge('scroll-pbs-1 scroll-pbs-2')).toBe('scroll-pbs-2') + expect(twMerge('scroll-pbe-1 scroll-pbe-2')).toBe('scroll-pbe-2') + expect(twMerge('scroll-mbs-1 scroll-mbs-2')).toBe('scroll-mbs-2') + expect(twMerge('scroll-mbe-1 scroll-mbe-2')).toBe('scroll-mbe-2') + + expect(twMerge('scroll-pt-1 scroll-pbs-2')).toBe('scroll-pt-1 scroll-pbs-2') + expect(twMerge('scroll-pb-1 scroll-pbe-2')).toBe('scroll-pb-1 scroll-pbe-2') + expect(twMerge('scroll-mt-1 scroll-mbs-2')).toBe('scroll-mt-1 scroll-mbs-2') + expect(twMerge('scroll-mb-1 scroll-mbe-2')).toBe('scroll-mb-1 scroll-mbe-2') + + expect(twMerge('scroll-p-0 scroll-pbs-1 scroll-pbe-1')).toBe( + 'scroll-p-0 scroll-pbs-1 scroll-pbe-1', + ) + expect(twMerge('scroll-pbs-1 scroll-pbe-1 scroll-p-0')).toBe('scroll-p-0') + expect(twMerge('scroll-m-0 scroll-mbs-1 scroll-mbe-1')).toBe( + 'scroll-m-0 scroll-mbs-1 scroll-mbe-1', + ) + expect(twMerge('scroll-mbs-1 scroll-mbe-1 scroll-m-0')).toBe('scroll-m-0') + + expect(twMerge('scroll-py-1 scroll-pbs-2 scroll-pbe-3')).toBe( + 'scroll-py-1 scroll-pbs-2 scroll-pbe-3', + ) + expect(twMerge('scroll-my-1 scroll-mbs-2 scroll-mbe-3')).toBe( + 'scroll-my-1 scroll-mbs-2 scroll-mbe-3', + ) + + // Logical border block utilities + + expect(twMerge('border-bs-1 border-bs-2')).toBe('border-bs-2') + expect(twMerge('border-be-1 border-be-2')).toBe('border-be-2') + expect(twMerge('border-bs-red border-bs-blue')).toBe('border-bs-blue') + expect(twMerge('border-be-red border-be-blue')).toBe('border-be-blue') + + expect(twMerge('border-2 border-bs-4 border-be-6')).toBe('border-2 border-bs-4 border-be-6') + expect(twMerge('border-bs-4 border-be-6 border-2')).toBe('border-2') + expect(twMerge('border-red border-bs-blue border-be-green')).toBe( + 'border-red border-bs-blue border-be-green', + ) + expect(twMerge('border-bs-blue border-be-green border-red')).toBe('border-red') + + expect(twMerge('border-y-2 border-bs-4 border-be-6')).toBe('border-y-2 border-bs-4 border-be-6') + expect(twMerge('border-t-2 border-bs-4 border-b-6 border-be-8')).toBe( + 'border-t-2 border-bs-4 border-b-6 border-be-8', + ) + expect(twMerge('border-y-red border-bs-blue border-be-green')).toBe( + 'border-y-red border-bs-blue border-be-green', + ) + + // Logical size utilities + + expect(twMerge('inline-1/2 inline-3/4')).toBe('inline-3/4') + expect(twMerge('block-1/2 block-3/4')).toBe('block-3/4') + expect(twMerge('min-inline-auto min-inline-full')).toBe('min-inline-full') + expect(twMerge('max-inline-none max-inline-10')).toBe('max-inline-10') + expect(twMerge('min-block-auto min-block-lh min-block-10')).toBe('min-block-10') + expect(twMerge('max-block-none max-block-lh max-block-10')).toBe('max-block-10') + + expect(twMerge('w-10 inline-20')).toBe('w-10 inline-20') + expect(twMerge('h-10 block-20')).toBe('h-10 block-20') + expect(twMerge('size-10 inline-20 block-30')).toBe('size-10 inline-20 block-30') + expect(twMerge('min-w-10 min-inline-20')).toBe('min-w-10 min-inline-20') + expect(twMerge('max-h-10 max-block-20')).toBe('max-h-10 max-block-20') + + // Font feature settings utilities + + expect(twMerge('font-features-["smcp"] font-features-["onum"]')).toBe('font-features-["onum"]') + expect(twMerge('font-features-[var(--font-features)] font-features-["liga","dlig"]')).toBe( + 'font-features-["liga","dlig"]', + ) + expect(twMerge('tabular-nums font-features-["smcp"]')).toBe( + 'tabular-nums font-features-["smcp"]', + ) + expect(twMerge('font-features-["smcp"] normal-nums')).toBe('font-features-["smcp"] normal-nums') + expect(twMerge('font-sans font-features-["smcp"]')).toBe('font-sans font-features-["smcp"]') + + // Fractions with decimal numerator/denominator + + expect(twMerge('aspect-8/11 aspect-8.5/11')).toBe('aspect-8.5/11') + expect(twMerge('w-8/11 w-8.5/11')).toBe('w-8.5/11') + expect(twMerge('inset-1/2 inset-1.25/2.5')).toBe('inset-1.25/2.5') +})