diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a54331dbe32..1f174abca67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727)) - Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745)) - Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790)) +- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809)) ## [4.2.1] - 2026-02-23 diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 505c473a5900..efcb351a345c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1054,6 +1054,13 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // To completely different utility ['w-4 h-4', 'size-4'], + // Goes beyond the default spacing scale that's being used in intellisense + // for code completion. Since it's about bare values, we should still be + // able to combine them. + ['w-123 h-123', 'size-123'], + ['w-128 h-128', 'size-128'], // `w-128` on its own would become `w-lg` + ['mt-123 mb-123', 'my-123'], + // Do not touch if not operating on the same variants ['hover:w-4 h-4', 'hover:w-4 h-4'], diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index f26516c136b7..8670dd2c1f6c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -312,12 +312,52 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st } } + let dynamicUtilities = new DefaultMap((candidate: string) => { + let result = new DefaultMap( + (_property: string) => new DefaultMap((_value: string) => new Set()), + ) + + let relevantProperties = new Set(computeUtilitiesPropertiesLookup.get(candidate).keys()) + if (relevantProperties.size === 0) return result + + for (let parsedCandidate of parseCandidate(designSystem, candidate)) { + if ( + parsedCandidate.kind !== 'functional' || + parsedCandidate.value?.kind !== 'named' // Necessary for bare values + ) { + continue + } + + for (let root of designSystem.utilities.keys('functional')) { + if (root === parsedCandidate.root) continue // Skip self + + let replacement = printUnprefixedCandidate(designSystem, { + ...cloneCandidate(parsedCandidate), + root, + }) + + let propertyValues = computeUtilitiesPropertiesLookup.get(replacement) + for (let [property, values] of propertyValues) { + if (!relevantProperties.has(property)) continue // Skip properties that are not relevant for the current candidate + + for (let value of values) { + result.get(property).get(value).add(replacement) + } + } + } + + return result + } + + return result + }) + // For each property, lookup other utilities that also set this property and // this exact value. If multiple properties are used, use the intersection of // each property. // // E.g.: `margin-top` → `mt-1`, `my-1`, `m-1` - let otherUtilities = candidatePropertiesValues.map((propertyValues) => { + let otherUtilities = candidatePropertiesValues.map((propertyValues, idx) => { let result: Set | null = null for (let property of propertyValues.keys()) { let otherUtilities = new Set() @@ -327,6 +367,12 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st } } + for (let value of propertyValues.get(property)) { + for (let candidate of dynamicUtilities.get(candidates[idx]).get(property).get(value)) { + otherUtilities.add(candidate) + } + } + if (result === null) result = otherUtilities else result = intersection(result, otherUtilities)