From d950a8d7360d2fbe890a84972caba31e7c0f2585 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:09:10 +0100 Subject: [PATCH 01/33] add support for aspect-ratio and shadow-color --- src/default-config.ts | 10 ++++++++++ tests/class-map.test.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/default-config.ts b/src/default-config.ts index 72a8034..f62da72 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -245,6 +245,11 @@ export function getDefaultConfig() { }, classGroups: { // Layout + /** + * Aspect Ratio + * @see https://tailwindcss.com/docs/aspect-ratio + */ + aspect: [{ aspect: ['auto', 'square', 'video', isCustomValue] }], /** * Container * @see https://tailwindcss.com/docs/container @@ -1093,6 +1098,11 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/box-shadow */ shadow: [{ shadow: ['', ...getSizesSimple(), 'inner', 'none'] }], + /** + * Box Shadow Color + * @see https://tailwindcss.com/docs/box-shadow-color + */ + 'shadow-color': [{ shadow: [isAny] }], /** * Opacity * @see https://tailwindcss.com/docs/opacity diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 61b83e9..efcffe7 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -17,6 +17,7 @@ test('class map has correct class groups at first part', () => { expect(classMap.validators).toHaveLength(0) expect(classGroupsByFirstPart).toEqual({ absolute: ['position'], + aspect: ['aspect'], align: ['vertival-alignment'], animate: ['animate'], antialiased: ['font-smoothing'], @@ -182,7 +183,7 @@ test('class map has correct class groups at first part', () => { select: ['select'], self: ['align-self'], sepia: ['sepia'], - shadow: ['shadow'], + shadow: ['shadow', 'shadow-color'], skew: ['skew-x', 'skew-y'], slashed: ['fvn-slashed-zero'], space: ['space-x', 'space-x-reverse', 'space-y', 'space-y-reverse'], From fec2b18870f6806e602009632b52b9fe89ebfb83 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:12:08 +0100 Subject: [PATCH 02/33] add support for columns and isTshirtSize validator --- src/default-config.ts | 43 +++++++++++++++++++---------------------- src/validators.ts | 9 +++++++-- tests/class-map.test.ts | 1 + 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index f62da72..4f57f5f 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1,5 +1,12 @@ import { fromTheme } from './from-theme' -import { isAny, isCustomLength, isCustomValue, isInteger, isLength } from './validators' +import { + isAny, + isCustomLength, + isCustomValue, + isInteger, + isLength, + isTshirtSize, +} from './validators' export function getDefaultConfig() { const colors = fromTheme('colors') @@ -26,8 +33,6 @@ export function getDefaultConfig() { const space = fromTheme('space') const translate = fromTheme('translate') - const getSizesSimple = () => ['sm', 'md', 'lg', 'xl', '2xl'] as const - const getSizesExtended = () => ['3xl', '4xl', '5xl', '6xl', '7xl'] as const const getOverscroll = () => ['auto', 'contain', 'none'] as const const getOverflow = () => ['auto', 'hidden', 'visible', 'scroll'] as const const getSpacingWithAuto = () => ['auto', spacing] as const @@ -77,10 +82,10 @@ export function getDefaultConfig() { theme: { colors: [isAny], spacing: [isLength], - blur: ['none', '', ...getSizesSimple(), '3xl', isCustomLength], + blur: ['none', '', isTshirtSize, isCustomLength], brightness: [isInteger], borderColor: [colors], - borderRadius: ['none', '', ...getSizesSimple(), '3xl', 'full', isCustomLength], + borderRadius: ['none', '', 'full', isTshirtSize, isCustomLength], borderWidth: getLengthWithEmpty(), contrast: [isInteger], grayscale: getZeroAndEmpty(), @@ -255,6 +260,11 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/container */ container: ['container'], + /** + * Columns + * @see https://tailwindcss.com/docs/columns + */ + columns: [{ columns: [isTshirtSize] }], /** * Box Decoration Break * @see https://tailwindcss.com/docs/box-decoration-break @@ -654,13 +664,12 @@ export function getDefaultConfig() { 'max-w': [ '0', 'none', - ...getSizesSimple(), - ...getSizesExtended(), 'full', 'min', 'max', 'prose', - { screen: getSizesSimple() }, + { screen: [isTshirtSize] }, + isTshirtSize, isCustomLength, ], }, @@ -690,19 +699,7 @@ export function getDefaultConfig() { * Font Size * @see https://tailwindcss.com/docs/font-size */ - 'font-size': [ - { - text: [ - 'xs', - ...getSizesSimple(), - 'base', - ...getSizesExtended(), - '8xl', - '9xl', - isCustomLength, - ], - }, - ], + 'font-size': [{ text: ['base', isTshirtSize, isCustomLength] }], /** * Font Smoothing * @see https://tailwindcss.com/docs/font-smoothing @@ -1097,7 +1094,7 @@ export function getDefaultConfig() { * Box Shadow * @see https://tailwindcss.com/docs/box-shadow */ - shadow: [{ shadow: ['', ...getSizesSimple(), 'inner', 'none'] }], + shadow: [{ shadow: ['', 'inner', 'none', isTshirtSize] }], /** * Box Shadow Color * @see https://tailwindcss.com/docs/box-shadow-color @@ -1138,7 +1135,7 @@ export function getDefaultConfig() { * Drop Shadow * @see https://tailwindcss.com/docs/drop-shadow */ - 'drop-shadow': [{ 'drop-shadow': ['', ...getSizesSimple(), 'none'] }], + 'drop-shadow': [{ 'drop-shadow': ['', 'none', isTshirtSize] }], /** * Grayscale * @see https://tailwindcss.com/docs/grayscale diff --git a/src/validators.ts b/src/validators.ts index 17a84e0..33cf524 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -1,14 +1,15 @@ const customValueRegex = /^\[(.+)\]$/ const fractionRegex = /^\d+\/\d+$/ const stringLengths = new Set(['px', 'full', 'screen']) +const tshirtUnitRegex = /^(\d+)?(xs|sm|md|lg|xl)$/ const lengthUnitRegex = /\d+(%|px|em|rem|vh|vw|pt|pc|in|cm|mm|cap|ch|ex|lh|rlh|vi|vb|vmin|vmax)/ export function isLength(classPart: string) { return ( - isCustomLength(classPart) || !Number.isNaN(Number(classPart)) || stringLengths.has(classPart) || - fractionRegex.test(classPart) + fractionRegex.test(classPart) || + isCustomLength(classPart) ) } @@ -39,3 +40,7 @@ export function isCustomValue(classPart: string) { export function isAny() { return true } + +export function isTshirtSize(classPart: string) { + return tshirtUnitRegex.test(classPart) +} diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index efcffe7..3c4afc0 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -72,6 +72,7 @@ test('class map has correct class groups at first part', () => { caret: ['caret-color'], clear: ['clear'], col: ['col-end', 'col-start', 'col-start-end'], + columns: ['columns'], container: ['container'], content: ['align-content', 'content'], contents: ['display'], From 6c78c4ceca39bee21482a553642f4f91ab77bdf2 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:59:35 +0100 Subject: [PATCH 03/33] add tsts for isTshirtSize validator --- tests/public-api.test.ts | 2 ++ tests/validators.test.ts | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index d6851ac..61c7ac7 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -19,6 +19,7 @@ test('has correct export types', () => { isInteger: expect.any(Function), isCustomValue: expect.any(Function), isAny: expect.any(Function), + isTshirtSize: expect.any(Function), }) expect(mergeConfigs).toStrictEqual(expect.any(Function)) expect(extendTailwindMerge).toStrictEqual(expect.any(Function)) @@ -119,6 +120,7 @@ test('validators have correct inputs and outputs', () => { expect(validators.isInteger('')).toEqual(expect.any(Boolean)) expect(validators.isCustomValue('')).toEqual(expect.any(Boolean)) expect(validators.isAny()).toEqual(expect.any(Boolean)) + expect(validators.isTshirtSize('')).toEqual(expect.any(Boolean)) }) test('mergeConfigs has correct inputs and outputs', () => { diff --git a/tests/validators.test.ts b/tests/validators.test.ts index f06b2f2..8dc3588 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -1,6 +1,6 @@ import { validators } from '../src' -const { isLength, isCustomLength, isInteger, isCustomValue, isAny } = validators +const { isLength, isCustomLength, isInteger, isCustomValue, isAny, isTshirtSize } = validators describe('validators', () => { test('isLength', () => { @@ -81,4 +81,24 @@ describe('validators', () => { // @ts-expect-error expect(isAny('something')).toBe(true) }) + + test('isTshirtSize', () => { + expect(isTshirtSize('xs')).toBe(true) + expect(isTshirtSize('sm')).toBe(true) + expect(isTshirtSize('md')).toBe(true) + expect(isTshirtSize('lg')).toBe(true) + expect(isTshirtSize('xl')).toBe(true) + expect(isTshirtSize('2xl')).toBe(true) + expect(isTshirtSize('10xl')).toBe(true) + expect(isTshirtSize('2xs')).toBe(true) + expect(isTshirtSize('2lg')).toBe(true) + + expect(isTshirtSize('')).toBe(false) + expect(isTshirtSize('hello')).toBe(false) + expect(isTshirtSize('1')).toBe(false) + expect(isTshirtSize('xl3')).toBe(false) + expect(isTshirtSize('2xl3')).toBe(false) + expect(isTshirtSize('-xl')).toBe(false) + expect(isTshirtSize('[sm]')).toBe(false) + }) }) From 96b8b3a1909043808765dc7dfbc87e0f7a6b2753 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:04:04 +0100 Subject: [PATCH 04/33] add isTshirtSize to documentation --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 68fa9c1..02dc245 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,7 @@ interface Validators { isCustomLength(classPart: string): boolean isInteger(classPart: string): boolean isCustomValue(classPart: string): boolean + isTshirtSize(classPart: string): boolean isAny(classPart: string): boolean } ``` @@ -525,6 +526,7 @@ A brief summary for each validator: - `isCustomLength` checks for custom length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`). - `isInteger` checks for integer values (`3`) and custom integer values (`[3]`). - `isCustomValue` checks whether the class part is enclosed in brackets (`[something]`) +- `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`) - `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm ceertain there are no other class groups in a namespace. ### `Config` From 5baf4081c8f85ce3d3a484028c8737dc98364872 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:06:57 +0100 Subject: [PATCH 05/33] remove outcommented code in default config --- src/default-config.ts | 144 ------------------------------------------ 1 file changed, 144 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 4f57f5f..39e8889 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -103,150 +103,6 @@ export function getDefaultConfig() { skew: [isInteger], space: [spacing], translate: [spacing], - - // Tailwind theme keys not in use because they would apply only to a single classGroup: - - // animation: ['none', 'spin', 'ping', 'pulse', 'bounce', isCustomValue], - // backdropBlur: [blur], - // backdropBrightness: [brightness], - // backdropContrast: [contrast], - // backdropGrayscale: [grayscale], - // backdropHueRotate: [hueRotate], - // backdropInvert: [invert], - // backdropOpacity: [opacity], - // backdropSaturate: [saturate], - // backdropSepia: [sepia], - // backgroundColor: [colors], - // backgroundImage: [ - // 'none', - // { 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] }, - // ], - // backgroundOpacity: [opacity], - // backgroundPosition: getPositions(), - // backgroundSize: ['auto', 'cover', 'contain'], - // borderOpacity: [opacity], - // boxShadow: ['', ...getSizesSimple(), 'inner', 'none'], - // caretColor: [colors], - // container: ['container'], - // content: [isCustomValue], - // cursor: [ - // 'auto', - // 'default', - // 'pointer', - // 'wait', - // 'text', - // 'move', - // 'help', - // 'not-allowed', - // isCustomValue, - // ], - // divideColor: [borderColor], - // divideOpacity: [borderOpacity], - // divideWidth: [borderWidth], - // dropShadow: ['', ...getSizesSimple(), 'none'], - // fill: ['current', isCustomValue], - // flex: ['1', 'auto', 'initial', 'none', isCustomValue], - // flexGrow: getZeroAndEmpty(), - // flexShrink: getZeroAndEmpty(), - // fontFamily: [isAny], - // fontSize: [ - // 'xs', - // ...getSizesSimple(), - // 'base', - // ...getSizesExtended(), - // '8xl', - // '9xl', - // isCustomLength, - // ], - // fontWeight: [ - // 'thin', - // 'extralight', - // 'light', - // 'normal', - // 'medium', - // 'semibold', - // 'bold', - // 'extrabold', - // 'black', - // ], - // gridAutoColumns: ['auto', 'min', 'max', 'fr', isCustomValue], - // gridAutoRows: ['auto', 'min', 'max', 'fr', isCustomValue], - // gridColumn: ['auto', { span: [isInteger] }], - // gridColumnEnd: getIntegerWithAuto(), - // gridColumnStart: getIntegerWithAuto(), - // gridRow: ['auto', { span: [isInteger] }], - // gridRowStart: getIntegerWithAuto(), - // gridRowEnd: getIntegerWithAuto(), - // gridTemplateColumns: [isAny], - // gridTemplateRows: [isAny], - // height: getSpacingWithAuto(), - // letterSpacing: [ - // 'tighter', - // 'tight', - // 'normal', - // 'wide', - // 'wider', - // 'widest', - // isCustomLength, - // ], - // lineHeight: ['none', 'tight', 'snug', 'normal', 'relaxed', 'loose', isLength], - // listStyleType: ['none', 'disc', 'decimal', isCustomValue], - // maxHeight: [spacing], - // maxWidth: [ - // '0', - // 'none', - // ...getSizesSimple(), - // ...getSizesExtended(), - // 'full', - // 'min', - // 'max', - // 'prose', - // { screen: getSizesSimple() }, - // isCustomLength, - // ], - // minHeight: ['full', 'screen', isLength], - // minWidth: ['full', 'min', 'max', isLength], - // objectPosition: getPositions(), - // order: ['first', 'last', 'none', isInteger], - // outline: ['none', 'white', 'black'], - // placeholderColor: [colors], - // placeholderOpacity: [opacity], - // ringColor: [colors], - // ringOffsetColor: [colors], - // ringOffsetWidth: [isLength], - // ringOpacity: [opacity], - // ringWidth: getLengthWithEmpty(), - // rotate: [isInteger], - // stroke: ['current', isCustomValue], - // strokeWidth: [isLength], - // textColor: [colors], - // textOpacity: [opacity], - // transformOrigin: [ - // 'center', - // 'top', - // 'top-right', - // 'right', - // 'bottom-right', - // 'bottom', - // 'bottom-left', - // 'left', - // 'top-left', - // ], - // transitionDelay: [isInteger], - // transitionDuration: [isInteger], - // transitionProperty: [ - // 'none', - // 'all', - // '', - // 'colors', - // 'opacity', - // 'shadow', - // 'transform', - // isCustomValue, - // ], - // transitionTimingFunction: ['linear', 'in', 'out', 'in-out', isCustomValue], - // width: ['auto', 'min', 'max', spacing], - // zIndex: [isLength], }, classGroups: { // Layout From 739f4d7738fb956a7fca51feaed82ef7467a4feb Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:14:00 +0100 Subject: [PATCH 06/33] add support for break-after, break-before and break-inside utilities --- src/default-config.ts | 17 +++++++++++++++++ tests/class-map.test.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/default-config.ts b/src/default-config.ts index 39e8889..aba8db7 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -76,6 +76,8 @@ export function getDefaultConfig() { ] as const const getAlign = () => ['start', 'end', 'center', 'between', 'around', 'evenly'] as const const getZeroAndEmpty = () => ['', '0'] as const + const getBreaks = () => + ['auto', 'avoid', 'all', 'avoid-page', 'page', 'left', 'right', 'column'] as const return { cacheSize: 500, @@ -121,6 +123,21 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/columns */ columns: [{ columns: [isTshirtSize] }], + /** + * Break After + * @see https://tailwindcss.com/docs/break-after + */ + 'break-after': [{ 'break-after': getBreaks() }], + /** + * Break Before + * @see https://tailwindcss.com/docs/break-before + */ + 'break-before': [{ 'break-before': getBreaks() }], + /** + * Break Inside + * @see https://tailwindcss.com/docs/break-inside + */ + 'break-inside': [{ 'break-before': ['auto', 'avoid', 'avoid-page', 'avoid-column'] }], /** * Box Decoration Break * @see https://tailwindcss.com/docs/box-decoration-break diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 3c4afc0..b5de9c8 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -66,7 +66,7 @@ test('class map has correct class groups at first part', () => { ], bottom: ['bottom'], box: ['box'], - break: ['break'], + break: ['break', 'break-after', 'break-before', 'break-inside'], brightness: ['brightness'], capitalize: ['text-transform'], caret: ['caret-color'], From 01772903ecfdcabb192d2f395abf583a1ac9c301 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:16:55 +0100 Subject: [PATCH 07/33] add support for text-indent utility --- src/default-config.ts | 5 +++++ tests/class-map.test.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index aba8db7..accd8ce 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -706,6 +706,11 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/text-overflow */ 'text-overflow': ['truncate', 'overflow-ellipsis', 'overflow-clip'], + /** + * Text Indent + * @see https://tailwindcss.com/docs/text-indent + */ + indent: [{ indent: [fromTheme('spacing')] }], /** * Vertical Alignment * @see https://tailwindcss.com/docs/vertical-align diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index b5de9c8..af12d21 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -107,6 +107,7 @@ test('class map has correct class groups at first part', () => { h: ['h'], hidden: ['display'], hue: ['hue-rotate'], + indent: ['indent'], inline: ['display'], inset: ['inset', 'inset-x', 'inset-y'], invert: ['invert'], From a0999e01c52570558212835e4653fe51d608d2cb Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:35:43 +0100 Subject: [PATCH 08/33] add support for text-decoration utilities --- src/default-config.ts | 21 ++++++++++++++++++--- tests/class-map.test.ts | 7 ++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index accd8ce..3f73a15 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -50,7 +50,7 @@ export function getDefaultConfig() { 'right-top', 'top', ] as const - const getBorderStyles = () => ['solid', 'dashed', 'dotted', 'double', 'none'] as const + const getLineStyles = () => ['solid', 'dashed', 'dotted', 'double', 'none'] as const const getBlendModes = () => [ { @@ -696,6 +696,21 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/text-decoration */ 'text-decoration': ['underline', 'line-through', 'no-underline'], + /** + * Text Decoration Style + * @see https://tailwindcss.com/docs/text-decoration-style + */ + 'text-decoration-style': [{ decoration: [...getLineStyles(), 'wavy'] }], + /** + * Text Decoration Thickness + * @see https://tailwindcss.com/docs/text-decoration-thickness + */ + 'text-decoration-thickness': [{ decoration: ['auto', 'from-font', isLength] }], + /** + * Text Decoration Color + * @see https://tailwindcss.com/docs/text-decoration-color + */ + 'text-decoration-color': [{ decoration: [fromTheme('colors')] }], /** * Text Transform * @see https://tailwindcss.com/docs/text-transform @@ -876,7 +891,7 @@ export function getDefaultConfig() { * Border Style * @see https://tailwindcss.com/docs/border-style */ - 'border-style': [{ border: getBorderStyles() }], + 'border-style': [{ border: getLineStyles() }], /** * Divide Width X * @see https://tailwindcss.com/docs/divide-width @@ -906,7 +921,7 @@ export function getDefaultConfig() { * Divide Style * @see https://tailwindcss.com/docs/divide-style */ - 'divide-style': [{ divide: getBorderStyles() }], + 'divide-style': [{ divide: getLineStyles() }], /** * Border Color * @see https://tailwindcss.com/docs/border-color diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index af12d21..f887e36 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -78,7 +78,12 @@ test('class map has correct class groups at first part', () => { contents: ['display'], contrast: ['contrast'], cursor: ['cursor'], - decoration: ['decoration'], + decoration: [ + 'decoration', + 'text-decoration-color', + 'text-decoration-style', + 'text-decoration-thickness', + ], delay: ['delay'], diagonal: ['fvn-fraction'], divide: [ From 55ab167b7167519873c5dd4d258dc62212d1659a Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:50:21 +0100 Subject: [PATCH 09/33] breaking: add support for outline utilities --- src/default-config.ts | 21 ++++++++++++++++++--- tests/class-map.test.ts | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 3f73a15..bf88ba8 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1245,10 +1245,25 @@ export function getDefaultConfig() { }, ], /** - * Outline - * @see https://tailwindcss.com/docs/outline + * Outline Width + * @see https://tailwindcss.com/docs/outline-width */ - outline: [{ outline: ['none', 'white', 'black'] }], + 'outline-w': [{ outline: [isLength] }], + /** + * Outline Style + * @see https://tailwindcss.com/docs/outline-style + */ + 'outline-style': [{ outline: ['', ...getLineStyles(), 'hidden'] }], + /** + * Outline Offset + * @see https://tailwindcss.com/docs/outline-offset + */ + 'outline-offset': [{ 'outline-offset': [isLength] }], + /** + * Outline Color + * @see https://tailwindcss.com/docs/outline-color + */ + 'outline-color': [{ outline: [fromTheme('colors')] }], /** * Pointer Events * @see https://tailwindcss.com/docs/pointer-events diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index f887e36..9c4ea25 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -147,7 +147,7 @@ test('class map has correct class groups at first part', () => { order: ['order'], ordinal: ['fvn-ordinal'], origin: ['transform-origin'], - outline: ['outline'], + outline: ['outline-color', 'outline-offset', 'outline-style', 'outline-w'], overflow: ['overflow', 'overflow-x', 'overflow-y', 'text-overflow'], overscroll: ['overscroll', 'overscroll-x', 'overscroll-y'], p: ['p'], From 8c07248a3a3b3371b629ce8ff032c8096bda92b9 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:58:34 +0100 Subject: [PATCH 10/33] update Tailwind version support text in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02dc245..48d95ef 100644 --- a/README.md +++ b/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 v2.0 up to v2.2, support for newer versions will be added continuously +- Supports Tailwind v3.0 (if you use Tailwind v2, check the [tailwind-merge v1.0.0 release notes](https://github.com/dcastil/tailwind-merge/releases/tag/v1.0.0) to figure out which version of tailwind-merge to use) - Works in Node >=12 and all modern browsers - Fully typed - [5.2 kB minified + gzipped](https://bundlephobia.com/package/tailwind-merge) (96.7% self, 3.3% hashlru) From 1269ce68ae39807ceadbecc98c0929fdfdb446d0 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:03:17 +0100 Subject: [PATCH 11/33] breaking: add support for new vertical-align utilities --- src/default-config.ts | 15 +++++++++++++-- tests/class-map.test.ts | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index bf88ba8..6d86d54 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -730,8 +730,19 @@ export function getDefaultConfig() { * Vertical Alignment * @see https://tailwindcss.com/docs/vertical-align */ - 'vertival-alignment': [ - { align: ['baseline', 'top', 'middle', 'bottom', 'text-top', 'text-bottom'] }, + 'vertical-align': [ + { + align: [ + 'baseline', + 'top', + 'middle', + 'bottom', + 'text-top', + 'text-bottom', + 'sub', + 'super', + ], + }, ], /** * Whitespace diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 9c4ea25..d76ddfe 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -18,7 +18,7 @@ test('class map has correct class groups at first part', () => { expect(classGroupsByFirstPart).toEqual({ absolute: ['position'], aspect: ['aspect'], - align: ['vertival-alignment'], + align: ['vertical-align'], animate: ['animate'], antialiased: ['font-smoothing'], appearance: ['appearance'], From 1437de60786ae686008b1038143d814c47d4b033 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:10:09 +0100 Subject: [PATCH 12/33] add support for accent-color utilities --- src/default-config.ts | 11 ++++++++--- tests/class-map.test.ts | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 6d86d54..3bb7dbb 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -710,7 +710,7 @@ export function getDefaultConfig() { * Text Decoration Color * @see https://tailwindcss.com/docs/text-decoration-color */ - 'text-decoration-color': [{ decoration: [fromTheme('colors')] }], + 'text-decoration-color': [{ decoration: [colors] }], /** * Text Transform * @see https://tailwindcss.com/docs/text-transform @@ -725,7 +725,7 @@ export function getDefaultConfig() { * Text Indent * @see https://tailwindcss.com/docs/text-indent */ - indent: [{ indent: [fromTheme('spacing')] }], + indent: [{ indent: [spacing] }], /** * Vertical Alignment * @see https://tailwindcss.com/docs/vertical-align @@ -1231,6 +1231,11 @@ export function getDefaultConfig() { */ 'skew-y': [{ 'skew-y': [skew] }], // Interactivity + /** + * Accent Color + * @see https://tailwindcss.com/docs/accent-color + */ + accent: [{ accent: ['auto', colors] }], /** * Appearance * @see https://tailwindcss.com/docs/appearance @@ -1274,7 +1279,7 @@ export function getDefaultConfig() { * Outline Color * @see https://tailwindcss.com/docs/outline-color */ - 'outline-color': [{ outline: [fromTheme('colors')] }], + 'outline-color': [{ outline: [colors] }], /** * Pointer Events * @see https://tailwindcss.com/docs/pointer-events diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index d76ddfe..eb67ec2 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -17,6 +17,7 @@ test('class map has correct class groups at first part', () => { expect(classMap.validators).toHaveLength(0) expect(classGroupsByFirstPart).toEqual({ absolute: ['position'], + accent: ['accent'], aspect: ['aspect'], align: ['vertical-align'], animate: ['animate'], From ebe96d3d2772ef8e81b3acaad28313b239084c1f Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:18:02 +0100 Subject: [PATCH 13/33] remove early development section from documentation --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 48d95ef..55ac0eb 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,6 @@ twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') - Fully typed - [5.2 kB minified + gzipped](https://bundlephobia.com/package/tailwind-merge) (96.7% self, 3.3% hashlru) -## Early development - -This library is in an early pre-v1 development stage and might have some bugs and inconveniences here and there. I use the library in production and intend it to be sufficient for production use, as long as you're fine with some potential breaking changes in minor releases until v1 (lock the version range to patch releases with `~` in your `package.json` to prevent accidental breaking changes). - -I want to keep the library on v0 until I feel confident enough that there aren't any major bugs or flaws in its API and implementation. If you find a bug or something you don't like, please [submit an issue](https://github.com/dcastil/tailwind-merge/issues/new/choose) or a pull request. I'm happy about any kind of feedback! - ## What is it for If you use Tailwind with a component-based UI renderer like [React](https://reactjs.org) or [Vue](https://vuejs.org), you're probably familiar with the situation that you want to change some styles of a component, but only in one place. From 24ff3ed40154484899a5b9b31a5d4696b92a0ca7 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:34:50 +0100 Subject: [PATCH 14/33] add support for scroll-snap utilities --- src/default-config.ts | 110 ++++++++++++++++++++++++++++++++++++++++ tests/class-map.test.ts | 17 +++++++ 2 files changed, 127 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index 3bb7dbb..fe2ad25 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1290,6 +1290,96 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/resize */ resize: [{ resize: ['none', 'y', 'x', ''] }], + /** + * Scroll Margin + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-m': [{ 'scroll-m': [spacing] }], + /** + * Scroll Margin X + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mx': [{ 'scroll-mx': [spacing] }], + /** + * Scroll Margin Y + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-my': [{ 'scroll-my': [spacing] }], + /** + * Scroll Margin Top + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mt': [{ 'scroll-mt': [spacing] }], + /** + * Scroll Margin Right + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mr': [{ 'scroll-mr': [spacing] }], + /** + * Scroll Margin Bottom + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-mb': [{ 'scroll-mb': [spacing] }], + /** + * Scroll Margin Left + * @see https://tailwindcss.com/docs/scroll-margin + */ + 'scroll-ml': [{ 'scroll-ml': [spacing] }], + /** + * Scroll Padding + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-p': [{ 'scroll-p': [spacing] }], + /** + * Scroll Padding X + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-px': [{ 'scroll-px': [spacing] }], + /** + * Scroll Padding Y + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-py': [{ 'scroll-py': [spacing] }], + /** + * Scroll Padding Top + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pt': [{ 'scroll-pt': [spacing] }], + /** + * Scroll Padding Right + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pr': [{ 'scroll-pr': [spacing] }], + /** + * Scroll Padding Bottom + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pb': [{ 'scroll-pb': [spacing] }], + /** + * Scroll Padding Left + * @see https://tailwindcss.com/docs/scroll-padding + */ + 'scroll-pl': [{ 'scroll-pl': [spacing] }], + /** + * Scroll Snap Align + * @see https://tailwindcss.com/docs/scroll-snap-align + */ + 'snap-align': [{ snap: ['start', 'end', 'center', 'align-none'] }], + /** + * Scroll Snap Stop + * @see https://tailwindcss.com/docs/scroll-snap-stop + */ + 'snap-stop': [{ snap: ['normal', 'always'] }], + /** + * Scroll Snap Type + * @see https://tailwindcss.com/docs/scroll-snap-type + */ + 'snap-type': [{ snap: ['none', 'x', 'y', 'both'] }], + /** + * Scroll Snap Type Strictness + * @see https://tailwindcss.com/docs/scroll-snap-type + */ + 'snap-strictness': [{ snap: ['mandatory', 'proximity'] }], /** * Select * @see https://tailwindcss.com/docs/select @@ -1379,6 +1469,26 @@ export function getDefaultConfig() { 'border-color-b', 'border-color-l', ], + 'scroll-m': [ + 'scroll-mx', + 'scroll-my', + 'scroll-mt', + 'scroll-mr', + 'scroll-mb', + 'scroll-ml', + ], + 'scroll-mx': ['scroll-mr', 'scroll-ml'], + 'scroll-my': ['scroll-mt', 'scroll-mb'], + 'scroll-p': [ + 'scroll-px', + 'scroll-py', + 'scroll-pt', + 'scroll-pr', + 'scroll-pb', + 'scroll-pl', + ], + 'scroll-px': ['scroll-pr', 'scroll-pl'], + 'scroll-py': ['scroll-pt', 'scroll-pb'], }, } as const } diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index eb67ec2..9703097 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -188,7 +188,24 @@ test('class map has correct class groups at first part', () => { row: ['row-end', 'row-start', 'row-start-end'], saturate: ['saturate'], scale: ['scale', 'scale-x', 'scale-y'], + scroll: [ + 'scroll-m', + 'scroll-mb', + 'scroll-ml', + 'scroll-mr', + 'scroll-mt', + 'scroll-mx', + 'scroll-my', + 'scroll-p', + 'scroll-pb', + 'scroll-pl', + 'scroll-pr', + 'scroll-pt', + 'scroll-px', + 'scroll-py', + ], select: ['select'], + snap: ['snap-align', 'snap-stop', 'snap-strictness', 'snap-type'], self: ['align-self'], sepia: ['sepia'], shadow: ['shadow', 'shadow-color'], From 10bd1c2e360c2d72df85e6e9f93d1f16d03291ee Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:30:22 +0100 Subject: [PATCH 15/33] add support for scroll-behavior utilities --- src/default-config.ts | 5 +++++ tests/class-map.test.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index fe2ad25..4045007 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -716,6 +716,11 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/text-transform */ 'text-transform': ['uppercase', 'lowercase', 'capitalize', 'normal-case'], + /** + * Scroll Behavior + * @see https://github.com/tailwindlabs/tailwindcss.com/issues/1016 + */ + 'scroll-behavior': [{ scroll: ['smooth', 'auto'] }], /** * Text Overflow * @see https://tailwindcss.com/docs/text-overflow diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 9703097..a47df0c 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -189,6 +189,7 @@ test('class map has correct class groups at first part', () => { saturate: ['saturate'], scale: ['scale', 'scale-x', 'scale-y'], scroll: [ + 'scroll-behavior', 'scroll-m', 'scroll-mb', 'scroll-ml', From 1bb21775dfd32cf1bf6781dff8fa09b6e6998e78 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:34:25 +0100 Subject: [PATCH 16/33] add support for touch-action utilities --- src/default-config.ts | 19 +++++++++++++++++-- tests/class-map.test.ts | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 4045007..85caf54 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1386,8 +1386,23 @@ export function getDefaultConfig() { */ 'snap-strictness': [{ snap: ['mandatory', 'proximity'] }], /** - * Select - * @see https://tailwindcss.com/docs/select + * Touch Action + * @see https://tailwindcss.com/docs/touch-action + */ + touch: [ + { + touch: [ + 'auto', + 'none', + 'pinch-zoom', + 'manipulation', + { pan: ['x', 'left', 'right', 'y', 'up', 'down'] }, + ], + }, + ], + /** + * User Select + * @see https://tailwindcss.com/docs/user-select */ select: [{ select: ['none', 'text', 'all', 'auto'] }], // SVG diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index a47df0c..58ad468 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -224,6 +224,7 @@ test('class map has correct class groups at first part', () => { text: ['font-size', 'text-alignment', 'text-color', 'text-opacity'], to: ['gradient-to'], top: ['top'], + touch: ['touch'], tracking: ['tracking'], transform: ['transform'], transition: ['transition'], From 8d39da8a0353df222f8b664ed2378b6b3a02e726 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:42:04 +0100 Subject: [PATCH 17/33] add support for flex-basis utilities --- src/default-config.ts | 7 ++++++- tests/class-map.test.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/default-config.ts b/src/default-config.ts index 85caf54..d896f06 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -281,6 +281,11 @@ export function getDefaultConfig() { */ z: [{ z: [isLength] }], // Flexbox and Grid + /** + * Flex Basis + * @see https://tailwindcss.com/docs/flex-basis + */ + 'flex-basis': [{ basis: [spacing] }], /** * Flex Direction * @see https://tailwindcss.com/docs/flex-direction @@ -1445,7 +1450,7 @@ export function getDefaultConfig() { inset: ['inset-x', 'inset-y', 'top', 'right', 'bottom', 'left'], 'inset-x': ['right', 'left'], 'inset-y': ['top', 'bottom'], - flex: ['flex-grow', 'flex-shrink'], + flex: ['flex-basis', 'flex-grow', 'flex-shrink'], 'col-start-end': ['col-start', 'col-end'], 'row-start-end': ['row-start', 'row-end'], gap: ['gap-x', 'gap-y'], diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 58ad468..6b9f28f 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -70,6 +70,7 @@ test('class map has correct class groups at first part', () => { break: ['break', 'break-after', 'break-before', 'break-inside'], brightness: ['brightness'], capitalize: ['text-transform'], + basis: ['flex-basis'], caret: ['caret-color'], clear: ['clear'], col: ['col-end', 'col-start', 'col-start-end'], From e6d8912e47bf9a89346b9b0cc822fb2bff2af172 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:46:20 +0100 Subject: [PATCH 18/33] breaking: use grow and shrink instead of flex-grow and flex-shrink utilities --- src/default-config.ts | 8 ++++---- tests/class-map.test.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index d896f06..a0b028b 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -285,7 +285,7 @@ export function getDefaultConfig() { * Flex Basis * @see https://tailwindcss.com/docs/flex-basis */ - 'flex-basis': [{ basis: [spacing] }], + basis: [{ basis: [spacing] }], /** * Flex Direction * @see https://tailwindcss.com/docs/flex-direction @@ -305,12 +305,12 @@ export function getDefaultConfig() { * Flex Grow * @see https://tailwindcss.com/docs/flex-grow */ - 'flex-grow': [{ 'flex-grow': getZeroAndEmpty() }], + grow: [{ grow: getZeroAndEmpty() }], /** * Flex Shrink * @see https://tailwindcss.com/docs/flex-shrink */ - 'flex-shrink': [{ 'flex-shrink': getZeroAndEmpty() }], + shrink: [{ shrink: getZeroAndEmpty() }], /** * Order * @see https://tailwindcss.com/docs/order @@ -1450,7 +1450,7 @@ export function getDefaultConfig() { inset: ['inset-x', 'inset-y', 'top', 'right', 'bottom', 'left'], 'inset-x': ['right', 'left'], 'inset-y': ['top', 'bottom'], - flex: ['flex-basis', 'flex-grow', 'flex-shrink'], + flex: ['basis', 'grow', 'shrink'], 'col-start-end': ['col-start', 'col-end'], 'row-start-end': ['row-start', 'row-end'], gap: ['gap-x', 'gap-y'], diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 6b9f28f..93694ff 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -18,11 +18,11 @@ test('class map has correct class groups at first part', () => { expect(classGroupsByFirstPart).toEqual({ absolute: ['position'], accent: ['accent'], - aspect: ['aspect'], align: ['vertical-align'], animate: ['animate'], antialiased: ['font-smoothing'], appearance: ['appearance'], + aspect: ['aspect'], auto: ['auto-cols', 'auto-rows'], backdrop: [ 'backdrop-blur', @@ -36,6 +36,7 @@ test('class map has correct class groups at first part', () => { 'backdrop-saturate', 'backdrop-sepia', ], + basis: ['basis'], bg: [ 'bg-attachment', 'bg-blend', @@ -70,7 +71,6 @@ test('class map has correct class groups at first part', () => { break: ['break', 'break-after', 'break-before', 'break-inside'], brightness: ['brightness'], capitalize: ['text-transform'], - basis: ['flex-basis'], caret: ['caret-color'], clear: ['clear'], col: ['col-end', 'col-start', 'col-start-end'], @@ -103,7 +103,7 @@ test('class map has correct class groups at first part', () => { fill: ['fill'], filter: ['filter'], fixed: ['position'], - flex: ['display', 'flex', 'flex-direction', 'flex-grow', 'flex-shrink', 'flex-wrap'], + flex: ['display', 'flex', 'flex-direction', 'flex-wrap'], float: ['float'], flow: ['display'], font: ['font-family', 'font-weight'], @@ -111,6 +111,7 @@ test('class map has correct class groups at first part', () => { gap: ['gap', 'gap-x', 'gap-y'], grayscale: ['grayscale'], grid: ['display', 'grid-cols', 'grid-flow', 'grid-rows'], + grow: ['grow'], h: ['h'], hidden: ['display'], hue: ['hue-rotate'], @@ -207,12 +208,13 @@ test('class map has correct class groups at first part', () => { 'scroll-py', ], select: ['select'], - snap: ['snap-align', 'snap-stop', 'snap-strictness', 'snap-type'], self: ['align-self'], sepia: ['sepia'], shadow: ['shadow', 'shadow-color'], + shrink: ['shrink'], skew: ['skew-x', 'skew-y'], slashed: ['fvn-slashed-zero'], + snap: ['snap-align', 'snap-stop', 'snap-strictness', 'snap-type'], space: ['space-x', 'space-x-reverse', 'space-y', 'space-y-reverse'], sr: ['sr'], stacked: ['fvn-fraction'], From 7ba73346425234b237f49dda2a7a2c23ed272f42 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:53:00 +0100 Subject: [PATCH 19/33] add support for new border-width and border-color utilities --- src/default-config.ts | 24 ++++++++++++++++++++++++ tests/class-map.test.ts | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index a0b028b..6860c09 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -883,6 +883,16 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/border-width */ 'border-w': [{ border: [borderWidth] }], + /** + * Border Width X + * @see https://tailwindcss.com/docs/border-width + */ + 'border-w-x': [{ 'border-x': [borderWidth] }], + /** + * Border Width Y + * @see https://tailwindcss.com/docs/border-width + */ + 'border-w-y': [{ 'border-y': [borderWidth] }], /** * Border Width Top * @see https://tailwindcss.com/docs/border-width @@ -948,6 +958,16 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/border-color */ 'border-color': [{ border: [borderColor] }], + /** + * Border Color X + * @see https://tailwindcss.com/docs/border-color + */ + 'border-color-x': [{ 'border-x': [borderColor] }], + /** + * Border Color Y + * @see https://tailwindcss.com/docs/border-color + */ + 'border-color-y': [{ 'border-y': [borderColor] }], /** * Border Color Top * @see https://tailwindcss.com/docs/border-color @@ -1488,12 +1508,16 @@ export function getDefaultConfig() { 'rounded-b': ['rounded-br', 'rounded-bl'], 'rounded-l': ['rounded-tl', 'rounded-bl'], 'border-w': ['border-w-t', 'border-w-r', 'border-w-b', 'border-w-l'], + 'border-w-x': ['border-w-r', 'border-w-l'], + 'border-w-y': ['border-w-t', 'border-w-b'], 'border-color': [ 'border-color-t', 'border-color-r', 'border-color-b', 'border-color-l', ], + 'border-color-x': ['border-color-r', 'border-color-l'], + 'border-color-y': ['border-color-t', 'border-color-b'], 'scroll-m': [ 'scroll-mx', 'scroll-my', diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 93694ff..4637a56 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -58,6 +58,8 @@ test('class map has correct class groups at first part', () => { 'border-color-l', 'border-color-r', 'border-color-t', + 'border-color-x', + 'border-color-y', 'border-opacity', 'border-style', 'border-w', @@ -65,6 +67,8 @@ test('class map has correct class groups at first part', () => { 'border-w-l', 'border-w-r', 'border-w-t', + 'border-w-x', + 'border-w-y', ], bottom: ['bottom'], box: ['box'], From 03b71bfa90e0e5b76e6b12c65cd0c703d71ef29c Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:55:32 +0100 Subject: [PATCH 20/33] add border-hidden utility --- src/default-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/default-config.ts b/src/default-config.ts index 6860c09..f8de5a5 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -922,7 +922,7 @@ export function getDefaultConfig() { * Border Style * @see https://tailwindcss.com/docs/border-style */ - 'border-style': [{ border: getLineStyles() }], + 'border-style': [{ border: [...getLineStyles(), 'hidden'] }], /** * Divide Width X * @see https://tailwindcss.com/docs/divide-width From 65b03e48914ac5d7d52eea9ec178b204d30609c9 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 13:19:31 +0100 Subject: [PATCH 21/33] breaking: rename some text-overflow classes --- src/default-config.ts | 2 +- tests/class-map.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index f8de5a5..064c602 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -730,7 +730,7 @@ export function getDefaultConfig() { * Text Overflow * @see https://tailwindcss.com/docs/text-overflow */ - 'text-overflow': ['truncate', 'overflow-ellipsis', 'overflow-clip'], + 'text-overflow': ['truncate', 'text-ellipsis', 'text-clip'], /** * Text Indent * @see https://tailwindcss.com/docs/text-indent diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 4637a56..54d5fef 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -155,7 +155,7 @@ test('class map has correct class groups at first part', () => { ordinal: ['fvn-ordinal'], origin: ['transform-origin'], outline: ['outline-color', 'outline-offset', 'outline-style', 'outline-w'], - overflow: ['overflow', 'overflow-x', 'overflow-y', 'text-overflow'], + overflow: ['overflow', 'overflow-x', 'overflow-y'], overscroll: ['overscroll', 'overscroll-x', 'overscroll-y'], p: ['p'], pb: ['pb'], @@ -228,7 +228,7 @@ 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-opacity'], + text: ['font-size', 'text-alignment', 'text-color', 'text-opacity', 'text-overflow'], to: ['gradient-to'], top: ['top'], touch: ['touch'], From f3f66054c147b14f68fc8aed7ce39666f5c4c50f Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 13:21:55 +0100 Subject: [PATCH 22/33] add overflow-clip utilities --- src/default-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/default-config.ts b/src/default-config.ts index 064c602..fb138d3 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -34,7 +34,7 @@ export function getDefaultConfig() { const translate = fromTheme('translate') const getOverscroll = () => ['auto', 'contain', 'none'] as const - const getOverflow = () => ['auto', 'hidden', 'visible', 'scroll'] as const + const getOverflow = () => ['auto', 'hidden', 'clip', 'visible', 'scroll'] as const const getSpacingWithAuto = () => ['auto', spacing] as const const getLengthWithEmpty = () => ['', isLength] as const const getIntegerWithAuto = () => ['auto', isInteger] as const From 61325c3d1fd4dce19c5983457d470fde225753db Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:24:32 +0100 Subject: [PATCH 23/33] add full color palette to fill and stroke utilities Added PR to maybe add back "current" options in https://github.com/tailwindlabs/tailwindcss/pull/6357 --- src/default-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index fb138d3..7dfe719 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1435,12 +1435,12 @@ export function getDefaultConfig() { * Fill * @see https://tailwindcss.com/docs/fill */ - fill: [{ fill: ['current', isCustomValue] }], + fill: [{ fill: [colors] }], /** * Stroke * @see https://tailwindcss.com/docs/stroke */ - stroke: [{ stroke: ['current', isCustomValue] }], + stroke: [{ stroke: [colors] }], /** * Stroke Width * @see https://tailwindcss.com/docs/stroke-width From cf93ba279c7c6d823f0ca0fdf188da6cbc8c93f9 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:30:04 +0100 Subject: [PATCH 24/33] add min/max/fit-content values to min/max-width/height utilities --- src/default-config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 7dfe719..4e6608d 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -532,7 +532,7 @@ export function getDefaultConfig() { * Min-Width * @see https://tailwindcss.com/docs/min-width */ - 'min-w': [{ 'min-w': ['full', 'min', 'max', isLength] }], + 'min-w': [{ 'min-w': ['min', 'max', 'fit', isLength] }], /** * Max-Width * @see https://tailwindcss.com/docs/max-width @@ -545,6 +545,7 @@ export function getDefaultConfig() { 'full', 'min', 'max', + 'fit', 'prose', { screen: [isTshirtSize] }, isTshirtSize, @@ -561,12 +562,12 @@ export function getDefaultConfig() { * Min-Height * @see https://tailwindcss.com/docs/min-height */ - 'min-h': [{ 'min-h': ['full', 'screen', isLength] }], + 'min-h': [{ 'min-h': ['min', 'max', 'fit', isLength] }], /** * Max-Height * @see https://tailwindcss.com/docs/max-height */ - 'max-h': [{ 'max-h': [spacing] }], + 'max-h': [{ 'max-h': [spacing, 'min', 'max', 'fit'] }], // Typography /** * Font Family From c9289c2173dbf74ebd3a6a0ee7e8640d023d5f6f Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:33:53 +0100 Subject: [PATCH 25/33] add support for will-change utilities --- src/default-config.ts | 7 +++++++ tests/class-map.test.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index 4e6608d..2814802 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1431,6 +1431,13 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/user-select */ select: [{ select: ['none', 'text', 'all', 'auto'] }], + /** + * Will Change + * @see https://tailwindcss.com/docs/will-change + */ + 'will-change': [ + { 'will-change': ['auto', 'scroll', 'contents', 'transform', isCustomValue] }, + ], // SVG /** * Fill diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 54d5fef..12f1623 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -243,6 +243,7 @@ test('class map has correct class groups at first part', () => { visible: ['visibility'], w: ['w'], whitespace: ['whitespace'], + will: ['will-change'], z: ['z'], }) }) From 2837e54a017ac7baa8dcb3db429b138aa77e3cfc Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:41:23 +0100 Subject: [PATCH 26/33] add support for all cursor values --- src/default-config.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/default-config.ts b/src/default-config.ts index 2814802..5d33a7b 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1287,6 +1287,34 @@ export function getDefaultConfig() { 'move', 'help', 'not-allowed', + 'none', + 'context-menu', + 'progress', + 'cell', + 'crosshair', + 'vertical-text', + 'alias', + 'copy', + 'no-drop', + 'grab', + 'grabbing', + 'all-scroll', + 'col-resize', + 'row-resize', + 'n-resize', + 'e-resize', + 's-resize', + 'w-resize', + 'ne-resize', + 'nw-resize', + 'se-resize', + 'sw-resize', + 'ew-resize', + 'ns-resize', + 'nesw-resize', + 'nwse-resize', + 'zoom-in', + 'zoom-out', isCustomValue, ], }, From f8acd7ca5be6cc40ad4c1cbdee7522bbc44c7870 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 16:13:09 +0100 Subject: [PATCH 27/33] add custom value support for font-weight, bg-position, bg-size and bg-image --- README.md | 10 +++++++- src/default-config.ts | 27 ++++++++++++++------ src/validators.ts | 26 +++++++++++++++++++ tests/public-api.test.ts | 10 +++++++- tests/validators.test.ts | 55 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 117 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 55ac0eb..fe94dee 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,10 @@ interface Validators { isInteger(classPart: string): boolean isCustomValue(classPart: string): boolean isTshirtSize(classPart: string): boolean + isCustomSize(classPart: string): boolean + isCustomPosition(classPart: string): boolean + isCustomUrl(classPart: string): boolean + isCustomWeight(classPart: string): boolean isAny(classPart: string): boolean } ``` @@ -520,7 +524,11 @@ A brief summary for each validator: - `isCustomLength` checks for custom length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`). - `isInteger` checks for integer values (`3`) and custom integer values (`[3]`). - `isCustomValue` checks whether the class part is enclosed in brackets (`[something]`) -- `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`) +- `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`). +- `isCustomSize` checks whether class part is custom value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames. +- `isCustomPosition` checks whether class part is custom value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames. +- `isCustomUrl` checks whether class part is custom value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames. +- `isCustomWeight` checks whether class part is custom value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames. - `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm ceertain there are no other class groups in a namespace. ### `Config` diff --git a/src/default-config.ts b/src/default-config.ts index 5d33a7b..3ef01c4 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -2,7 +2,11 @@ import { fromTheme } from './from-theme' import { isAny, isCustomLength, + isCustomPosition, + isCustomSize, + isCustomUrl, isCustomValue, + isCustomWeight, isInteger, isLength, isTshirtSize, @@ -569,11 +573,6 @@ export function getDefaultConfig() { */ 'max-h': [{ 'max-h': [spacing, 'min', 'max', 'fit'] }], // Typography - /** - * Font Family - * @see https://tailwindcss.com/docs/font-family - */ - 'font-family': [{ font: [isAny] }], /** * Font Size * @see https://tailwindcss.com/docs/font-size @@ -605,9 +604,15 @@ export function getDefaultConfig() { 'bold', 'extrabold', 'black', + isCustomWeight, ], }, ], + /** + * Font Family + * @see https://tailwindcss.com/docs/font-family + */ + 'font-family': [{ font: [isAny] }], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric @@ -790,7 +795,7 @@ export function getDefaultConfig() { * Background Position * @see https://tailwindcss.com/docs/background-position */ - 'bg-position': [{ bg: getPositions() }], + 'bg-position': [{ bg: [...getPositions(), isCustomPosition] }], /** * Background Repeat * @see https://tailwindcss.com/docs/background-repeat @@ -800,13 +805,19 @@ export function getDefaultConfig() { * Background Size * @see https://tailwindcss.com/docs/background-size */ - 'bg-size': [{ bg: ['auto', 'cover', 'contain'] }], + 'bg-size': [{ bg: ['auto', 'cover', 'contain', isCustomSize] }], /** * Background Image * @see https://tailwindcss.com/docs/background-image */ 'bg-image': [ - { bg: ['none', { 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] }] }, + { + bg: [ + 'none', + { 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] }, + isCustomUrl, + ], + }, ], /** * Background Blend Mode diff --git a/src/validators.ts b/src/validators.ts index 33cf524..bb4203c 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -23,6 +23,32 @@ export function isCustomLength(classPart: string) { return false } +export function isCustomSize(classPart: string) { + const customValue = customValueRegex.exec(classPart)?.[1] + + return customValue ? customValue.startsWith('size:') : false +} + +export function isCustomPosition(classPart: string) { + const customValue = customValueRegex.exec(classPart)?.[1] + + return customValue ? customValue.startsWith('position:') : false +} + +export function isCustomUrl(classPart: string) { + const customValue = customValueRegex.exec(classPart)?.[1] + + return customValue ? customValue.startsWith('url(') || customValue.startsWith('url:') : false +} + +export function isCustomWeight(classPart: string) { + const customValue = customValueRegex.exec(classPart)?.[1] + + return customValue + ? !Number.isNaN(Number(customValue)) || customValue.startsWith('weight:') + : false +} + export function isInteger(classPart: string) { const customValue = customValueRegex.exec(classPart)?.[1] diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 61c7ac7..4d10ac6 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -18,8 +18,12 @@ test('has correct export types', () => { isCustomLength: expect.any(Function), isInteger: expect.any(Function), isCustomValue: expect.any(Function), - isAny: expect.any(Function), isTshirtSize: expect.any(Function), + isCustomSize: expect.any(Function), + isCustomPosition: expect.any(Function), + isCustomUrl: expect.any(Function), + isCustomWeight: expect.any(Function), + isAny: expect.any(Function), }) expect(mergeConfigs).toStrictEqual(expect.any(Function)) expect(extendTailwindMerge).toStrictEqual(expect.any(Function)) @@ -121,6 +125,10 @@ test('validators have correct inputs and outputs', () => { expect(validators.isCustomValue('')).toEqual(expect.any(Boolean)) expect(validators.isAny()).toEqual(expect.any(Boolean)) expect(validators.isTshirtSize('')).toEqual(expect.any(Boolean)) + expect(validators.isCustomSize('')).toEqual(expect.any(Boolean)) + expect(validators.isCustomPosition('')).toEqual(expect.any(Boolean)) + expect(validators.isCustomUrl('')).toEqual(expect.any(Boolean)) + expect(validators.isCustomWeight('')).toEqual(expect.any(Boolean)) }) test('mergeConfigs has correct inputs and outputs', () => { diff --git a/tests/validators.test.ts b/tests/validators.test.ts index 8dc3588..b054793 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -1,6 +1,17 @@ import { validators } from '../src' -const { isLength, isCustomLength, isInteger, isCustomValue, isAny, isTshirtSize } = validators +const { + isLength, + isCustomLength, + isInteger, + isCustomValue, + isAny, + isTshirtSize, + isCustomSize, + isCustomPosition, + isCustomUrl, + isCustomWeight, +} = validators describe('validators', () => { test('isLength', () => { @@ -101,4 +112,46 @@ describe('validators', () => { expect(isTshirtSize('-xl')).toBe(false) expect(isTshirtSize('[sm]')).toBe(false) }) + + test('isCustomSize', () => { + expect(isCustomSize('[size:2px]')).toBe(true) + expect(isCustomSize('[size:bla]')).toBe(true) + + expect(isCustomSize('[2px]')).toBe(false) + expect(isCustomSize('[bla]')).toBe(false) + expect(isCustomSize('size:2px')).toBe(false) + }) + + test('isCustomPosition', () => { + expect(isCustomPosition('[position:2px]')).toBe(true) + expect(isCustomPosition('[position:bla]')).toBe(true) + + expect(isCustomPosition('[2px]')).toBe(false) + expect(isCustomPosition('[bla]')).toBe(false) + expect(isCustomPosition('position:2px')).toBe(false) + }) + + test('isCustomUrl', () => { + expect(isCustomUrl('[url:var(--my-url)]')).toBe(true) + expect(isCustomUrl('[url(something)]')).toBe(true) + expect(isCustomUrl('[url:bla]')).toBe(true) + + expect(isCustomUrl('[var(--my-url)]')).toBe(false) + expect(isCustomUrl('[bla]')).toBe(false) + expect(isCustomUrl('url:2px')).toBe(false) + expect(isCustomUrl('url(2px)')).toBe(false) + }) + + test('isCustomWeight', () => { + expect(isCustomWeight('[weight:black]')).toBe(true) + expect(isCustomWeight('[weight:bla]')).toBe(true) + expect(isCustomWeight('[weight:230]')).toBe(true) + expect(isCustomWeight('[450]')).toBe(true) + + expect(isCustomWeight('[2px]')).toBe(false) + expect(isCustomWeight('[bla]')).toBe(false) + expect(isCustomWeight('[black]')).toBe(false) + expect(isCustomWeight('black')).toBe(false) + expect(isCustomWeight('450')).toBe(false) + }) }) From bfe2cc9bb221107fa0bf363cc325ddbb04677f43 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 16:22:44 +0100 Subject: [PATCH 28/33] breaking: rename box decoration utiltities --- src/default-config.ts | 2 +- tests/class-map.test.ts | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/default-config.ts b/src/default-config.ts index 3ef01c4..ca457b6 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -146,7 +146,7 @@ export function getDefaultConfig() { * Box Decoration Break * @see https://tailwindcss.com/docs/box-decoration-break */ - decoration: [{ decoration: ['slice', 'clone'] }], + 'box-decoration': [{ 'box-decoration': ['slice', 'clone'] }], /** * Box Sizing * @see https://tailwindcss.com/docs/box-sizing diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 12f1623..f501222 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -71,7 +71,7 @@ test('class map has correct class groups at first part', () => { 'border-w-y', ], bottom: ['bottom'], - box: ['box'], + box: ['box', 'box-decoration'], break: ['break', 'break-after', 'break-before', 'break-inside'], brightness: ['brightness'], capitalize: ['text-transform'], @@ -84,12 +84,7 @@ test('class map has correct class groups at first part', () => { contents: ['display'], contrast: ['contrast'], cursor: ['cursor'], - decoration: [ - 'decoration', - 'text-decoration-color', - 'text-decoration-style', - 'text-decoration-thickness', - ], + decoration: ['text-decoration-color', 'text-decoration-style', 'text-decoration-thickness'], delay: ['delay'], diagonal: ['fvn-fraction'], divide: [ From 014a01180f7016792ee36046446c86e10071dc04 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 16:42:34 +0100 Subject: [PATCH 29/33] add arbitrary value support for vertical-align --- src/default-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/default-config.ts b/src/default-config.ts index ca457b6..3928353 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -757,6 +757,7 @@ export function getDefaultConfig() { 'text-bottom', 'sub', 'super', + isCustomLength, ], }, ], From adc3c02c7f035069beec1c62777ec008172587ab Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 17:42:23 +0100 Subject: [PATCH 30/33] breaking: rename custom values to arbitrary values everywhere Tailwind is using the term "arbitrary values" in its v3 documentation. To stay consistent, I need to name them arbitrary values as well. --- README.md | 36 ++--- src/default-config.ts | 54 ++++---- src/merge-classlist.ts | 2 +- src/validators.ts | 50 +++---- ...alues.test.ts => arbitrary-values.test.ts} | 10 +- tests/public-api.test.ts | 24 ++-- tests/validators.test.ts | 128 +++++++++--------- 7 files changed, 154 insertions(+), 150 deletions(-) rename tests/{custom-values.test.ts => arbitrary-values.test.ts} (68%) diff --git a/README.md b/README.md index fe94dee..5df6933 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ twMerge('hover:p-2 hover:p-4') // → 'hover:p-4' twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4' ``` -### Supports custom values +### Supports arbitrary values ```ts twMerge('bg-black bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--mystery-var)]' @@ -187,13 +187,13 @@ E.g. here is the overflow class group which results in the classes `overflow-aut const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }] ``` -Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows custom values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group. +Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows arbitrary values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group. E.g. here is the fill class group. ```ts -const isCustomValue = (classPart: string) => /^\[.+\]$/.test(classPart) -const fillClassGroup = [{ fill: ['current', isCustomValue] }] +const isArbitraryValue = (classPart: string) => /^\[.+\]$/.test(classPart) +const fillClassGroup = [{ fill: ['current', isArbitraryValue] }] ``` Because the function is under the `fill` key, it will only get called for classes which start with `fill-`. Also, the function only gets passed the part of the class name which comes after `fill-`, this way you can use the same function in multiple class groups. tailwind-merge exports its own [validators](#validators), so you don't need to recreate them. @@ -500,14 +500,14 @@ const customTwMerge = createTailwindMerge(getDefaultConfig, (config) => ```ts interface Validators { isLength(classPart: string): boolean - isCustomLength(classPart: string): boolean + isArbitraryLength(classPart: string): boolean isInteger(classPart: string): boolean - isCustomValue(classPart: string): boolean + isArbitraryValue(classPart: string): boolean isTshirtSize(classPart: string): boolean - isCustomSize(classPart: string): boolean - isCustomPosition(classPart: string): boolean - isCustomUrl(classPart: string): boolean - isCustomWeight(classPart: string): boolean + isArbitrarySize(classPart: string): boolean + isArbitraryPosition(classPart: string): boolean + isArbitraryUrl(classPart: string): boolean + isArbitraryWeight(classPart: string): boolean isAny(classPart: string): boolean } ``` @@ -520,15 +520,15 @@ const paddingClassGroup = [{ p: [validators.isLength] }] A brief summary for each validator: -- `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a custom length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`. -- `isCustomLength` checks for custom length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`). -- `isInteger` checks for integer values (`3`) and custom integer values (`[3]`). -- `isCustomValue` checks whether the class part is enclosed in brackets (`[something]`) +- `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a arbitrary length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`. +- `isArbitraryLength` checks for arbitrary length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`). +- `isInteger` checks for integer values (`3`) and arbitrary integer values (`[3]`). +- `isArbitraryValue` checks whether the class part is enclosed in brackets (`[something]`) - `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`). -- `isCustomSize` checks whether class part is custom value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames. -- `isCustomPosition` checks whether class part is custom value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames. -- `isCustomUrl` checks whether class part is custom value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames. -- `isCustomWeight` checks whether class part is custom value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames. +- `isArbitrarySize` checks whether class part is arbitrary value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames. +- `isArbitraryPosition` checks whether class part is arbitrary value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames. +- `isArbitraryUrl` checks whether class part is arbitrary value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames. +- `isArbitraryWeight` checks whether class part is arbitrary value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames. - `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm ceertain there are no other class groups in a namespace. ### `Config` diff --git a/src/default-config.ts b/src/default-config.ts index 3928353..84be1b8 100644 --- a/src/default-config.ts +++ b/src/default-config.ts @@ -1,12 +1,12 @@ import { fromTheme } from './from-theme' import { isAny, - isCustomLength, - isCustomPosition, - isCustomSize, - isCustomUrl, - isCustomValue, - isCustomWeight, + isArbitraryLength, + isArbitraryPosition, + isArbitrarySize, + isArbitraryUrl, + isArbitraryValue, + isArbitraryWeight, isInteger, isLength, isTshirtSize, @@ -88,10 +88,10 @@ export function getDefaultConfig() { theme: { colors: [isAny], spacing: [isLength], - blur: ['none', '', isTshirtSize, isCustomLength], + blur: ['none', '', isTshirtSize, isArbitraryLength], brightness: [isInteger], borderColor: [colors], - borderRadius: ['none', '', 'full', isTshirtSize, isCustomLength], + borderRadius: ['none', '', 'full', isTshirtSize, isArbitraryLength], borderWidth: getLengthWithEmpty(), contrast: [isInteger], grayscale: getZeroAndEmpty(), @@ -116,7 +116,7 @@ export function getDefaultConfig() { * Aspect Ratio * @see https://tailwindcss.com/docs/aspect-ratio */ - aspect: [{ aspect: ['auto', 'square', 'video', isCustomValue] }], + aspect: [{ aspect: ['auto', 'square', 'video', isArbitraryValue] }], /** * Container * @see https://tailwindcss.com/docs/container @@ -304,7 +304,7 @@ export function getDefaultConfig() { * Flex * @see https://tailwindcss.com/docs/flex */ - flex: [{ flex: ['1', 'auto', 'initial', 'none', isCustomValue] }], + flex: [{ flex: ['1', 'auto', 'initial', 'none', isArbitraryValue] }], /** * Flex Grow * @see https://tailwindcss.com/docs/flex-grow @@ -369,12 +369,12 @@ export function getDefaultConfig() { * Grid Auto Columns * @see https://tailwindcss.com/docs/grid-auto-columns */ - 'auto-cols': [{ 'auto-cols': ['auto', 'min', 'max', 'fr', isCustomValue] }], + 'auto-cols': [{ 'auto-cols': ['auto', 'min', 'max', 'fr', isArbitraryValue] }], /** * Grid Auto Rows * @see https://tailwindcss.com/docs/grid-auto-rows */ - 'auto-rows': [{ 'auto-rows': ['auto', 'min', 'max', 'fr', isCustomValue] }], + 'auto-rows': [{ 'auto-rows': ['auto', 'min', 'max', 'fr', isArbitraryValue] }], /** * Gap * @see https://tailwindcss.com/docs/gap @@ -553,7 +553,7 @@ export function getDefaultConfig() { 'prose', { screen: [isTshirtSize] }, isTshirtSize, - isCustomLength, + isArbitraryLength, ], }, ], @@ -577,7 +577,7 @@ export function getDefaultConfig() { * Font Size * @see https://tailwindcss.com/docs/font-size */ - 'font-size': [{ text: ['base', isTshirtSize, isCustomLength] }], + 'font-size': [{ text: ['base', isTshirtSize, isArbitraryLength] }], /** * Font Smoothing * @see https://tailwindcss.com/docs/font-smoothing @@ -604,7 +604,7 @@ export function getDefaultConfig() { 'bold', 'extrabold', 'black', - isCustomWeight, + isArbitraryWeight, ], }, ], @@ -656,7 +656,7 @@ export function getDefaultConfig() { 'wide', 'wider', 'widest', - isCustomLength, + isArbitraryLength, ], }, ], @@ -671,7 +671,7 @@ export function getDefaultConfig() { * List Style Type * @see https://tailwindcss.com/docs/list-style-type */ - 'list-style-type': [{ list: ['none', 'disc', 'decimal', isCustomValue] }], + 'list-style-type': [{ list: ['none', 'disc', 'decimal', isArbitraryValue] }], /** * List Style Position * @see https://tailwindcss.com/docs/list-style-position @@ -757,7 +757,7 @@ export function getDefaultConfig() { 'text-bottom', 'sub', 'super', - isCustomLength, + isArbitraryLength, ], }, ], @@ -796,7 +796,7 @@ export function getDefaultConfig() { * Background Position * @see https://tailwindcss.com/docs/background-position */ - 'bg-position': [{ bg: [...getPositions(), isCustomPosition] }], + 'bg-position': [{ bg: [...getPositions(), isArbitraryPosition] }], /** * Background Repeat * @see https://tailwindcss.com/docs/background-repeat @@ -806,7 +806,7 @@ export function getDefaultConfig() { * Background Size * @see https://tailwindcss.com/docs/background-size */ - 'bg-size': [{ bg: ['auto', 'cover', 'contain', isCustomSize] }], + 'bg-size': [{ bg: ['auto', 'cover', 'contain', isArbitrarySize] }], /** * Background Image * @see https://tailwindcss.com/docs/background-image @@ -816,7 +816,7 @@ export function getDefaultConfig() { bg: [ 'none', { 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] }, - isCustomUrl, + isArbitraryUrl, ], }, ], @@ -1184,7 +1184,7 @@ export function getDefaultConfig() { 'opacity', 'shadow', 'transform', - isCustomValue, + isArbitraryValue, ], }, ], @@ -1197,7 +1197,7 @@ export function getDefaultConfig() { * Transition Timing Function * @see https://tailwindcss.com/docs/transition-timing-function */ - ease: [{ ease: ['linear', 'in', 'out', 'in-out', isCustomValue] }], + ease: [{ ease: ['linear', 'in', 'out', 'in-out', isArbitraryValue] }], /** * Transition Delay * @see https://tailwindcss.com/docs/transition-delay @@ -1207,7 +1207,7 @@ export function getDefaultConfig() { * Animation * @see https://tailwindcss.com/docs/animation */ - animate: [{ animate: ['none', 'spin', 'ping', 'pulse', 'bounce', isCustomValue] }], + animate: [{ animate: ['none', 'spin', 'ping', 'pulse', 'bounce', isArbitraryValue] }], // Transforms /** * Transform @@ -1327,7 +1327,7 @@ export function getDefaultConfig() { 'nwse-resize', 'zoom-in', 'zoom-out', - isCustomValue, + isArbitraryValue, ], }, ], @@ -1476,7 +1476,7 @@ export function getDefaultConfig() { * @see https://tailwindcss.com/docs/will-change */ 'will-change': [ - { 'will-change': ['auto', 'scroll', 'contents', 'transform', isCustomValue] }, + { 'will-change': ['auto', 'scroll', 'contents', 'transform', isArbitraryValue] }, ], // SVG /** @@ -1505,7 +1505,7 @@ export function getDefaultConfig() { * Content * @see https://tailwindcss.com/docs/just-in-time-mode#content-utilities */ - content: [{ content: [isCustomValue] }], + content: [{ content: [isArbitraryValue] }], /** * Caret Color * @see https://tailwindcss.com/docs/just-in-time-mode#caret-color-utilities diff --git a/src/merge-classlist.ts b/src/merge-classlist.ts index 9fcf269..b95fa82 100644 --- a/src/merge-classlist.ts +++ b/src/merge-classlist.ts @@ -2,7 +2,7 @@ import { ConfigUtils } from './config-utils' const SPLIT_CLASSES_REGEX = /\s+/ const IMPORTANT_MODIFIER = '!' -// Regex is needed so we don't match against colons in labels for custom values like `text-[color:var(--mystery-var)]` +// Regex is needed so we don't match against colons in labels for arbitrary values like `text-[color:var(--mystery-var)]` // I'd prefer to use a negative lookbehind for all supported labels, but lookbheinds don't have good browser support yet. More info: https://caniuse.com/js-regexp-lookbehind const PREFIX_SEPARATOR_REGEX = /:(?![^[]*\])/ const PREFIX_SEPARATOR = ':' diff --git a/src/validators.ts b/src/validators.ts index bb4203c..23d39e5 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -1,4 +1,4 @@ -const customValueRegex = /^\[(.+)\]$/ +const arbitraryValueRegex = /^\[(.+)\]$/ const fractionRegex = /^\d+\/\d+$/ const stringLengths = new Set(['px', 'full', 'screen']) const tshirtUnitRegex = /^(\d+)?(xs|sm|md|lg|xl)$/ @@ -9,58 +9,60 @@ export function isLength(classPart: string) { !Number.isNaN(Number(classPart)) || stringLengths.has(classPart) || fractionRegex.test(classPart) || - isCustomLength(classPart) + isArbitraryLength(classPart) ) } -export function isCustomLength(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] +export function isArbitraryLength(classPart: string) { + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - if (customValue) { - return customValue.startsWith('length:') || lengthUnitRegex.test(customValue) + if (arbitraryValue) { + return arbitraryValue.startsWith('length:') || lengthUnitRegex.test(arbitraryValue) } return false } -export function isCustomSize(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] +export function isArbitrarySize(classPart: string) { + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - return customValue ? customValue.startsWith('size:') : false + return arbitraryValue ? arbitraryValue.startsWith('size:') : false } -export function isCustomPosition(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] +export function isArbitraryPosition(classPart: string) { + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - return customValue ? customValue.startsWith('position:') : false + return arbitraryValue ? arbitraryValue.startsWith('position:') : false } -export function isCustomUrl(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] +export function isArbitraryUrl(classPart: string) { + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - return customValue ? customValue.startsWith('url(') || customValue.startsWith('url:') : false + return arbitraryValue + ? arbitraryValue.startsWith('url(') || arbitraryValue.startsWith('url:') + : false } -export function isCustomWeight(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] +export function isArbitraryWeight(classPart: string) { + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - return customValue - ? !Number.isNaN(Number(customValue)) || customValue.startsWith('weight:') + return arbitraryValue + ? !Number.isNaN(Number(arbitraryValue)) || arbitraryValue.startsWith('weight:') : false } export function isInteger(classPart: string) { - const customValue = customValueRegex.exec(classPart)?.[1] + const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1] - if (customValue) { - return Number.isInteger(Number(customValue)) + if (arbitraryValue) { + return Number.isInteger(Number(arbitraryValue)) } return Number.isInteger(Number(classPart)) } -export function isCustomValue(classPart: string) { - return customValueRegex.test(classPart) +export function isArbitraryValue(classPart: string) { + return arbitraryValueRegex.test(classPart) } export function isAny() { diff --git a/tests/custom-values.test.ts b/tests/arbitrary-values.test.ts similarity index 68% rename from tests/custom-values.test.ts rename to tests/arbitrary-values.test.ts index 96a1728..69da5f8 100644 --- a/tests/custom-values.test.ts +++ b/tests/arbitrary-values.test.ts @@ -1,21 +1,23 @@ import { twMerge } from '../src' -test('handles custom length conflicts correctly', () => { +test('handles arbitrary length conflicts correctly', () => { expect(twMerge('m-[2px] m-[10px]')).toBe('m-[10px]') expect(twMerge('my-[2px] m-[10rem]')).toBe('m-[10rem]') expect(twMerge('cursor-pointer cursor-[grab]')).toBe('cursor-[grab]') - expect(twMerge('m-[2px] m-[calc(100%-var(--custom))]')).toBe('m-[calc(100%-var(--custom))]') + expect(twMerge('m-[2px] m-[calc(100%-var(--arbitrary))]')).toBe( + 'm-[calc(100%-var(--arbitrary))]' + ) expect(twMerge('m-[2px] m-[length:var(--mystery-var)]')).toBe('m-[length:var(--mystery-var)]') }) -test('handles custom length conflicts with labels and prefixes correctly', () => { +test('handles arbitrary length conflicts with labels and prefixes correctly', () => { expect(twMerge('hover:m-[2px] hover:m-[length:var(--c)]')).toBe('hover:m-[length:var(--c)]') expect(twMerge('hover:focus:m-[2px] focus:hover:m-[length:var(--c)]')).toBe( 'focus:hover:m-[length:var(--c)]' ) }) -test('handles complex custom value conflicts correctly', () => { +test('handles complex arbitrary value conflicts correctly', () => { expect(twMerge('grid-rows-[1fr,auto] grid-rows-2')).toBe('grid-rows-2') expect(twMerge('grid-rows-[repeat(20,minmax(0,1fr))] grid-rows-3')).toBe('grid-rows-3') }) diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 4d10ac6..93b1722 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -15,14 +15,14 @@ test('has correct export types', () => { expect(getDefaultConfig).toStrictEqual(expect.any(Function)) expect(validators).toEqual({ isLength: expect.any(Function), - isCustomLength: expect.any(Function), + isArbitraryLength: expect.any(Function), isInteger: expect.any(Function), - isCustomValue: expect.any(Function), + isArbitraryValue: expect.any(Function), isTshirtSize: expect.any(Function), - isCustomSize: expect.any(Function), - isCustomPosition: expect.any(Function), - isCustomUrl: expect.any(Function), - isCustomWeight: expect.any(Function), + isArbitrarySize: expect.any(Function), + isArbitraryPosition: expect.any(Function), + isArbitraryUrl: expect.any(Function), + isArbitraryWeight: expect.any(Function), isAny: expect.any(Function), }) expect(mergeConfigs).toStrictEqual(expect.any(Function)) @@ -120,15 +120,15 @@ test('createTailwindMerge() has correct inputs and outputs', () => { test('validators have correct inputs and outputs', () => { expect(validators.isLength('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomLength('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryLength('')).toEqual(expect.any(Boolean)) expect(validators.isInteger('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomValue('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryValue('')).toEqual(expect.any(Boolean)) expect(validators.isAny()).toEqual(expect.any(Boolean)) expect(validators.isTshirtSize('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomSize('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomPosition('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomUrl('')).toEqual(expect.any(Boolean)) - expect(validators.isCustomWeight('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitrarySize('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryPosition('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryUrl('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryWeight('')).toEqual(expect.any(Boolean)) }) test('mergeConfigs has correct inputs and outputs', () => { diff --git a/tests/validators.test.ts b/tests/validators.test.ts index b054793..4440f83 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -2,15 +2,15 @@ import { validators } from '../src' const { isLength, - isCustomLength, + isArbitraryLength, isInteger, - isCustomValue, + isArbitraryValue, isAny, isTshirtSize, - isCustomSize, - isCustomPosition, - isCustomUrl, - isCustomWeight, + isArbitrarySize, + isArbitraryPosition, + isArbitraryUrl, + isArbitraryWeight, } = validators describe('validators', () => { @@ -28,7 +28,7 @@ describe('validators', () => { expect(isLength('[481px]')).toBe(true) expect(isLength('[19.1rem]')).toBe(true) expect(isLength('[50vw]')).toBe(true) - expect(isLength('[length:var(--custom)]')).toBe(true) + expect(isLength('[length:var(--arbitrary)]')).toBe(true) expect(isLength('1d5')).toBe(false) expect(isLength('[1]')).toBe(false) @@ -37,20 +37,20 @@ describe('validators', () => { expect(isLength('one')).toBe(false) }) - test('isCustomLength', () => { - expect(isCustomLength('[3.7%]')).toBe(true) - expect(isCustomLength('[481px]')).toBe(true) - expect(isCustomLength('[19.1rem]')).toBe(true) - expect(isCustomLength('[50vw]')).toBe(true) - expect(isCustomLength('[length:var(--custom)]')).toBe(true) - - expect(isCustomLength('1')).toBe(false) - expect(isCustomLength('3px')).toBe(false) - expect(isCustomLength('1d5')).toBe(false) - expect(isCustomLength('[1]')).toBe(false) - expect(isCustomLength('[12px')).toBe(false) - expect(isCustomLength('12px]')).toBe(false) - expect(isCustomLength('one')).toBe(false) + test('isArbitraryLength', () => { + expect(isArbitraryLength('[3.7%]')).toBe(true) + expect(isArbitraryLength('[481px]')).toBe(true) + expect(isArbitraryLength('[19.1rem]')).toBe(true) + expect(isArbitraryLength('[50vw]')).toBe(true) + expect(isArbitraryLength('[length:var(--arbitrary)]')).toBe(true) + + expect(isArbitraryLength('1')).toBe(false) + expect(isArbitraryLength('3px')).toBe(false) + expect(isArbitraryLength('1d5')).toBe(false) + expect(isArbitraryLength('[1]')).toBe(false) + expect(isArbitraryLength('[12px')).toBe(false) + expect(isArbitraryLength('12px]')).toBe(false) + expect(isArbitraryLength('one')).toBe(false) }) test('isInteger', () => { @@ -71,18 +71,18 @@ describe('validators', () => { expect(isInteger('1px')).toBe(false) }) - test('isCustomValue', () => { - expect(isCustomValue('[1]')).toBe(true) - expect(isCustomValue('[bla]')).toBe(true) - expect(isCustomValue('[not-a-custom-value?]')).toBe(true) - expect(isCustomValue('[auto,auto,minmax(0,1fr),calc(100vw-50%)]')).toBe(true) - - expect(isCustomValue('[]')).toBe(false) - expect(isCustomValue('[1')).toBe(false) - expect(isCustomValue('1]')).toBe(false) - expect(isCustomValue('1')).toBe(false) - expect(isCustomValue('one')).toBe(false) - expect(isCustomValue('o[n]e')).toBe(false) + test('isArbitraryValue', () => { + expect(isArbitraryValue('[1]')).toBe(true) + expect(isArbitraryValue('[bla]')).toBe(true) + expect(isArbitraryValue('[not-an-arbitrary-value?]')).toBe(true) + expect(isArbitraryValue('[auto,auto,minmax(0,1fr),calc(100vw-50%)]')).toBe(true) + + expect(isArbitraryValue('[]')).toBe(false) + expect(isArbitraryValue('[1')).toBe(false) + expect(isArbitraryValue('1]')).toBe(false) + expect(isArbitraryValue('1')).toBe(false) + expect(isArbitraryValue('one')).toBe(false) + expect(isArbitraryValue('o[n]e')).toBe(false) }) test('isAny', () => { @@ -113,45 +113,45 @@ describe('validators', () => { expect(isTshirtSize('[sm]')).toBe(false) }) - test('isCustomSize', () => { - expect(isCustomSize('[size:2px]')).toBe(true) - expect(isCustomSize('[size:bla]')).toBe(true) + test('isArbitrarySize', () => { + expect(isArbitrarySize('[size:2px]')).toBe(true) + expect(isArbitrarySize('[size:bla]')).toBe(true) - expect(isCustomSize('[2px]')).toBe(false) - expect(isCustomSize('[bla]')).toBe(false) - expect(isCustomSize('size:2px')).toBe(false) + expect(isArbitrarySize('[2px]')).toBe(false) + expect(isArbitrarySize('[bla]')).toBe(false) + expect(isArbitrarySize('size:2px')).toBe(false) }) - test('isCustomPosition', () => { - expect(isCustomPosition('[position:2px]')).toBe(true) - expect(isCustomPosition('[position:bla]')).toBe(true) + test('isArbitraryPosition', () => { + expect(isArbitraryPosition('[position:2px]')).toBe(true) + expect(isArbitraryPosition('[position:bla]')).toBe(true) - expect(isCustomPosition('[2px]')).toBe(false) - expect(isCustomPosition('[bla]')).toBe(false) - expect(isCustomPosition('position:2px')).toBe(false) + expect(isArbitraryPosition('[2px]')).toBe(false) + expect(isArbitraryPosition('[bla]')).toBe(false) + expect(isArbitraryPosition('position:2px')).toBe(false) }) - test('isCustomUrl', () => { - expect(isCustomUrl('[url:var(--my-url)]')).toBe(true) - expect(isCustomUrl('[url(something)]')).toBe(true) - expect(isCustomUrl('[url:bla]')).toBe(true) + test('isArbitraryUrl', () => { + expect(isArbitraryUrl('[url:var(--my-url)]')).toBe(true) + expect(isArbitraryUrl('[url(something)]')).toBe(true) + expect(isArbitraryUrl('[url:bla]')).toBe(true) - expect(isCustomUrl('[var(--my-url)]')).toBe(false) - expect(isCustomUrl('[bla]')).toBe(false) - expect(isCustomUrl('url:2px')).toBe(false) - expect(isCustomUrl('url(2px)')).toBe(false) + expect(isArbitraryUrl('[var(--my-url)]')).toBe(false) + expect(isArbitraryUrl('[bla]')).toBe(false) + expect(isArbitraryUrl('url:2px')).toBe(false) + expect(isArbitraryUrl('url(2px)')).toBe(false) }) - test('isCustomWeight', () => { - expect(isCustomWeight('[weight:black]')).toBe(true) - expect(isCustomWeight('[weight:bla]')).toBe(true) - expect(isCustomWeight('[weight:230]')).toBe(true) - expect(isCustomWeight('[450]')).toBe(true) - - expect(isCustomWeight('[2px]')).toBe(false) - expect(isCustomWeight('[bla]')).toBe(false) - expect(isCustomWeight('[black]')).toBe(false) - expect(isCustomWeight('black')).toBe(false) - expect(isCustomWeight('450')).toBe(false) + test('isArbitraryWeight', () => { + expect(isArbitraryWeight('[weight:black]')).toBe(true) + expect(isArbitraryWeight('[weight:bla]')).toBe(true) + expect(isArbitraryWeight('[weight:230]')).toBe(true) + expect(isArbitraryWeight('[450]')).toBe(true) + + expect(isArbitraryWeight('[2px]')).toBe(false) + expect(isArbitraryWeight('[bla]')).toBe(false) + expect(isArbitraryWeight('[black]')).toBe(false) + expect(isArbitraryWeight('black')).toBe(false) + expect(isArbitraryWeight('450')).toBe(false) }) }) From 607f78fbe5f2e69e6ef6d2a94c3a323965e57706 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 17:55:27 +0100 Subject: [PATCH 31/33] add support for arbitrary properties --- src/class-utils.ts | 19 +++++++++++++++- tests/arbitrary-properties.test.ts | 36 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/arbitrary-properties.test.ts diff --git a/src/class-utils.ts b/src/class-utils.ts index 0b46e25..f6b6038 100644 --- a/src/class-utils.ts +++ b/src/class-utils.ts @@ -24,7 +24,7 @@ export function createClassUtils(config: Config) { classParts.shift() } - return getGroupRecursive(classParts, classMap) + return getGroupRecursive(classParts, classMap) || getGroupIdForArbitraryProperty(className) } function getConflictingClassGroupIds(classGroupId: ClassGroupId) { @@ -64,6 +64,23 @@ function getGroupRecursive( return classPartObject.validators.find(({ validator }) => validator(classRest))?.classGroupId } +const arbitraryPropertyRegex = /^\[(.+)\]$/ + +function getGroupIdForArbitraryProperty(className: string) { + if (arbitraryPropertyRegex.test(className)) { + const arbitraryPropertyClassName = arbitraryPropertyRegex.exec(className)![1] + const property = arbitraryPropertyClassName?.substring( + 0, + arbitraryPropertyClassName.indexOf(':') + ) + + if (property) { + // I use two dots here because one dot is used as prefix for class groups in plugins + return 'arbitrary..' + property + } + } +} + /** * Exported for testing only */ diff --git a/tests/arbitrary-properties.test.ts b/tests/arbitrary-properties.test.ts new file mode 100644 index 0000000..49ea8b2 --- /dev/null +++ b/tests/arbitrary-properties.test.ts @@ -0,0 +1,36 @@ +import { twMerge } from '../src' + +test('handles arbitrary property conflicts correctly', () => { + expect(twMerge('[paint-order:markers] [paint-order:normal]')).toBe('[paint-order:normal]') + expect( + twMerge('[paint-order:markers] [--my-var:2rem] [paint-order:normal] [--my-var:4px]') + ).toBe('[paint-order:normal] [--my-var:4px]') +}) + +test('handles arbitrary property conflicts with prefixes correctly', () => { + expect(twMerge('[paint-order:markers] hover:[paint-order:normal]')).toBe( + '[paint-order:markers] hover:[paint-order:normal]' + ) + expect(twMerge('hover:[paint-order:markers] hover:[paint-order:normal]')).toBe( + 'hover:[paint-order:normal]' + ) + expect(twMerge('hover:focus:[paint-order:markers] focus:hover:[paint-order:normal]')).toBe( + 'focus:hover:[paint-order:normal]' + ) + expect( + twMerge('[paint-order:markers] [paint-order:normal] [--my-var:2rem] lg:[--my-var:4px]') + ).toBe('[paint-order:normal] [--my-var:2rem] lg:[--my-var:4px]') +}) + +test('handles complex arbitrary property conflicts correctly', () => { + expect(twMerge('[-unknown-prop:::123:::] [-unknown-prop:url(https://hi.com)]')).toBe( + '[-unknown-prop:url(https://hi.com)]' + ) +}) + +test('handles important modifier correctly', () => { + expect(twMerge('![some:prop] [some:other]')).toBe('![some:prop] [some:other]') + expect(twMerge('![some:prop] [some:other] [some:one] ![some:another]')).toBe( + '[some:one] ![some:another]' + ) +}) From 803889df7be2d75882f4882db32a402f15b32f2d Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 18:07:49 +0100 Subject: [PATCH 32/33] add documentation about arbitrary property support --- README.md | 13 +++++++++++++ tests/readme-examples.test.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5df6933..22024b3 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,19 @@ twMerge('bg-black bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--myster twMerge('grid-cols-[1fr,auto] grid-cols-2') // → 'grid-cols-2' ``` +### Supports arbitrary properties + +```ts +twMerge('[mask-type:luminance] [mask-type:alpha]') // → '[mask-type:alpha]' +twMerge('[--scroll-offset:56px] lg:[--scroll-offset:44px]') +// → '[--scroll-offset:56px] lg:[--scroll-offset:44px]' + +// Don't do this! +twMerge('[padding:1rem] p-8') // → '[padding:1rem] p-8' +``` + +Watch out for mixing arbitrary properties which could be expressed as Tailwind classes. tailwind-merge does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep the bundle size small. + ### Supports important modifier ```ts diff --git a/tests/readme-examples.test.ts b/tests/readme-examples.test.ts index ffd5ef6..8f73906 100644 --- a/tests/readme-examples.test.ts +++ b/tests/readme-examples.test.ts @@ -6,7 +6,7 @@ const twMergeExampleRegex = /twMerge\((?[\w\s\-:[\]#(),!\n'"]+?)\)(?!.*(?.+)['"]/g test('readme examples', () => { - expect.assertions(18) + expect.assertions(21) return fs.promises .readFile(`${__dirname}/../README.md`, { encoding: 'utf-8' }) From 1f799bc7adce26c3889bc1e896291481c8aae392 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Fri, 10 Dec 2021 18:34:30 +0100 Subject: [PATCH 33/33] adjust Tailwind support message in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22024b3..bfcbe03 100644 --- a/README.md +++ b/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 v3.0 (if you use Tailwind v2, check the [tailwind-merge v1.0.0 release notes](https://github.com/dcastil/tailwind-merge/releases/tag/v1.0.0) to figure out which version of tailwind-merge to use) +- Supports Tailwind v3.0 (if you use Tailwind v2, use [tailwind-merge v0.9.0](https://github.com/dcastil/tailwind-merge/tree/v0.9.0)) - Works in Node >=12 and all modern browsers - Fully typed - [5.2 kB minified + gzipped](https://bundlephobia.com/package/tailwind-merge) (96.7% self, 3.3% hashlru)