diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f984fc00c9..54f6806d3de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Ensure `scale-*` utilities support percentage values ([#13182](https://github.com/tailwindlabs/tailwindcss/pull/13182)) +- Prevent `content-none` from being overridden when conditionally styling `::before`/`::after` ([#13187](https://github.com/tailwindlabs/tailwindcss/pull/13187)) +- Remove default borders from `iframe` elements ([#13189](https://github.com/tailwindlabs/tailwindcss/pull/13189)) + +### Changed + +- Replace `--radius-none` and `--radius-full` theme values with static `rounded-none` and `rounded-full` utilities ([#13186](https://github.com/tailwindlabs/tailwindcss/pull/13186)) ## [4.0.0-alpha.7] - 2024-03-08 diff --git a/oxide/crates/core/src/lib.rs b/oxide/crates/core/src/lib.rs index 87327f39a164..6f04adae0a0c 100644 --- a/oxide/crates/core/src/lib.rs +++ b/oxide/crates/core/src/lib.rs @@ -433,9 +433,9 @@ fn read_all_files_sync(changed_content: Vec) -> Vec> { changed_content .into_iter() - .map(|c| match (c.file, c.content) { - (Some(file), None) => std::fs::read(file).unwrap(), - (None, Some(content)) => content.into_bytes(), + .filter_map(|c| match (c.file, c.content) { + (Some(file), None) => std::fs::read(file).ok(), + (None, Some(content)) => Some(content.into_bytes()), _ => Default::default(), }) .collect() diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index f3627ff9144e..d9298485184d 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -100,7 +100,8 @@ export async function handle(args: Result>) { ) // Compile the input - let result = compile(input, candidates) + let { build } = compile(input) + let result = build(candidates) // Optimize the output if (args['--minify'] || args['--optimize']) { @@ -193,7 +194,7 @@ export async function handle(args: Result>) { } // Compile the input - let result = compile(input, candidates) + result = compile(input).build(candidates) // Optimize the output if (args['--minify'] || args['--optimize']) { diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap index fd246450beb6..e581e4bb5815 100644 --- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -271,10 +271,8 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; - --radius-none: 0px; - --radius-full: 9999px; - --radius-sm: .125rem; --radius: .25rem; + --radius-sm: .125rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; @@ -404,16 +402,18 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` } @layer base { - *, :after, :before, ::backdrop { + *, :after, :before, ::backdrop, :first-letter { box-sizing: border-box; + border: 0 solid; + margin: 0; + padding: 0; } ::file-selector-button { box-sizing: border-box; - } - - * { + border: 0 solid; margin: 0; + padding: 0; } html, :host { @@ -432,7 +432,6 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` hr { color: inherit; - border: 0 solid; border-top-width: 1px; height: 0; } @@ -496,8 +495,6 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` font-variation-settings: inherit; color: inherit; background: none; - border: 1px solid; - padding: 0; } ::file-selector-button { @@ -506,18 +503,18 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` font-variation-settings: inherit; color: inherit; background: none; + } + + input:where(:not([type="button"], [type="reset"], [type="submit"])), select, textarea { border: 1px solid; - padding: 0; } button, input:where([type="button"], [type="reset"], [type="submit"]) { appearance: button; - border: 0; } ::file-selector-button { appearance: button; - border: 0; } :-moz-focusring { @@ -548,24 +545,10 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` display: list-item; } - fieldset { - border: 0; - padding: 0; - } - - legend { - padding: 0; - } - ol, ul, menu { - padding: 0; list-style: none; } - dialog { - padding: 0; - } - textarea { resize: vertical; } diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 73621836de53..303ffb92414b 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -53,7 +53,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // No `@tailwind` means we don't have to look for candidates if (!hasTailwind) { - replaceCss(compile(root.toString(), [])) + replaceCss(compile(root.toString()).build([])) return } @@ -83,7 +83,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - replaceCss(compile(root.toString(), candidates)) + replaceCss(compile(root.toString()).build(candidates)) }, ], } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 6842aa7de037..64cfa3036b2d 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -63,7 +63,7 @@ export default function tailwindcss(): Plugin[] { } function generateCss(css: string) { - return compile(css, Array.from(candidates)) + return compile(css).build(Array.from(candidates)) } function generateOptimizedCss(css: string) { diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css index 9590f6200cea..82a8bb58b2bc 100644 --- a/packages/tailwindcss/preflight.css +++ b/packages/tailwindcss/preflight.css @@ -1,21 +1,19 @@ /* - Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. */ *, ::after, ::before, ::backdrop, +::first-letter, ::file-selector-button { - box-sizing: border-box; -} - -/* - Remove any default margins. -*/ - -* { - margin: 0; + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ } /* @@ -65,7 +63,6 @@ body { hr { height: 0; /* 1 */ color: inherit; /* 2 */ - border: 0 solid; /* 3 */ border-top-width: 1px; /* 3 */ } @@ -180,9 +177,7 @@ table { /* 1. Inherit the font styles in all browsers. - 2. Reset the default inset border style to solid. - 3. Remove the default background color. - 4. Remove default padding. + 2. Remove the default background color. */ button, @@ -195,20 +190,26 @@ textarea, font-feature-settings: inherit; /* 1 */ font-variation-settings: inherit; /* 1 */ color: inherit; /* 1 */ - border: 1px solid; /* 2 */ - background: transparent; /* 3 */ - padding: 0; /* 4 */ + background: transparent; /* 2 */ +} + +/* + Reset the default inset border style for form controls to solid. +*/ + +input:where(:not([type='button'], [type='reset'], [type='submit'])), +select, +textarea { + border: 1px solid; } /* - 1. Correct the inability to style the border radius in iOS Safari. - 2. Make borders opt-in. + Correct the inability to style the border radius in iOS Safari. */ button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { - appearance: button; /* 1 */ - border: 0; /* 2 */ + appearance: button; } /* @@ -260,19 +261,6 @@ summary { display: list-item; } -/* - Remove the default border and spacing for fieldset and legend elements. -*/ - -fieldset { - border: 0; - padding: 0; -} - -legend { - padding: 0; -} - /* Make lists unstyled by default. */ @@ -281,15 +269,6 @@ ol, ul, menu { list-style: none; - padding: 0; -} - -/* - Remove the default padding from dialog elements. -*/ - -dialog { - padding: 0; } /* diff --git a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap index f8f0e86859e4..ccaf12fe76dc 100644 --- a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap @@ -270,10 +270,8 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; - --radius-none: 0px; - --radius-full: 9999px; - --radius-sm: .125rem; --radius: .25rem; + --radius-sm: .125rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 017025da96ef..f4f1ad0bff6d 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -1244,6 +1244,36 @@ exports[`getClassList 1`] = ` "rotate-45", "rotate-6", "rotate-90", + "rounded-b-full", + "rounded-b-none", + "rounded-bl-full", + "rounded-bl-none", + "rounded-br-full", + "rounded-br-none", + "rounded-e-full", + "rounded-e-none", + "rounded-ee-full", + "rounded-ee-none", + "rounded-es-full", + "rounded-es-none", + "rounded-full", + "rounded-l-full", + "rounded-l-none", + "rounded-none", + "rounded-r-full", + "rounded-r-none", + "rounded-s-full", + "rounded-s-none", + "rounded-se-full", + "rounded-se-none", + "rounded-ss-full", + "rounded-ss-none", + "rounded-t-full", + "rounded-t-none", + "rounded-tl-full", + "rounded-tl-none", + "rounded-tr-full", + "rounded-tr-none", "row-auto", "row-end-1", "row-end-10", diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index d316c541196e..dae7cf0e8893 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -105,6 +105,13 @@ export function toCss(ast: AstNode[]) { return css } + if (node.selector === '@tailwind utilities') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + return css + } + // Print at-rules without nodes with a `;` instead of an empty block. // // E.g.: diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index f10e95d09cb2..f46dec7ec647 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -1,9 +1,8 @@ import { scanDir } from '@tailwindcss/oxide' import { bench } from 'vitest' -import { parseCandidate, parseVariant } from './candidate' +import { parseCandidate } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' -import { DefaultMap } from './utils/default-map' // FOLDER=path/to/folder vitest bench const root = process.env.FOLDER || process.cwd() @@ -15,10 +14,6 @@ const designSystem = buildDesignSystem(new Theme()) bench('parseCandidate', () => { for (let candidate of result.candidates) { - parseCandidate( - candidate, - designSystem.utilities, - new DefaultMap((variant, map) => parseVariant(variant, designSystem.variants, map)), - ) + parseCandidate(candidate, designSystem) } }) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 672e8cbb4551..17d1371b6e7c 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest' -import { parseCandidate, parseVariant } from './candidate' +import { buildDesignSystem } from './design-system' +import { Theme } from './theme' import { Utilities } from './utilities' -import { DefaultMap } from './utils/default-map' import { Variants } from './variants' function run( @@ -11,11 +11,12 @@ function run( utilities ??= new Utilities() variants ??= new Variants() - let parsedVariants = new DefaultMap((variant, map) => { - return parseVariant(variant, variants!, map) - }) + let designSystem = buildDesignSystem(new Theme()) - return parseCandidate(candidate, utilities, parsedVariants) + designSystem.utilities = utilities + designSystem.variants = variants + + return designSystem.parseCandidate(candidate) } it('should skip unknown utilities', () => { diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index e3f6b92f2827..03420e4d8bbc 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -1,3 +1,4 @@ +import type { DesignSystem } from './design-system' import { decodeArbitraryValue } from './utils/decode-arbitrary-value' import { segment } from './utils/segment' @@ -206,14 +207,7 @@ export type Candidate = important: boolean } -export function parseCandidate( - input: string, - utilities: { - has: (value: string) => boolean - kind: (root: string) => Omit - }, - parsedVariants: { get: (value: string) => Variant | null }, -): Candidate | null { +export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null { // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants // ^^^^^^^^^ -> Base @@ -228,7 +222,7 @@ export function parseCandidate( let parsedCandidateVariants: Variant[] = [] for (let variant of rawVariants) { - let parsedVariant = parsedVariants.get(variant) + let parsedVariant = designSystem.parseVariant(variant) if (parsedVariant === null) return null // Variants are applied left-to-right meaning that any representing pseudo- @@ -320,7 +314,7 @@ export function parseCandidate( base = base.slice(1) } - let [root, value] = findRoot(base, utilities) + let [root, value] = findRoot(base, designSystem.utilities) let modifierSegment: string | null = null @@ -335,13 +329,13 @@ export function parseCandidate( modifierSegment = rootModifierSegment // Try to find the root and value, without the modifier present - ;[root, value] = findRoot(rootWithoutModifier, utilities) + ;[root, value] = findRoot(rootWithoutModifier, designSystem.utilities) } // If there's no root, the candidate isn't a valid class and can be discarded. if (root === null) return null - let kind = utilities.kind(root) + let kind = designSystem.utilities.kind(root) if (kind === 'static') { if (value !== null) return null @@ -475,15 +469,7 @@ function parseModifier(modifier: string): CandidateModifier { } } -export function parseVariant( - variant: string, - variants: { - has: (value: string) => boolean - kind: (root: string) => Omit - compounds: (root: string) => boolean - }, - parsedVariants: { get: (value: string) => Variant | null }, -): Variant | null { +export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null { // Arbitrary variants if (variant[0] === '[' && variant[variant.length - 1] === ']') { /** @@ -535,20 +521,20 @@ export function parseVariant( // - `group-hover/foo/bar` if (additionalModifier) return null - let [root, value] = findRoot(variantWithoutModifier, variants) + let [root, value] = findRoot(variantWithoutModifier, designSystem.variants) // Variant is invalid, therefore the candidate is invalid and we can skip // continue parsing it. if (root === null) return null - switch (variants.kind(root)) { + switch (designSystem.variants.kind(root)) { case 'static': { if (value !== null) return null return { kind: 'static', root, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } @@ -564,7 +550,7 @@ export function parseVariant( kind: 'arbitrary', value: decodeArbitraryValue(value.slice(1, -1)), }, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } @@ -573,14 +559,14 @@ export function parseVariant( root, modifier: modifier === null ? null : parseModifier(modifier), value: { kind: 'named', value }, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } case 'compound': { if (value === null) return null - let subVariant = parsedVariants.get(value) + let subVariant = designSystem.parseVariant(value) if (subVariant === null) return null if (subVariant.compounds === false) return null @@ -589,7 +575,7 @@ export function parseVariant( root, modifier: modifier === null ? null : { kind: 'named', value: modifier }, variant: subVariant, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index b91c19acab11..ebbe2a74c52b 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,120 +1,52 @@ import { rule, type AstNode, type Rule } from './ast' -import { parseCandidate, parseVariant, type Candidate, type Variant } from './candidate' +import { type Candidate, type Variant } from './candidate' import { type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' -import { DefaultMap } from './utils/default-map' import { escape } from './utils/escape' import type { Variants } from './variants' export function compileCandidates( - rawCandidates: string[], + rawCandidates: Iterable, designSystem: DesignSystem, - { throwOnInvalidCandidate = false } = {}, + { onInvalidCandidate }: { onInvalidCandidate?: (candidate: string) => void } = {}, ) { - // Ensure the candidates are sorted alphabetically - rawCandidates.sort() - let nodeSorting = new Map< AstNode, { properties: number[]; variants: bigint; candidate: string } >() let astNodes: AstNode[] = [] - - // A lazy map implementation that will return the variant if it exists. If it - // doesn't exist yet, the raw string variant will be parsed and added to the - // map. - let parsedVariants: DefaultMap = new DefaultMap((variant, map) => { - return parseVariant(variant, designSystem.variants, map) - }) - let candidates = new Map() // Parse candidates and variants for (let rawCandidate of rawCandidates) { - let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants) + let candidate = designSystem.parseCandidate(rawCandidate) if (candidate === null) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } + onInvalidCandidate?.(rawCandidate) continue // Bail, invalid candidate } candidates.set(candidate, rawCandidate) } // Sort the variants - let variants = Array.from(parsedVariants.values()).sort((a, z) => { + let variants = designSystem.getUsedVariants().sort((a, z) => { return designSystem.variants.compare(a, z) }) // Create the AST next: for (let [candidate, rawCandidate] of candidates) { - let nodes: AstNode[] = [] - - // Handle arbitrary properties - if (candidate.kind === 'arbitrary') { - let compileFn = designSystem.utilities.getArbitrary() - - // Build the node - let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - nodes = compiledNodes + let astNode = designSystem.compileAstNodes(rawCandidate) + if (astNode === null) { + onInvalidCandidate?.(rawCandidate) + continue next } - // Handle named utilities - else if (candidate.kind === 'static' || candidate.kind === 'functional') { - // Safety: At this point it is safe to use TypeScript's non-null assertion - // operator because if the `candidate.root` didn't exist, `parseCandidate` - // would have returned `null` and we would have returned early resulting - // in not hitting this code path. - let { compileFn } = designSystem.utilities.get(candidate.root)! - - // Build the node - let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - nodes = compiledNodes - } - - let propertySort = getPropertySort(nodes) - - if (candidate.important) { - applyImportant(nodes) - } - - let node: Rule = { - kind: 'rule', - selector: `.${escape(rawCandidate)}`, - nodes, - } + let { node, propertySort } = astNode + // Track the variant order which is a number with each bit representing a + // variant. This allows us to sort the rules based on the order of + // variants used. let variantOrder = 0n for (let variant of candidate.variants) { - let result = applyVariant(node, variant, designSystem.variants) - - // When the variant results in `null`, it means that the variant cannot be - // applied to the rule. Discard the candidate and continue to the next - // one. - if (result === null) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - // Track the variant order which is a number with each bit representing a - // variant. This allows us to sort the rules based on the order of - // variants used. variantOrder |= 1n << BigInt(variants.indexOf(variant)) } @@ -153,7 +85,9 @@ export function compileCandidates( // Sort by lowest property index first (aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) || // Sort by most properties first, then by least properties - zSorting.properties.length - aSorting.properties.length + zSorting.properties.length - aSorting.properties.length || + // Sort alphabetically + (aSorting.candidate < zSorting.candidate ? -1 : 1) ) }) @@ -163,6 +97,65 @@ export function compileCandidates( } } +export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem) { + let candidate = designSystem.parseCandidate(rawCandidate) + if (candidate === null) return null + + let nodes: AstNode[] = [] + + // Handle arbitrary properties + if (candidate.kind === 'arbitrary') { + let compileFn = designSystem.utilities.getArbitrary() + + // Build the node + let compiledNodes = compileFn(candidate) + if (compiledNodes === undefined) return null + + nodes = compiledNodes + } + + // Handle named utilities + else if (candidate.kind === 'static' || candidate.kind === 'functional') { + // Safety: At this point it is safe to use TypeScript's non-null assertion + // operator because if the `candidate.root` didn't exist, `parseCandidate` + // would have returned `null` and we would have returned early resulting + // in not hitting this code path. + let { compileFn } = designSystem.utilities.get(candidate.root)! + + // Build the node + let compiledNodes = compileFn(candidate) + if (compiledNodes === undefined) return null + + nodes = compiledNodes + } + + let propertySort = getPropertySort(nodes) + + if (candidate.important) { + applyImportant(nodes) + } + + let node: Rule = { + kind: 'rule', + selector: `.${escape(rawCandidate)}`, + nodes, + } + + for (let variant of candidate.variants) { + let result = applyVariant(node, variant, designSystem.variants) + + // When the variant results in `null`, it means that the variant cannot be + // applied to the rule. Discard the candidate and continue to the next + // one. + if (result === null) return null + } + + return { + node, + propertySort, + } +} + export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void { if (variant.kind === 'arbitrary') { node.nodes = [rule(variant.selector, node.nodes)] diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 190c5e7e244c..bebd82a33c23 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,9 +1,11 @@ import { toCss } from './ast' -import { compileCandidates } from './compile' +import { parseCandidate, parseVariant } from './candidate' +import { compileAstNodes, compileCandidates } from './compile' import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense' import { getClassOrder } from './sort' import type { Theme } from './theme' import { Utilities, createUtilities } from './utilities' +import { DefaultMap } from './utils/default-map' import { Variants, createVariants } from './variants' export type DesignSystem = { @@ -15,19 +17,32 @@ export type DesignSystem = { getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] + + parseCandidate(candidate: string): ReturnType + parseVariant(variant: string): ReturnType + compileAstNodes(candidate: string): ReturnType + + getUsedVariants(): ReturnType[] } export function buildDesignSystem(theme: Theme): DesignSystem { - return { + let utilities = createUtilities(theme) + let variants = createVariants(theme) + + let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) + let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) + let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem)) + + let designSystem: DesignSystem = { theme, - utilities: createUtilities(theme), - variants: createVariants(theme), + utilities, + variants, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] for (let className of classes) { - let { astNodes } = compileCandidates([className], this, { throwOnInvalidCandidate: false }) + let { astNodes } = compileCandidates([className], this) if (astNodes.length === 0) { result.push(null) } else { @@ -47,5 +62,20 @@ export function buildDesignSystem(theme: Theme): DesignSystem { getVariants() { return getVariants(this) }, + + parseCandidate(candidate: string) { + return parsedCandidates.get(candidate) + }, + parseVariant(variant: string) { + return parsedVariants.get(variant) + }, + compileAstNodes(candidate: string) { + return compiledAstNodes.get(candidate) + }, + getUsedVariants() { + return Array.from(parsedVariants.values()) + }, } + + return designSystem } diff --git a/packages/tailwindcss/src/index.bench.ts b/packages/tailwindcss/src/index.bench.ts index 1953aa19525b..6ec69f2f62cb 100644 --- a/packages/tailwindcss/src/index.bench.ts +++ b/packages/tailwindcss/src/index.bench.ts @@ -9,10 +9,7 @@ const css = String.raw bench('compile', async () => { let { candidates } = scanDir({ base: root, globs: true }) - compile( - css` - @tailwind utilities; - `, - candidates, - ) + compile(css` + @tailwind utilities; + `).build(candidates) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index cf7025102e5b..1488b4817747 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -93,9 +93,7 @@ describe('compiling CSS', () => { .grid { display: grid; - } - - @tailwind utilities;" + }" `) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d066a5a1f335..9678028f0216 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -6,13 +6,21 @@ import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' -export function compile(css: string, rawCandidates: string[]) { +export function compile(css: string): { + build(candidates: string[]): string +} { let ast = CSS.parse(css) if (process.env.NODE_ENV !== 'test') { ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `)) } + // Track all invalid candidates + let invalidCandidates = new Set() + function onInvalidCandidate(candidate: string) { + invalidCandidates.add(candidate) + } + // Find all `@theme` declarations let theme = new Theme() let firstThemeRule: Rule | null = null @@ -97,11 +105,14 @@ export function compile(css: string, rawCandidates: string[]) { let designSystem = buildDesignSystem(theme) + let tailwindUtilitiesNode: Rule | null = null + // Find `@tailwind utilities` and replace it with the actual generated utility // class CSS. - walk(ast, (node, { replaceWith }) => { + walk(ast, (node) => { if (node.kind === 'rule' && node.selector === '@tailwind utilities') { - replaceWith(compileCandidates(rawCandidates, designSystem).astNodes) + tailwindUtilitiesNode = node + // Stop walking after finding `@tailwind utilities` to avoid walking all // of the generated CSS. This means `@tailwind utilities` can only appear // once per file but that's the intended usage at this point in time. @@ -122,7 +133,9 @@ export function compile(css: string, rawCandidates: string[]) { { // Parse the candidates to an AST that we can replace the `@apply` rule with. let candidateAst = compileCandidates(candidates, designSystem, { - throwOnInvalidCandidate: true, + onInvalidCandidate: (candidate) => { + throw new Error(`Cannot apply unknown utility class: ${candidate}`) + }, }).astNodes // Collect the nodes to insert in place of the `@apply` rule. When a @@ -162,7 +175,53 @@ export function compile(css: string, rawCandidates: string[]) { }) } - return toCss(ast) + // Track all valid candidates, these are the incoming `rawCandidate` that + // resulted in a generated AST Node. All the other `rawCandidates` are invalid + // and should be ignored. + let allValidCandidates = new Set() + let compiledCss = toCss(ast) + let previousAstNodeCount = 0 + + return { + build(newRawCandidates: string[]) { + let didChange = false + + // Add all new candidates unless we know that they are invalid. + let prevSize = allValidCandidates.size + for (let candidate of newRawCandidates) { + if (!invalidCandidates.has(candidate)) { + allValidCandidates.add(candidate) + didChange ||= allValidCandidates.size !== prevSize + } + } + + // If no new candidates were added, we can return the original CSS. This + // currently assumes that we only add new candidates and never remove any. + if (!didChange) { + return compiledCss + } + + if (tailwindUtilitiesNode) { + let newNodes = compileCandidates(allValidCandidates, designSystem, { + onInvalidCandidate, + }).astNodes + + // If no new ast nodes were generated, then we can return the original + // CSS. This currently assumes that we only add new ast nodes and never + // remove any. + if (previousAstNodeCount === newNodes.length) { + return compiledCss + } + + previousAstNodeCount = newNodes.length + + tailwindUtilitiesNode.nodes = newNodes + compiledCss = toCss(ast) + } + + return compiledCss + }, + } } export function optimizeCss( diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 8b67641c6d42..4b1d814fb917 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -1,8 +1,6 @@ import { decl, rule } from './ast' -import { parseVariant, type Variant } from './candidate' import { applyVariant } from './compile' import type { DesignSystem } from './design-system' -import { DefaultMap } from './utils/default-map' interface ClassMetadata { modifiers: string[] @@ -40,7 +38,7 @@ export function getClassList(design: DesignSystem): ClassEntry[] { } } - list.sort((a, b) => a[0].localeCompare(b[0])) + list.sort((a, b) => (a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1)) return list } @@ -60,9 +58,6 @@ export interface VariantEntry { export function getVariants(design: DesignSystem) { let list: VariantEntry[] = [] - let parsedVariants = new DefaultMap((variant, map) => - parseVariant(variant, design.variants, map), - ) for (let [root, variant] of design.variants.entries()) { if (variant.kind === 'arbitrary') continue @@ -74,7 +69,7 @@ export function getVariants(design: DesignSystem) { if (value) name += `-${value}` if (modifier) name += `/${modifier}` - let variant = parsedVariants.get(name) + let variant = design.parseVariant(name) if (!variant) return [] diff --git a/packages/tailwindcss/src/sort.ts b/packages/tailwindcss/src/sort.ts index b3434ed094dd..e15c4f02ef51 100644 --- a/packages/tailwindcss/src/sort.ts +++ b/packages/tailwindcss/src/sort.ts @@ -3,9 +3,7 @@ import type { DesignSystem } from './design-system' export function getClassOrder(design: DesignSystem, classes: string[]): [string, bigint | null][] { // Generate a sorted AST - let { astNodes, nodeSorting } = compileCandidates(Array.from(classes), design, { - throwOnInvalidCandidate: false, - }) + let { astNodes, nodeSorting } = compileCandidates(Array.from(classes), design) // Map class names to their order in the AST // `null` indicates a non-Tailwind class diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 8df6feab2559..0395604824e3 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -1,9 +1,9 @@ import { compile, optimizeCss } from '..' export function compileCss(css: string, candidates: string[] = []) { - return optimizeCss(compile(css, candidates)).trim() + return optimizeCss(compile(css).build(candidates)).trim() } export function run(candidates: string[]) { - return optimizeCss(compile('@tailwind utilities;', candidates)).trim() + return optimizeCss(compile('@tailwind utilities;').build(candidates)).trim() } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 5d92d89fb92f..0873766a4bf6 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -5787,8 +5787,6 @@ test('rounded', () => { compileCss( css` @theme { - --radius-none: 0px; - --radius-full: 9999px; --radius-sm: 0.125rem; --radius: 0.25rem; } @@ -5798,8 +5796,6 @@ test('rounded', () => { ), ).toMatchInlineSnapshot(` ":root { - --radius-none: 0px; - --radius-full: 9999px; --radius-sm: .125rem; --radius: .25rem; } @@ -5813,7 +5809,7 @@ test('rounded', () => { } .rounded-full { - border-radius: 9999px; + border-radius: 3.40282e38px; } .rounded-none { @@ -5862,8 +5858,8 @@ test('rounded-s', () => { } .rounded-s-full { - border-start-start-radius: 9999px; - border-end-start-radius: 9999px; + border-start-start-radius: 3.40282e38px; + border-end-start-radius: 3.40282e38px; } .rounded-s-none { @@ -5914,8 +5910,8 @@ test('rounded-e', () => { } .rounded-e-full { - border-start-end-radius: 9999px; - border-end-end-radius: 9999px; + border-start-end-radius: 3.40282e38px; + border-end-end-radius: 3.40282e38px; } .rounded-e-none { @@ -5966,8 +5962,8 @@ test('rounded-t', () => { } .rounded-t-full { - border-top-left-radius: 9999px; - border-top-right-radius: 9999px; + border-top-left-radius: 3.40282e38px; + border-top-right-radius: 3.40282e38px; } .rounded-t-none { @@ -6018,8 +6014,8 @@ test('rounded-r', () => { } .rounded-r-full { - border-top-right-radius: 9999px; - border-bottom-right-radius: 9999px; + border-top-right-radius: 3.40282e38px; + border-bottom-right-radius: 3.40282e38px; } .rounded-r-none { @@ -6070,8 +6066,8 @@ test('rounded-b', () => { } .rounded-b-full { - border-bottom-right-radius: 9999px; - border-bottom-left-radius: 9999px; + border-bottom-right-radius: 3.40282e38px; + border-bottom-left-radius: 3.40282e38px; } .rounded-b-none { @@ -6122,8 +6118,8 @@ test('rounded-l', () => { } .rounded-l-full { - border-top-left-radius: 9999px; - border-bottom-left-radius: 9999px; + border-top-left-radius: 3.40282e38px; + border-bottom-left-radius: 3.40282e38px; } .rounded-l-none { @@ -6172,7 +6168,7 @@ test('rounded-ss', () => { } .rounded-ss-full { - border-start-start-radius: 9999px; + border-start-start-radius: 3.40282e38px; } .rounded-ss-none { @@ -6225,7 +6221,7 @@ test('rounded-se', () => { } .rounded-se-full { - border-start-end-radius: 9999px; + border-start-end-radius: 3.40282e38px; } .rounded-se-none { @@ -6278,7 +6274,7 @@ test('rounded-ee', () => { } .rounded-ee-full { - border-end-end-radius: 9999px; + border-end-end-radius: 3.40282e38px; } .rounded-ee-none { @@ -6331,7 +6327,7 @@ test('rounded-es', () => { } .rounded-es-full { - border-end-start-radius: 9999px; + border-end-start-radius: 3.40282e38px; } .rounded-es-none { @@ -6384,7 +6380,7 @@ test('rounded-tl', () => { } .rounded-tl-full { - border-top-left-radius: 9999px; + border-top-left-radius: 3.40282e38px; } .rounded-tl-none { @@ -6437,7 +6433,7 @@ test('rounded-tr', () => { } .rounded-tr-full { - border-top-right-radius: 9999px; + border-top-right-radius: 3.40282e38px; } .rounded-tr-none { @@ -6490,7 +6486,7 @@ test('rounded-br', () => { } .rounded-br-full { - border-bottom-right-radius: 9999px; + border-bottom-right-radius: 3.40282e38px; } .rounded-br-none { @@ -6543,7 +6539,7 @@ test('rounded-bl', () => { } .rounded-bl-full { - border-bottom-left-radius: 9999px; + border-bottom-left-radius: 3.40282e38px; } .rounded-bl-none { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 87589a7ce168..d0c1e32ffb9c 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -1954,99 +1954,39 @@ export function createUtilities(theme: Theme) { staticUtility('break-all', [['word-break', 'break-all']]) staticUtility('break-keep', [['word-break', 'break-keep']]) - // rounded-* - functionalUtility('rounded', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-radius', value)], - }) - - functionalUtility('rounded-s', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-start-start-radius', value), - decl('border-end-start-radius', value), - ], - }) - - functionalUtility('rounded-e', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-start-end-radius', value), - decl('border-end-end-radius', value), - ], - }) - - functionalUtility('rounded-t', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-top-left-radius', value), - decl('border-top-right-radius', value), - ], - }) - - functionalUtility('rounded-r', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-top-right-radius', value), - decl('border-bottom-right-radius', value), - ], - }) - - functionalUtility('rounded-b', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-bottom-right-radius', value), - decl('border-bottom-left-radius', value), - ], - }) - - functionalUtility('rounded-l', { - themeKeys: ['--radius'], - handle: (value) => [ - decl('border-top-left-radius', value), - decl('border-bottom-left-radius', value), - ], - }) - - functionalUtility('rounded-ss', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-start-start-radius', value)], - }) - - functionalUtility('rounded-se', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-start-end-radius', value)], - }) - - functionalUtility('rounded-ee', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-end-end-radius', value)], - }) - - functionalUtility('rounded-es', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-end-start-radius', value)], - }) - - functionalUtility('rounded-tl', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-top-left-radius', value)], - }) - - functionalUtility('rounded-tr', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-top-right-radius', value)], - }) - - functionalUtility('rounded-br', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-bottom-right-radius', value)], - }) - - functionalUtility('rounded-bl', { - themeKeys: ['--radius'], - handle: (value) => [decl('border-bottom-left-radius', value)], - }) + { + // border-radius + for (let [root, properties] of [ + ['rounded', ['border-radius']], + ['rounded-s', ['border-start-start-radius', 'border-end-start-radius']], + ['rounded-e', ['border-start-end-radius', 'border-end-end-radius']], + ['rounded-t', ['border-top-left-radius', 'border-top-right-radius']], + ['rounded-r', ['border-top-right-radius', 'border-bottom-right-radius']], + ['rounded-b', ['border-bottom-right-radius', 'border-bottom-left-radius']], + ['rounded-l', ['border-top-left-radius', 'border-bottom-left-radius']], + ['rounded-ss', ['border-start-start-radius']], + ['rounded-se', ['border-start-end-radius']], + ['rounded-ee', ['border-end-end-radius']], + ['rounded-es', ['border-end-start-radius']], + ['rounded-tl', ['border-top-left-radius']], + ['rounded-tr', ['border-top-right-radius']], + ['rounded-br', ['border-bottom-right-radius']], + ['rounded-bl', ['border-bottom-left-radius']], + ] as const) { + staticUtility( + `${root}-none`, + properties.map((property) => [property, '0']), + ) + staticUtility( + `${root}-full`, + properties.map((property) => [property, 'calc(infinity * 1px)']), + ) + functionalUtility(root, { + themeKeys: ['--radius'], + handle: (value) => properties.map((property) => decl(property, value)), + }) + } + } staticUtility('border-solid', [ ['--tw-border-style', 'solid'], @@ -3490,7 +3430,10 @@ export function createUtilities(theme: Theme) { handle: (value) => [decl('will-change', value)], }) - staticUtility('content-none', [['content', 'none']]) + staticUtility('content-none', [ + ['--tw-content', 'none'], + ['content', 'none'], + ]) functionalUtility('content', { themeKeys: [], handle: (value) => [ diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 675fa69316f2..3df052b5adfb 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1280,7 +1280,13 @@ test('supports', () => { expect( run(['supports-gap:grid', 'supports-[display:grid]:flex', 'supports-[selector(A_>_B)]:flex']), ).toMatchInlineSnapshot(` - "@supports (display: grid) { + "@supports (gap: var(--tw)) { + .supports-gap\\:grid { + display: grid; + } + } + + @supports (display: grid) { .supports-\\[display\\:grid\\]\\:flex { display: flex; } @@ -1290,12 +1296,6 @@ test('supports', () => { .supports-\\[selector\\(A_\\>_B\\)\\]\\:flex { display: flex; } - } - - @supports (gap: var(--tw)) { - .supports-gap\\:grid { - display: grid; - } }" `) }) @@ -1322,11 +1322,11 @@ test('not', () => { display: flex; } - .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { + .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { display: flex; } - .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { + .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { display: flex; } @@ -1334,11 +1334,11 @@ test('not', () => { display: flex; } - .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { display: flex; } - .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { display: flex; }" `) @@ -1362,11 +1362,11 @@ test('has', () => { display: flex; } - .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { + .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { display: flex; } - .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { + .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { display: flex; } @@ -1374,11 +1374,11 @@ test('has', () => { display: flex; } - .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { display: flex; } - .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { display: flex; } @@ -1405,43 +1405,43 @@ test('aria', () => { 'peer-aria-checked/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { + ".group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { display: flex; } - .group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { + .group-aria-checked\\:flex:is(:where(.group)[aria-checked="true"] *) { display: flex; } - .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { + .group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { display: flex; } - .group-aria-checked\\:flex:is(:where(.group)[aria-checked="true"] *) { + .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { display: flex; } - .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { + .peer-aria-\\[modal\\]\\:flex:is(:where(.peer)[aria-modal] ~ *) { display: flex; } - .peer-aria-\\[modal\\]\\:flex:is(:where(.peer)[aria-modal] ~ *) { + .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *) { display: flex; } - .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { + .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { display: flex; } - .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *) { + .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { display: flex; } - .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { + .aria-checked\\:flex[aria-checked="true"] { display: flex; } - .aria-checked\\:flex[aria-checked="true"] { + .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { display: flex; }" `) @@ -1460,27 +1460,27 @@ test('data', () => { 'peer-data-[disabled]/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { + ".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { display: flex; } - .group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { + .group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { display: flex; } - .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { + .peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *) { display: flex; } - .peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *) { + .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { display: flex; } - .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + .data-disabled\\:flex[data-disabled] { display: flex; } - .data-disabled\\:flex[data-disabled] { + .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { display: flex; }" `) @@ -1567,14 +1567,14 @@ test('container queries', () => { --width-lg: 1024px; } - @container name (width < 1024px) { - .\\@max-lg\\/name\\:flex { + @container (width < 1024px) { + .\\@max-lg\\:flex { display: flex; } } - @container (width < 1024px) { - .\\@max-lg\\:flex { + @container name (width < 1024px) { + .\\@max-lg\\/name\\:flex { display: flex; } } @@ -1615,12 +1615,6 @@ test('container queries', () => { } } - @container name (width >= 1024px) { - .\\@lg\\/name\\:flex { - display: flex; - } - } - @container (width >= 1024px) { .\\@lg\\:flex { display: flex; @@ -1628,7 +1622,7 @@ test('container queries', () => { } @container name (width >= 1024px) { - .\\@min-lg\\/name\\:flex { + .\\@lg\\/name\\:flex { display: flex; } } @@ -1637,6 +1631,12 @@ test('container queries', () => { .\\@min-lg\\:flex { display: flex; } + } + + @container name (width >= 1024px) { + .\\@min-lg\\/name\\:flex { + display: flex; + } }" `) }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 3e5138c628a6..3cc0919ad278 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -94,7 +94,7 @@ export class Variants { if (z === null) return 1 if (a.kind === 'arbitrary' && z.kind === 'arbitrary') { - return a.selector.localeCompare(z.selector) + return a.selector < z.selector ? -1 : 1 } else if (a.kind === 'arbitrary') { return 1 } else if (z.kind === 'arbitrary') { @@ -114,7 +114,7 @@ export class Variants { let compareFn = this.compareFns.get(aOrder) if (compareFn === undefined) return 0 - return compareFn(a, z) + return compareFn(a, z) || (a.root < z.root ? -1 : 1) } keys() { @@ -469,7 +469,7 @@ export function createVariants(theme: Theme): Variants { let order = // Compare by bucket name - aBucket.localeCompare(zBucket) || + (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || // If bucket names are the same, compare by value (direction === 'asc' ? parseInt(aValue) - parseInt(zValue) @@ -489,7 +489,7 @@ export function createVariants(theme: Theme): Variants { // In this scenario, we want to alphabetically sort `calc(100%-1rem)` and // `calc(100%-2rem)` to make it deterministic. if (Number.isNaN(order)) { - return aValue.localeCompare(zValue) + return aValue < zValue ? -1 : 1 } return order diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 42911ca9814f..f16736f668f3 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -233,6 +233,20 @@ test('scale can be a number or percentage', async ({ page }) => { expect(await getPropertyValue('#x', 'scale')).toEqual('1.5') }) +// https://github.com/tailwindlabs/tailwindcss/issues/13185 +test('content-none persists when conditionally styling a pseudo-element', async ({ page }) => { + let { getPropertyValue } = await render( + page, + html`
Hello world
`, + ) + + expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') + + await page.locator('#x').hover() + + expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') +}) + // --- const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8') @@ -241,21 +255,18 @@ async function render(page: Page, content: string) { await page.setContent(content) await page.addStyleTag({ content: optimizeCss( - compile( - css` - @layer theme, base, components, utilities; - @layer theme { - ${defaultTheme} - } - @layer base { - ${preflight} - } - @layer utilities { - @tailwind utilities; - } - `, - scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential), - ), + compile(css` + @layer theme, base, components, utilities; + @layer theme { + ${defaultTheme} + } + @layer base { + ${preflight} + } + @layer utilities { + @tailwind utilities; + } + `).build(scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential)), ), }) diff --git a/packages/tailwindcss/theme.css b/packages/tailwindcss/theme.css index d751f895b4c3..1c05262c712c 100644 --- a/packages/tailwindcss/theme.css +++ b/packages/tailwindcss/theme.css @@ -278,10 +278,8 @@ --blur-3xl: 64px; /* Radii */ - --radius-none: 0px; - --radius-full: 9999px; - --radius-sm: 0.125rem; --radius: 0.25rem; + --radius-sm: 0.125rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem;