Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Support `borderRadius.*` as an alias for `--radius-*` when using dot notation inside the `theme()` function ([#14436](https://github.com/tailwindlabs/tailwindcss/pull/14436))
- Ensure individual variants from groups are always sorted earlier than stacked variants from the same groups ([#14431](https://github.com/tailwindlabs/tailwindcss/pull/14431))

## [4.0.0-alpha.24] - 2024-09-11

Expand Down
48 changes: 24 additions & 24 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1404,13 +1404,13 @@ describe('matchVariant', () => {

expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.potato-yellow .potato-\\[yellow\\]\\:underline {
text-decoration-line: underline;
}

.potato-baked .potato-\\[baked\\]\\:flex {
display: flex;
}

.potato-yellow .potato-\\[yellow\\]\\:underline {
text-decoration-line: underline;
}
}"
`)
})
Expand All @@ -1435,17 +1435,17 @@ describe('matchVariant', () => {

expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
@media (potato: yellow) {
.potato-\\[yellow\\]\\:underline {
text-decoration-line: underline;
}
}

@media (potato: baked) {
.potato-\\[baked\\]\\:flex {
display: flex;
}
}

@media (potato: yellow) {
.potato-\\[yellow\\]\\:underline {
text-decoration-line: underline;
}
}
}"
`)
})
Expand Down Expand Up @@ -1473,18 +1473,18 @@ describe('matchVariant', () => {

expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
@media (potato: yellow) {
@media (potato: baked) {
@supports (font: bold) {
.potato-\\[yellow\\]\\:underline:large-potato {
text-decoration-line: underline;
.potato-\\[baked\\]\\:flex:large-potato {
display: flex;
}
}
}

@media (potato: baked) {
@media (potato: yellow) {
@supports (font: bold) {
.potato-\\[baked\\]\\:flex:large-potato {
display: flex;
.potato-\\[yellow\\]\\:underline:large-potato {
text-decoration-line: underline;
}
}
}
Expand Down Expand Up @@ -1541,10 +1541,10 @@ describe('matchVariant', () => {
return ({ matchVariant }: PluginAPI) => {
matchVariant('alphabet', (side) => `&${side}`, {
values: {
a: '[data-value="a"]',
b: '[data-value="b"]',
c: '[data-value="c"]',
d: '[data-value="d"]',
d: '[data-order="1"]',
a: '[data-order="2"]',
c: '[data-order="3"]',
b: '[data-order="4"]',
},
})
}
Expand All @@ -1560,19 +1560,19 @@ describe('matchVariant', () => {

expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.alphabet-a\\:underline[data-value="a"] {
.alphabet-d\\:underline[data-order="1"] {
text-decoration-line: underline;
}

.alphabet-b\\:underline[data-value="b"] {
.alphabet-a\\:underline[data-order="2"] {
text-decoration-line: underline;
}

.alphabet-c\\:underline[data-value="c"] {
.alphabet-c\\:underline[data-order="3"] {
text-decoration-line: underline;
}

.alphabet-d\\:underline[data-value="d"] {
.alphabet-b\\:underline[data-order="4"] {
text-decoration-line: underline;
}
}"
Expand Down
7 changes: 4 additions & 3 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ export function buildPluginApi(
return 0
}

if (options && typeof options.sort === 'function') {
let aValue = options.values?.[a.value.value] ?? a.value.value
let zValue = options.values?.[z.value.value] ?? z.value.value
let aValue = options?.values?.[a.value.value] ?? a.value.value
let zValue = options?.values?.[z.value.value] ?? z.value.value

if (options && typeof options.sort === 'function') {
return options.sort(
{ value: aValue, modifier: a.modifier?.value ?? null },
{ value: zValue, modifier: z.modifier?.value ?? null },
Expand All @@ -163,6 +163,7 @@ export function buildPluginApi(
let aOrder = defaultOptionKeys.indexOf(a.value.value)
let zOrder = defaultOptionKeys.indexOf(z.value.value)

if (aOrder - zOrder === 0) return aValue < zValue ? -1 : 1
return aOrder - zOrder
},
)
Expand Down
7 changes: 2 additions & 5 deletions packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ export function compileCandidates(
matches.set(rawCandidate, candidates)
}

// Sort the variants
let variants = designSystem.getUsedVariants().sort((a, z) => {
return designSystem.variants.compare(a, z)
})
let variantOrderMap = designSystem.getVariantOrder()

// Create the AST
for (let [rawCandidate, candidates] of matches) {
Expand All @@ -51,7 +48,7 @@ export function compileCandidates(
// variants used.
let variantOrder = 0n
for (let variant of candidate.variants) {
variantOrder |= 1n << BigInt(variants.indexOf(variant))
variantOrder |= 1n << BigInt(variantOrderMap.get(variant)!)
}

nodeSorting.set(node, {
Expand Down
35 changes: 29 additions & 6 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toCss } from './ast'
import { parseCandidate, parseVariant, type Candidate } from './candidate'
import { parseCandidate, parseVariant, type Candidate, type Variant } from './candidate'
import { compileAstNodes, compileCandidates } from './compile'
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
import { getClassOrder } from './sort'
Expand All @@ -13,17 +13,19 @@ export type DesignSystem = {
utilities: Utilities
variants: Variants

candidatesToCss(classes: string[]): (string | null)[]
getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]

parseCandidate(candidate: string): Candidate[]
parseVariant(variant: string): ReturnType<typeof parseVariant>
parseVariant(variant: string): Variant | null
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>

getUsedVariants(): ReturnType<typeof parseVariant>[]
getVariantOrder(): Map<Variant, number>
resolveThemeValue(path: string): string | undefined

// Used by IntelliSense
candidatesToCss(classes: string[]): (string | null)[]
}

export function buildDesignSystem(theme: Theme): DesignSystem {
Expand Down Expand Up @@ -77,8 +79,29 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
compileAstNodes(candidate: Candidate) {
return compiledAstNodes.get(candidate)
},
getUsedVariants() {
return Array.from(parsedVariants.values())
getVariantOrder() {
let variants = Array.from(parsedVariants.values())
variants.sort((a, z) => this.variants.compare(a, z))

let order = new Map<Variant, number>()
let prevVariant: Variant | undefined = undefined
let index: number = 0

for (let variant of variants) {
if (variant === null) {
continue
}
// This variant is not the same order as the previous one
// so it goes into a new group
if (prevVariant !== undefined && this.variants.compare(prevVariant, variant) !== 0) {
index++
}

order.set(variant, index)
prevVariant = variant
}

return order
Comment on lines +82 to +104
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecrypticace came up with this nugget: We can use aMap<Variant, number> which helps us:

  • Avoid creating n sets
  • Avoid doing a linear search in the compile function for constant lookups

},

resolveThemeValue(path: `${ThemeKey}` | `${ThemeKey}${string}`) {
Expand Down
73 changes: 62 additions & 11 deletions packages/tailwindcss/src/variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,57 @@ test('sorting stacked min-* and max-* variants', async () => {
`)
})

test('stacked min-* and max-* variants should come after unprefixed variants', async () => {
expect(
await compileCss(
css`
@theme {
/* Explicitly ordered in a strange way */
--breakpoint-sm: 640px;
--breakpoint-lg: 1024px;
--breakpoint-md: 768px;
}
@tailwind utilities;
`,
['sm:flex', 'min-sm:max-lg:flex', 'md:flex', 'min-md:max-lg:flex'],
),
).toMatchInlineSnapshot(`
":root {
--breakpoint-sm: 640px;
--breakpoint-lg: 1024px;
--breakpoint-md: 768px;
}

@media (width >= 640px) {
.sm\\:flex {
display: flex;
}
}

@media (width >= 640px) {
@media (width < 1024px) {
.min-sm\\:max-lg\\:flex {
display: flex;
}
}
}

@media (width >= 768px) {
.md\\:flex {
display: flex;
}
}

@media (width >= 768px) {
@media (width < 1024px) {
.min-md\\:max-lg\\:flex {
display: flex;
}
}
}"
Comment on lines +1289 to +1315
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now has the non-stacked rules (sm:flex and md:flex) before the stacked ones, regardless of sm or md being used.

`)
})

test('min, max and unprefixed breakpoints', async () => {
expect(
await compileCss(
Expand Down Expand Up @@ -2246,14 +2297,14 @@ test('container queries', async () => {
--width-lg: 1024px;
}

@container (width < 1024px) {
.\\@max-lg\\:flex {
@container name (width < 1024px) {
.\\@max-lg\\/name\\:flex {
display: flex;
}
}

@container name (width < 1024px) {
.\\@max-lg\\/name\\:flex {
@container (width < 1024px) {
.\\@max-lg\\:flex {
display: flex;
}
Comment on lines +2300 to 2309
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the @max container queries are using the compareBreakpoints() helper which will return 0 for the same numeric value, effectively ignoring the value of the modifier. I guess this makes this "undefined behavior". The new behavior now matches what we do for group- compounds variants though (where we first show the ones having a name followed by those that won't), so this seems like a reasonable change.

Previously, both the named and non-named variant would be getting a separate variant order but now they follow through and are ordered based on the raw candidate name I think.

}
Expand Down Expand Up @@ -2294,20 +2345,14 @@ test('container queries', async () => {
}
}

@container (width >= 1024px) {
.\\@lg\\:flex {
display: flex;
}
}

@container name (width >= 1024px) {
.\\@lg\\/name\\:flex {
display: flex;
}
}

@container (width >= 1024px) {
.\\@min-lg\\:flex {
.\\@lg\\:flex {
display: flex;
}
}
Expand All @@ -2316,6 +2361,12 @@ test('container queries', async () => {
.\\@min-lg\\/name\\:flex {
display: flex;
}
}

@container (width >= 1024px) {
.\\@min-lg\\:flex {
display: flex;
}
}"
`)
})
Expand Down
17 changes: 13 additions & 4 deletions packages/tailwindcss/src/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,22 @@ export class Variants {
if (orderedByVariant !== 0) return orderedByVariant

if (a.kind === 'compound' && z.kind === 'compound') {
return this.compare(a.variant, z.variant)
let order = this.compare(a.variant, z.variant)
if (order === 0) {
if (a.modifier && z.modifier) {
return a.modifier.value < z.modifier.value ? -1 : 1
} else if (a.modifier) {
return 1
} else if (z.modifier) {
return -1
}
}
Comment on lines +120 to +129
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now explicitly handles named modifiers for compound variants, so named groups will appear before their equivalent unnamed group.

return order
}

let compareFn = this.compareFns.get(aOrder)
if (compareFn === undefined) return 0

return compareFn(a, z) || (a.root < z.root ? -1 : 1)
if (compareFn === undefined) return a.root < z.root ? -1 : 1
return compareFn(a, z)
}

keys() {
Expand Down