diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c103bc64a1..3b639aed9009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) +### Fixed + +- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827)) + ## [4.2.2] - 2026-03-18 ### Fixed diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 4b6ecbfd6c3c..fef9eee08008 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1120,6 +1120,40 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCombinedCanonicalization(input, candidates.trim(), expected) }) }) + + // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558 + test.each([ + ['tracking-[-0.05em]', 'tracking-tighter'], + ['tracking-[-0.025em]', 'tracking-tight'], + ['tracking-[0em]', 'tracking-normal'], + ['tracking-[0.025em]', 'tracking-wide'], + ['tracking-[0.05em]', 'tracking-wider'], + ['tracking-[0.1em]', 'tracking-widest'], + + // Negative values that don't make sense + // See: https://tailwindcss.com/docs/letter-spacing#using-negative-values + ['-tracking-tighter', 'tracking-wider'], + ['-tracking-tight', 'tracking-wide'], + ['-tracking-normal', 'tracking-normal'], + ['-tracking-wide', 'tracking-tight'], + ['-tracking-wider', 'tracking-tighter'], + ])(testName, { timeout }, async (candidate, expected) => { + await expectCanonicalization( + css` + @import 'tailwindcss'; + @theme { + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-normal: 0em; + --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + } + `, + candidate, + expected, + ) + }) }) describe('theme to var', () => { diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 566b4a3f76b2..eaa055b25dd4 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1128,11 +1128,29 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. Not sure how to migrate - // this one so let's just skip it for now. - // - // TODO: Do we just migrate to the first one? - if (replacements.length > 1) return + // Multiple utilities can map to the same signature. + if (replacements.length > 1) { + // Prefer positive values over negative values + let maybeReplacement: string | undefined = undefined + for (let replacement of replacements) { + if (replacement[0] === '-') continue // Skip negative values + + // If multiple non-negative replacements exists then we are unsure + // what to do, so let's bail. + if (maybeReplacement) return + + // Consider this replacement + maybeReplacement = replacement + } + + if (maybeReplacement) { + for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { + yield replacementCandidate + } + } + + return + } // If we didn't find any replacement utilities, let's try to strip the // modifier and find a replacement then. If we do, we can try to re-add the @@ -1353,11 +1371,29 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. Not sure how to migrate - // this one so let's just skip it for now. - // - // TODO: Do we just migrate to the first one? - if (replacements.length > 1) return + // Multiple utilities can map to the same signature. + if (replacements.length > 1) { + // Prefer positive values over negative values + let maybeReplacement: string | undefined = undefined + for (let replacement of replacements) { + if (replacement[0] === '-') continue // Skip negative values + + // If multiple non-negative replacements exists then we are unsure + // what to do, so let's bail. + if (maybeReplacement) return + + // Consider this replacement + maybeReplacement = replacement + } + + if (maybeReplacement) { + for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { + yield replacementCandidate + } + } + + return + } // If we didn't find any replacement utilities, let's try to strip the // modifier and find a replacement then. If we do, we can try to re-add the