diff --git a/docs/README.md b/docs/README.md index 75bafea1..2c0a284b 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 (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.1 (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/docs/features.md b/docs/features.md index 41be44be..ba0329f1 100644 --- a/docs/features.md +++ b/docs/features.md @@ -33,7 +33,7 @@ twMerge('hover:p-2 hover:p-4') // → 'hover:p-4' twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4' ``` -The order of standard modifiers does not matter for tailwind-merge. +tailwind-merge knows when the order of standard modifiers matters and when not and resolves conflicts accordingly. ### Supports arbitrary values diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index b332426d..fbafd88f 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -43,6 +43,7 @@ export const getDefaultConfig = () => { const themeRadius = fromTheme('radius') const themeShadow = fromTheme('shadow') const themeInsetShadow = fromTheme('inset-shadow') + const themeTextShadow = fromTheme('text-shadow') const themeDropShadow = fromTheme('drop-shadow') const themeBlur = fromTheme('blur') const themePerspective = fromTheme('perspective') @@ -62,16 +63,26 @@ export const getDefaultConfig = () => { ['auto', 'avoid', 'all', 'avoid-page', 'page', 'left', 'right', 'column'] as const const scalePosition = () => [ - 'bottom', 'center', + 'top', + 'bottom', 'left', - 'left-bottom', - 'left-top', 'right', - 'right-bottom', + 'top-left', + // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378 + 'left-top', + 'top-right', + // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378 'right-top', - 'top', + 'bottom-right', + // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378 + 'right-bottom', + 'bottom-left', + // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378 + 'left-bottom', ] as const + const scalePositionWithArbitrary = () => + [...scalePosition(), isArbitraryVariable, isArbitraryValue] as const const scaleOverflow = () => ['auto', 'hidden', 'clip', 'visible', 'scroll'] as const const scaleOverscroll = () => ['auto', 'contain', 'none'] as const const scaleUnambiguousSpacing = () => @@ -92,8 +103,20 @@ export const getDefaultConfig = () => { const scaleGridAutoColsRows = () => ['auto', 'min', 'max', 'fr', isArbitraryVariable, isArbitraryValue] as const const scaleAlignPrimaryAxis = () => - ['start', 'end', 'center', 'between', 'around', 'evenly', 'stretch', 'baseline'] as const - const scaleAlignSecondaryAxis = () => ['start', 'end', 'center', 'stretch'] as const + [ + 'start', + 'end', + 'center', + 'between', + 'around', + 'evenly', + 'stretch', + 'baseline', + 'center-safe', + 'end-safe', + ] as const + const scaleAlignSecondaryAxis = () => + ['start', 'end', 'center', 'stretch', 'center-safe', 'end-safe'] as const const scaleMargin = () => ['auto', ...scaleUnambiguousSpacing()] as const const scaleSizing = () => [ @@ -112,6 +135,23 @@ export const getDefaultConfig = () => { ...scaleUnambiguousSpacing(), ] as const const scaleColor = () => [themeColor, isArbitraryVariable, isArbitraryValue] as const + const scaleBgPosition = () => + [ + ...scalePosition(), + isArbitraryVariablePosition, + isArbitraryPosition, + { position: [isArbitraryVariable, isArbitraryValue] }, + ] as const + const scaleBgRepeat = () => ['no-repeat', { repeat: ['', 'x', 'y', 'space', 'round'] }] as const + const scaleBgSize = () => + [ + 'auto', + 'cover', + 'contain', + isArbitraryVariableSize, + isArbitrarySize, + { size: [isArbitraryVariable, isArbitraryValue] }, + ] as const const scaleGradientStopPosition = () => [isPercent, isArbitraryVariableLength, isArbitraryLength] as const const scaleRadius = () => @@ -146,6 +186,8 @@ export const getDefaultConfig = () => { 'color', 'luminosity', ] as const + const scaleMaskImagePosition = () => + [isNumber, isPercent, isArbitraryVariablePosition, isArbitraryPosition] as const const scaleBlur = () => [ // Deprecated since Tailwind CSS v4.0.0 @@ -155,20 +197,6 @@ export const getDefaultConfig = () => { isArbitraryVariable, isArbitraryValue, ] as const - const scaleOrigin = () => - [ - 'center', - 'top', - 'top-right', - 'right', - 'bottom-right', - 'bottom', - 'bottom-left', - 'left', - 'top-left', - isArbitraryVariable, - isArbitraryValue, - ] as const const scaleRotate = () => ['none', isNumber, isArbitraryVariable, isArbitraryValue] as const const scaleScale = () => ['none', isNumber, isArbitraryVariable, isArbitraryValue] as const const scaleSkew = () => [isNumber, isArbitraryVariable, isArbitraryValue] as const @@ -204,6 +232,7 @@ export const getDefaultConfig = () => { shadow: [isTshirtSize], spacing: ['px', isNumber], text: [isTshirtSize], + 'text-shadow': [isTshirtSize], tracking: ['tighter', 'tight', 'normal', 'wide', 'wider', 'widest'], }, classGroups: { @@ -321,9 +350,7 @@ export const getDefaultConfig = () => { * Object Position * @see https://tailwindcss.com/docs/object-position */ - 'object-position': [ - { object: [...scalePosition(), isArbitraryValue, isArbitraryVariable] }, - ], + 'object-position': [{ object: scalePositionWithArbitrary() }], /** * Overflow * @see https://tailwindcss.com/docs/overflow @@ -569,12 +596,14 @@ export const getDefaultConfig = () => { * Align Items * @see https://tailwindcss.com/docs/align-items */ - 'align-items': [{ items: [...scaleAlignSecondaryAxis(), 'baseline'] }], + 'align-items': [{ items: [...scaleAlignSecondaryAxis(), { baseline: ['', 'last'] }] }], /** * Align Self * @see https://tailwindcss.com/docs/align-self */ - 'align-self': [{ self: ['auto', ...scaleAlignSecondaryAxis(), 'baseline'] }], + 'align-self': [ + { self: ['auto', ...scaleAlignSecondaryAxis(), { baseline: ['', 'last'] }] }, + ], /** * Place Content * @see https://tailwindcss.com/docs/place-content @@ -994,6 +1023,11 @@ export const getDefaultConfig = () => { * @see https://tailwindcss.com/docs/word-break */ break: [{ break: ['normal', 'words', 'all', 'keep'] }], + /** + * Overflow Wrap + * @see https://tailwindcss.com/docs/overflow-wrap + */ + wrap: [{ wrap: ['break-word', 'anywhere', 'normal'] }], /** * Hyphens * @see https://tailwindcss.com/docs/hyphens @@ -1028,21 +1062,17 @@ export const getDefaultConfig = () => { * Background Position * @see https://tailwindcss.com/docs/background-position */ - 'bg-position': [ - { bg: [...scalePosition(), isArbitraryVariablePosition, isArbitraryPosition] }, - ], + 'bg-position': [{ bg: scaleBgPosition() }], /** * Background Repeat * @see https://tailwindcss.com/docs/background-repeat */ - 'bg-repeat': [{ bg: ['no-repeat', { repeat: ['', 'x', 'y', 'space', 'round'] }] }], + 'bg-repeat': [{ bg: scaleBgRepeat() }], /** * Background Size * @see https://tailwindcss.com/docs/background-size */ - 'bg-size': [ - { bg: ['auto', 'cover', 'contain', isArbitraryVariableSize, isArbitrarySize] }, - ], + 'bg-size': [{ bg: scaleBgSize() }], /** * Background Image * @see https://tailwindcss.com/docs/background-image @@ -1329,7 +1359,7 @@ export const getDefaultConfig = () => { * Outline Color * @see https://tailwindcss.com/docs/outline-color */ - 'outline-color': [{ outline: [themeColor] }], + 'outline-color': [{ outline: scaleColor() }], // --------------- // --- Effects --- @@ -1364,9 +1394,9 @@ export const getDefaultConfig = () => { { 'inset-shadow': [ 'none', - isArbitraryVariable, - isArbitraryValue, themeInsetShadow, + isArbitraryVariableShadow, + isArbitraryShadow, ], }, ], @@ -1416,6 +1446,25 @@ export const getDefaultConfig = () => { * @see https://tailwindcss.com/docs/box-shadow#setting-the-inset-ring-color */ 'inset-ring-color': [{ 'inset-ring': scaleColor() }], + /** + * Text Shadow + * @see https://tailwindcss.com/docs/text-shadow + */ + 'text-shadow': [ + { + 'text-shadow': [ + 'none', + themeTextShadow, + isArbitraryVariableShadow, + isArbitraryShadow, + ], + }, + ], + /** + * Text Shadow Color + * @see https://tailwindcss.com/docs/text-shadow#setting-the-shadow-color + */ + 'text-shadow-color': [{ 'text-shadow': scaleColor() }], /** * Opacity * @see https://tailwindcss.com/docs/opacity @@ -1431,6 +1480,104 @@ export const getDefaultConfig = () => { * @see https://tailwindcss.com/docs/background-blend-mode */ 'bg-blend': [{ 'bg-blend': scaleBlendMode() }], + /** + * Mask Clip + * @see https://tailwindcss.com/docs/mask-clip + */ + 'mask-clip': [ + { 'mask-clip': ['border', 'padding', 'content', 'fill', 'stroke', 'view'] }, + 'mask-no-clip', + ], + /** + * Mask Composite + * @see https://tailwindcss.com/docs/mask-composite + */ + 'mask-composite': [{ mask: ['add', 'subtract', 'intersect', 'exclude'] }], + /** + * Mask Image + * @see https://tailwindcss.com/docs/mask-image + */ + 'mask-image-linear-pos': [{ 'mask-linear': [isNumber] }], + 'mask-image-linear-from-pos': [{ 'mask-linear-from': scaleMaskImagePosition() }], + 'mask-image-linear-to-pos': [{ 'mask-linear-to': scaleMaskImagePosition() }], + 'mask-image-linear-from-color': [{ 'mask-linear-from': scaleColor() }], + 'mask-image-linear-to-color': [{ 'mask-linear-to': scaleColor() }], + 'mask-image-t-from-pos': [{ 'mask-t-from': scaleMaskImagePosition() }], + 'mask-image-t-to-pos': [{ 'mask-t-to': scaleMaskImagePosition() }], + 'mask-image-t-from-color': [{ 'mask-t-from': scaleColor() }], + 'mask-image-t-to-color': [{ 'mask-t-to': scaleColor() }], + 'mask-image-r-from-pos': [{ 'mask-r-from': scaleMaskImagePosition() }], + 'mask-image-r-to-pos': [{ 'mask-r-to': scaleMaskImagePosition() }], + 'mask-image-r-from-color': [{ 'mask-r-from': scaleColor() }], + 'mask-image-r-to-color': [{ 'mask-r-to': scaleColor() }], + 'mask-image-b-from-pos': [{ 'mask-b-from': scaleMaskImagePosition() }], + 'mask-image-b-to-pos': [{ 'mask-b-to': scaleMaskImagePosition() }], + 'mask-image-b-from-color': [{ 'mask-b-from': scaleColor() }], + 'mask-image-b-to-color': [{ 'mask-b-to': scaleColor() }], + 'mask-image-l-from-pos': [{ 'mask-l-from': scaleMaskImagePosition() }], + 'mask-image-l-to-pos': [{ 'mask-l-to': scaleMaskImagePosition() }], + 'mask-image-l-from-color': [{ 'mask-l-from': scaleColor() }], + 'mask-image-l-to-color': [{ 'mask-l-to': scaleColor() }], + 'mask-image-x-from-pos': [{ 'mask-x-from': scaleMaskImagePosition() }], + 'mask-image-x-to-pos': [{ 'mask-x-to': scaleMaskImagePosition() }], + 'mask-image-x-from-color': [{ 'mask-x-from': scaleColor() }], + 'mask-image-x-to-color': [{ 'mask-x-to': scaleColor() }], + 'mask-image-y-from-pos': [{ 'mask-y-from': scaleMaskImagePosition() }], + 'mask-image-y-to-pos': [{ 'mask-y-to': scaleMaskImagePosition() }], + 'mask-image-y-from-color': [{ 'mask-y-from': scaleColor() }], + 'mask-image-y-to-color': [{ 'mask-y-to': scaleColor() }], + 'mask-image-radial': [{ 'mask-radial': [isArbitraryVariable, isArbitraryValue] }], + 'mask-image-radial-from-pos': [{ 'mask-radial-from': scaleMaskImagePosition() }], + 'mask-image-radial-to-pos': [{ 'mask-radial-to': scaleMaskImagePosition() }], + 'mask-image-radial-from-color': [{ 'mask-radial-from': scaleColor() }], + 'mask-image-radial-to-color': [{ 'mask-radial-to': scaleColor() }], + 'mask-image-radial-shape': [{ 'mask-radial': ['circle', 'ellipse'] }], + 'mask-image-radial-size': [ + { 'mask-radial': [{ closest: ['side', 'corner'], farthest: ['side', 'corner'] }] }, + ], + 'mask-image-radial-pos': [{ 'mask-radial-at': scalePosition() }], + 'mask-image-conic-pos': [{ 'mask-conic': [isNumber] }], + 'mask-image-conic-from-pos': [{ 'mask-conic-from': scaleMaskImagePosition() }], + 'mask-image-conic-to-pos': [{ 'mask-conic-to': scaleMaskImagePosition() }], + 'mask-image-conic-from-color': [{ 'mask-conic-from': scaleColor() }], + 'mask-image-conic-to-color': [{ 'mask-conic-to': scaleColor() }], + /** + * Mask Mode + * @see https://tailwindcss.com/docs/mask-mode + */ + 'mask-mode': [{ mask: ['alpha', 'luminance', 'match'] }], + /** + * Mask Origin + * @see https://tailwindcss.com/docs/mask-origin + */ + 'mask-origin': [ + { 'mask-origin': ['border', 'padding', 'content', 'fill', 'stroke', 'view'] }, + ], + /** + * Mask Position + * @see https://tailwindcss.com/docs/mask-position + */ + 'mask-position': [{ mask: scaleBgPosition() }], + /** + * Mask Repeat + * @see https://tailwindcss.com/docs/mask-repeat + */ + 'mask-repeat': [{ mask: scaleBgRepeat() }], + /** + * Mask Size + * @see https://tailwindcss.com/docs/mask-size + */ + 'mask-size': [{ mask: scaleBgSize() }], + /** + * Mask Type + * @see https://tailwindcss.com/docs/mask-type + */ + 'mask-type': [{ 'mask-type': ['alpha', 'luminance'] }], + /** + * Mask Image + * @see https://tailwindcss.com/docs/mask-image + */ + 'mask-image': [{ mask: ['none', isArbitraryVariable, isArbitraryValue] }], // --------------- // --- Filters --- @@ -1477,11 +1624,16 @@ export const getDefaultConfig = () => { '', 'none', themeDropShadow, - isArbitraryVariable, - isArbitraryValue, + isArbitraryVariableShadow, + isArbitraryShadow, ], }, ], + /** + * Drop Shadow Color + * @see https://tailwindcss.com/docs/filter-drop-shadow#setting-the-shadow-color + */ + 'drop-shadow-color': [{ 'drop-shadow': scaleColor() }], /** * Grayscale * @see https://tailwindcss.com/docs/grayscale @@ -1690,7 +1842,7 @@ export const getDefaultConfig = () => { * Perspective Origin * @see https://tailwindcss.com/docs/perspective-origin */ - 'perspective-origin': [{ 'perspective-origin': scaleOrigin() }], + 'perspective-origin': [{ 'perspective-origin': scalePositionWithArbitrary() }], /** * Rotate * @see https://tailwindcss.com/docs/rotate @@ -1762,7 +1914,7 @@ export const getDefaultConfig = () => { * Transform Origin * @see https://tailwindcss.com/docs/transform-origin */ - 'transform-origin': [{ origin: scaleOrigin() }], + 'transform-origin': [{ origin: scalePositionWithArbitrary() }], /** * Transform Style * @see https://tailwindcss.com/docs/transform-style @@ -2132,6 +2284,8 @@ export const getDefaultConfig = () => { 'rounded-l': ['rounded-tl', 'rounded-bl'], 'border-spacing': ['border-spacing-x', 'border-spacing-y'], 'border-w': [ + 'border-w-x', + 'border-w-y', 'border-w-s', 'border-w-e', 'border-w-t', @@ -2142,6 +2296,8 @@ export const getDefaultConfig = () => { 'border-w-x': ['border-w-r', 'border-w-l'], 'border-w-y': ['border-w-t', 'border-w-b'], 'border-color': [ + 'border-color-x', + 'border-color-y', 'border-color-s', 'border-color-e', 'border-color-t', @@ -2186,17 +2342,18 @@ export const getDefaultConfig = () => { 'font-size': ['leading'], }, orderSensitiveModifiers: [ - 'before', + '*', + '**', 'after', - 'placeholder', + 'backdrop', + 'before', + 'details-content', 'file', + 'first-letter', + 'first-line', 'marker', + 'placeholder', 'selection', - 'first-line', - 'first-letter', - 'backdrop', - '*', - '**', ], } as const satisfies Config } diff --git a/src/lib/types.ts b/src/lib/types.ts index 3f6c31d4..274c95a0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -107,8 +107,10 @@ interface ConfigGroupsPart>> @@ -116,6 +118,7 @@ interface ConfigGroupsPart fractionRegex.test(value) -export const isNumber = (value: string) => Boolean(value) && !Number.isNaN(Number(value)) +export const isNumber = (value: string) => !!value && !Number.isNaN(Number(value)) -export const isInteger = (value: string) => Boolean(value) && Number.isInteger(Number(value)) +export const isInteger = (value: string) => !!value && Number.isInteger(Number(value)) export const isPercent = (value: string) => value.endsWith('%') && isNumber(value.slice(0, -1)) @@ -52,7 +52,8 @@ export const isArbitraryPosition = (value: string) => export const isArbitraryImage = (value: string) => getIsArbitraryValue(value, isLabelImage, isImage) -export const isArbitraryShadow = (value: string) => getIsArbitraryValue(value, isNever, isShadow) +export const isArbitraryShadow = (value: string) => + getIsArbitraryValue(value, isLabelShadow, isShadow) export const isArbitraryVariable = (value: string) => arbitraryVariableRegex.test(value) @@ -112,15 +113,11 @@ const getIsArbitraryVariable = ( // Labels -const isLabelPosition = (label: string) => label === 'position' +const isLabelPosition = (label: string) => label === 'position' || label === 'percentage' -const imageLabels = new Set(['image', 'url']) +const isLabelImage = (label: string) => label === 'image' || label === 'url' -const isLabelImage = (label: string) => imageLabels.has(label) - -const sizeLabels = new Set(['length', 'size', 'percentage']) - -const isLabelSize = (label: string) => sizeLabels.has(label) +const isLabelSize = (label: string) => label === 'length' || label === 'size' || label === 'bg-size' const isLabelLength = (label: string) => label === 'length' diff --git a/tests/arbitrary-values.test.ts b/tests/arbitrary-values.test.ts index 9f8e0818..6a4d0471 100644 --- a/tests/arbitrary-values.test.ts +++ b/tests/arbitrary-values.test.ts @@ -67,9 +67,9 @@ test('handles ambiguous arbitrary values correctly', () => { expect(twMerge('text-2xl text-[calc(theme(fontSize.4xl)/1.125)]')).toBe( 'text-[calc(theme(fontSize.4xl)/1.125)]', ) - expect(twMerge('bg-cover bg-[percentage:30%] bg-[length:200px_100px]')).toBe( - 'bg-[length:200px_100px]', - ) + expect( + twMerge('bg-cover bg-[percentage:30%] bg-[size:200px_100px] bg-[length:200px_100px]'), + ).toBe('bg-[percentage:30%] bg-[length:200px_100px]') expect( twMerge( 'bg-none bg-[url(.)] bg-[image:.] bg-[url:.] bg-[linear-gradient(.)] bg-linear-to-r', diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 7d81fae1..2e3e99ef 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -103,7 +103,7 @@ test('class map has correct class groups at first part', () => { 'divide-y', 'divide-y-reverse', ], - drop: ['drop-shadow'], + drop: ['drop-shadow', 'drop-shadow-color'], duration: ['duration'], ease: ['ease'], end: ['end'], @@ -150,6 +150,59 @@ test('class map has correct class groups at first part', () => { list: ['display', 'list-image', 'list-style-position', 'list-style-type'], lowercase: ['text-transform'], m: ['m'], + mask: [ + 'mask-clip', + 'mask-composite', + 'mask-image', + 'mask-image-b-from-color', + 'mask-image-b-from-pos', + 'mask-image-b-to-color', + 'mask-image-b-to-pos', + 'mask-image-conic-from-color', + 'mask-image-conic-from-pos', + 'mask-image-conic-pos', + 'mask-image-conic-to-color', + 'mask-image-conic-to-pos', + 'mask-image-l-from-color', + 'mask-image-l-from-pos', + 'mask-image-l-to-color', + 'mask-image-l-to-pos', + 'mask-image-linear-from-color', + 'mask-image-linear-from-pos', + 'mask-image-linear-pos', + 'mask-image-linear-to-color', + 'mask-image-linear-to-pos', + 'mask-image-r-from-color', + 'mask-image-r-from-pos', + 'mask-image-r-to-color', + 'mask-image-r-to-pos', + 'mask-image-radial', + 'mask-image-radial-from-color', + 'mask-image-radial-from-pos', + 'mask-image-radial-pos', + 'mask-image-radial-shape', + 'mask-image-radial-size', + 'mask-image-radial-to-color', + 'mask-image-radial-to-pos', + 'mask-image-t-from-color', + 'mask-image-t-from-pos', + 'mask-image-t-to-color', + 'mask-image-t-to-pos', + 'mask-image-x-from-color', + 'mask-image-x-from-pos', + 'mask-image-x-to-color', + 'mask-image-x-to-pos', + 'mask-image-y-from-color', + 'mask-image-y-from-pos', + 'mask-image-y-to-color', + 'mask-image-y-to-pos', + 'mask-mode', + 'mask-origin', + 'mask-position', + 'mask-repeat', + 'mask-size', + 'mask-type', + ], max: ['max-h', 'max-w'], mb: ['mb'], me: ['me'], @@ -254,7 +307,15 @@ test('class map has correct class groups at first part', () => { subpixel: ['font-smoothing'], table: ['display', 'table-layout'], tabular: ['fvn-spacing'], - text: ['font-size', 'text-alignment', 'text-color', 'text-overflow', 'text-wrap'], + text: [ + 'font-size', + 'text-alignment', + 'text-color', + 'text-overflow', + 'text-shadow', + 'text-shadow-color', + 'text-wrap', + ], to: ['gradient-to', 'gradient-to-pos'], top: ['top'], touch: ['touch', 'touch-pz', 'touch-x', 'touch-y'], @@ -270,6 +331,7 @@ test('class map has correct class groups at first part', () => { w: ['w'], whitespace: ['whitespace'], will: ['will-change'], + wrap: ['wrap'], z: ['z'], }) }) diff --git a/tests/tailwind-css-versions.test.ts b/tests/tailwind-css-versions.test.ts index 9c47b4dc..41fbcec4 100644 --- a/tests/tailwind-css-versions.test.ts +++ b/tests/tailwind-css-versions.test.ts @@ -87,3 +87,77 @@ test('supports Tailwind CSS v4.0 features', () => { 'via-red-500 via-(length:--mobile-header-gradient)', ) }) + +test('supports Tailwind CSS v4.1 features', () => { + expect(twMerge('items-baseline items-baseline-last')).toBe('items-baseline-last') + expect(twMerge('self-baseline self-baseline-last')).toBe('self-baseline-last') + expect(twMerge('place-content-center place-content-end-safe place-content-center-safe')).toBe( + 'place-content-center-safe', + ) + expect(twMerge('items-center-safe items-baseline items-end-safe')).toBe('items-end-safe') + expect(twMerge('wrap-break-word wrap-normal wrap-anywhere')).toBe('wrap-anywhere') + expect(twMerge('text-shadow-none text-shadow-2xl')).toBe('text-shadow-2xl') + expect( + twMerge( + 'text-shadow-none text-shadow-md text-shadow-red text-shadow-red-500 shadow-red shadow-3xs', + ), + ).toBe('text-shadow-md text-shadow-red-500 shadow-red shadow-3xs') + expect(twMerge('mask-add mask-subtract')).toBe('mask-subtract') + expect( + twMerge( + // mask-image + 'mask-(--foo) mask-[foo] mask-none', + // mask-image-linear-pos + 'mask-linear-1 mask-linear-2', + // mask-image-linear-from-pos + 'mask-linear-from-[position:test] mask-linear-from-3', + // mask-image-linear-to-pos + 'mask-linear-to-[position:test] mask-linear-to-3', + // mask-image-linear-from-color + 'mask-linear-from-color-red mask-linear-from-color-3', + // mask-image-linear-to-color + 'mask-linear-to-color-red mask-linear-to-color-3', + // mask-image-t-from-pos + 'mask-t-from-[position:test] mask-t-from-3', + // mask-image-t-to-pos + 'mask-t-to-[position:test] mask-t-to-3', + // mask-image-t-from-color + 'mask-t-from-color-red mask-t-from-color-3', + // mask-image-radial + 'mask-radial-(--test) mask-radial-[test]', + // mask-image-radial-from-pos + 'mask-radial-from-[position:test] mask-radial-from-3', + // mask-image-radial-to-pos + 'mask-radial-to-[position:test] mask-radial-to-3', + // mask-image-radial-from-color + 'mask-radial-from-color-red mask-radial-from-color-3', + ), + ).toBe( + 'mask-none mask-linear-2 mask-linear-from-3 mask-linear-to-3 mask-linear-from-color-3 mask-linear-to-color-3 mask-t-from-3 mask-t-to-3 mask-t-from-color-3 mask-radial-[test] mask-radial-from-3 mask-radial-to-3 mask-radial-from-color-3', + ) + expect( + twMerge( + // mask-image + 'mask-(--something) mask-[something]', + // mask-position + 'mask-top-left mask-center mask-(position:--var) mask-[position:1px_1px] mask-position-(--var) mask-position-[1px_1px]', + ), + ).toBe('mask-[something] mask-position-[1px_1px]') + expect( + twMerge( + // mask-image + 'mask-(--something) mask-[something]', + // mask-size + 'mask-auto mask-[size:foo] mask-(size:--foo) mask-size-[foo] mask-size-(--foo) mask-cover mask-contain', + ), + ).toBe('mask-[something] mask-contain') + expect(twMerge('mask-type-luminance mask-type-alpha')).toBe('mask-type-alpha') + expect(twMerge('shadow-md shadow-lg/25 text-shadow-md text-shadow-lg/25')).toBe( + 'shadow-lg/25 text-shadow-lg/25', + ) + expect( + twMerge('drop-shadow-some-color drop-shadow-[#123456] drop-shadow-lg drop-shadow-[10px_0]'), + ).toBe('drop-shadow-[#123456] drop-shadow-[10px_0]') + expect(twMerge('drop-shadow-[#123456] drop-shadow-some-color')).toBe('drop-shadow-some-color') + expect(twMerge('drop-shadow-2xl drop-shadow-[shadow:foo]')).toBe('drop-shadow-[shadow:foo]') +}) diff --git a/tests/validators.test.ts b/tests/validators.test.ts index 9fdd96d4..b123aac2 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -95,6 +95,7 @@ test('isArbitraryNumber', () => { test('isArbitraryPosition', () => { expect(isArbitraryPosition('[position:2px]')).toBe(true) expect(isArbitraryPosition('[position:bla]')).toBe(true) + expect(isArbitraryPosition('[percentage:bla]')).toBe(true) expect(isArbitraryPosition('[2px]')).toBe(false) expect(isArbitraryPosition('[bla]')).toBe(false) @@ -120,11 +121,11 @@ test('isArbitrarySize', () => { expect(isArbitrarySize('[size:2px]')).toBe(true) expect(isArbitrarySize('[size:bla]')).toBe(true) expect(isArbitrarySize('[length:bla]')).toBe(true) - expect(isArbitrarySize('[percentage:bla]')).toBe(true) expect(isArbitrarySize('[2px]')).toBe(false) expect(isArbitrarySize('[bla]')).toBe(false) expect(isArbitrarySize('size:2px')).toBe(false) + expect(isArbitrarySize('[percentage:bla]')).toBe(false) }) test('isArbitraryValue', () => { @@ -187,6 +188,7 @@ test('isArbitraryVariablePosition', () => { expect(isArbitraryVariablePosition('(other:test)')).toBe(false) expect(isArbitraryVariablePosition('(test)')).toBe(false) expect(isArbitraryVariablePosition('position:test')).toBe(false) + expect(isArbitraryVariablePosition('percentage:test')).toBe(false) }) test('isArbitraryVariableShadow', () => { @@ -200,11 +202,11 @@ test('isArbitraryVariableShadow', () => { test('isArbitraryVariableSize', () => { expect(isArbitraryVariableSize('(size:test)')).toBe(true) expect(isArbitraryVariableSize('(length:test)')).toBe(true) - expect(isArbitraryVariableSize('(percentage:test)')).toBe(true) expect(isArbitraryVariableSize('(other:test)')).toBe(false) expect(isArbitraryVariableSize('(test)')).toBe(false) expect(isArbitraryVariableSize('size:test')).toBe(false) + expect(isArbitraryVariableSize('(percentage:test)')).toBe(false) }) test('isFraction', () => {