From d141afaed8013bf9a02823aa828d49dc5cbded32 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 17 Oct 2024 09:36:03 -0400 Subject: [PATCH 01/22] Add function for depth-first AST traversal --- packages/tailwindcss/src/ast.ts | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index e9d1ce48c9df..cdfdf85255e9 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -128,6 +128,47 @@ export function walk( } } +// This is a depth-first traversal of the AST +export function walkDepth( + ast: AstNode[], + visit: ( + node: AstNode, + utils: { + parent: AstNode | null + path: AstNode[] + context: Record + replaceWith(newNode: AstNode[]): void + }, + ) => void, + parentPath: AstNode[] = [], + context: Record = {}, +) { + for (let i = 0; i < ast.length; i++) { + let node = ast[i] + let path = [...parentPath, node] + let parent = parentPath.at(-1) ?? null + + if (node.kind === 'rule') { + walkDepth(node.nodes, visit, path, context) + } else if (node.kind === 'context') { + walkDepth(node.nodes, visit, parentPath, { ...context, ...node.context }) + continue + } + + visit(node, { + parent, + context, + path, + replaceWith(newNode) { + ast.splice(i, 1, ...newNode) + + // Skip over the newly inserted nodes (being depth-first it doesn't make sense to visit them) + i += newNode.length - 1 + }, + }) + } +} + export function toCss(ast: AstNode[]) { let atRoots: string = '' let seenAtProperties = new Set() From aa5fc874cf0cb156bc3ebfb52e9d9af64ed8e3d5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 14 Oct 2024 13:14:00 -0400 Subject: [PATCH 02/22] Fix intellisense variant selector calculation --- packages/tailwindcss/src/intellisense.test.ts | 33 +++++++++++++-- packages/tailwindcss/src/intellisense.ts | 42 ++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 83785dfb0b1e..ee23be97d830 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -72,14 +72,41 @@ test('getVariants compound', () => { ] expect(list).toEqual([ - ['&:is(:where(.group):hover *)'], - ['&:is(:where(.group\\/sidebar):hover *)'], - ['&:is(:where(.group):is(:where(.group):hover *) *)'], + ['@media (hover: hover) { &:is(:where(.group):hover *) }'], + ['@media (hover: hover) { &:is(:where(.group\\/sidebar):hover *) }'], + ['@media (hover: hover) { &:is(:where(.group):is(:where(.group):hover *) *) }'], [], [], ]) }) +test('variant selectors are in the correct order', async () => { + let input = css` + @variant overactive { + &:hover { + @media (hover: hover) { + &:focus { + &:active { + @slot; + } + } + } + } + } + ` + + let design = await __unstable__loadDesignSystem(input) + let variants = design.getVariants() + let overactive = variants.find((v) => v.name === 'overactive')! + + expect(overactive).toBeTruthy() + expect(overactive.selectors({})).toMatchInlineSnapshot(` + [ + "@media (hover: hover) { &:hover { &:focus { &:active } } }", + ] + `) +}) + test('The variant `has-force` does not crash', () => { let design = loadDesignSystem() let variants = design.getVariants() diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index aaaedd3cffaf..41fddea5814c 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -1,4 +1,4 @@ -import { decl, rule } from './ast' +import { rule, walkDepth } from './ast' import { applyVariant } from './compile' import type { DesignSystem } from './design-system' @@ -69,7 +69,7 @@ export function getVariants(design: DesignSystem) { if (!variant) return [] // Apply the variant to a placeholder rule - let node = rule('.__placeholder__', [decl('color', 'red')]) + let node = rule('.__placeholder__', []) // If the rule produces no nodes it means the variant does not apply if (applyVariant(node, variant, design.variants) === null) { @@ -79,11 +79,41 @@ export function getVariants(design: DesignSystem) { // Now look at the selector(s) inside the rule let selectors: string[] = [] - for (let child of node.nodes) { - if (child.kind === 'rule') { - selectors.push(child.selector) + // Produce v3-style selector strings in the face of nested rules + // This is more visible for things like group-*, not-*, etc… + walkDepth(node.nodes, (node, { path }) => { + if (node.kind !== 'rule') return + if (node.nodes.length > 0) return + + // Sort at-rules before style rules + path.sort((a, b) => { + // This won't actually happen, but it's here to make TypeScript happy + if (a.kind !== 'rule' || b.kind !== 'rule') return 0 + + let aIsAtRule = a.selector.startsWith('@') + let bIsAtRule = b.selector.startsWith('@') + + if (aIsAtRule && !bIsAtRule) return -1 + if (!aIsAtRule && bIsAtRule) return 1 + + return 0 + }) + + // A list of the selectors / at rules encountered to get to this point + let group = path.flatMap((node) => { + if (node.kind !== 'rule') return [] + return node.selector === '&' ? [] : [node.selector] + }) + + // Build a v3-style nested selector + let selector = '' + + for (let i = group.length - 1; i >= 0; i--) { + selector = selector === '' ? group[i] : `${group[i]} { ${selector} }` } - } + + selectors.push(selector) + }) return selectors } From f8143a6a8b64ad65edaab0015db130b020cbb248 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 12:00:48 -0400 Subject: [PATCH 03/22] Add Intellisense API benchmark --- .../tailwindcss/src/intellisense.bench.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/tailwindcss/src/intellisense.bench.ts diff --git a/packages/tailwindcss/src/intellisense.bench.ts b/packages/tailwindcss/src/intellisense.bench.ts new file mode 100644 index 000000000000..c9a53749ab71 --- /dev/null +++ b/packages/tailwindcss/src/intellisense.bench.ts @@ -0,0 +1,56 @@ +import { bench } from 'vitest' +import { buildDesignSystem } from './design-system' +import { Theme } from './theme' + +function loadDesignSystem() { + let theme = new Theme() + theme.add('--spacing-0_5', '0.125rem') + theme.add('--spacing-1', '0.25rem') + theme.add('--spacing-3', '0.75rem') + theme.add('--spacing-4', '1rem') + theme.add('--width-4', '1rem') + theme.add('--colors-red-500', 'red') + theme.add('--colors-blue-500', 'blue') + theme.add('--breakpoint-sm', '640px') + theme.add('--font-size-xs', '0.75rem') + theme.add('--font-size-xs--line-height', '1rem') + theme.add('--perspective-dramatic', '100px') + theme.add('--perspective-normal', '500px') + theme.add('--opacity-background', '0.3') + + return buildDesignSystem(theme) +} + +let design = loadDesignSystem() + +bench('getClassList', () => { + design.getClassList() +}) + +bench('getVariants', () => { + design.getVariants() +}) + +bench('getVariants -> selectors(…)', () => { + let variants = design.getVariants() + let group = variants.find((v) => v.name === 'group')! + + // A selector-based variant + group.selectors({ value: 'hover' }) + + // A selector-based variant with a modifier + group.selectors({ value: 'hover', modifier: 'sidebar' }) + + // A nested, compound, selector-based variant + group.selectors({ value: 'group-hover' }) + + // This variant produced an at rule + group.selectors({ value: 'sm' }) + + // This variant does not exist + group.selectors({ value: 'md' }) +}) + +bench('candidatesToCss', () => { + design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]']) +}) From 12b9fae93b913c17734d08fb20460f50993bad90 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 17 Oct 2024 09:34:19 -0400 Subject: [PATCH 04/22] Use single rule for parallel variants when possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We combine all style rule selectors into one when using the simple variant syntax `@variant (…);` At rules are still split into separate rules. --- packages/tailwindcss/src/index.test.ts | 2 +- packages/tailwindcss/src/index.ts | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 5c1f6286c6a5..753768531355 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2179,7 +2179,7 @@ describe('@variant', () => { expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` "@layer utilities { - .group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) { + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { display: flex; } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 331816d7554b..e02799e21144 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -164,9 +164,30 @@ async function parseCss( let selectors = segment(selector.slice(1, -1), ',') + let atRuleSelectors: string[] = [] + let styleRuleSelectors: string[] = [] + + for (let selector of selectors) { + if (selector.startsWith('@')) { + atRuleSelectors.push(selector) + } else { + styleRuleSelectors.push(selector) + } + } + customVariants.push((designSystem) => { designSystem.variants.static(name, (r) => { - r.nodes = selectors.map((selector) => rule(selector, r.nodes)) + let nodes: AstNode[] = [] + + if (styleRuleSelectors.length > 0) { + nodes.push(rule(styleRuleSelectors.join(', '), r.nodes)) + } + + for (let selector of atRuleSelectors) { + nodes.push(rule(selector, r.nodes)) + } + + r.nodes = nodes }) }) From 5b5bfc911ea48b8e0ce950dce45a61052a26bb6a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 17 Oct 2024 13:12:47 -0400 Subject: [PATCH 05/22] Add path information to `visit` --- packages/tailwindcss/src/ast.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index cdfdf85255e9..2ab5a0046e26 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -87,25 +87,30 @@ export function walk( parent: AstNode | null replaceWith(newNode: AstNode | AstNode[]): void context: Record + path: AstNode[] }, ) => void | WalkAction, - parent: AstNode | null = null, + parentPath: AstNode[] = [], context: Record = {}, ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] + let path = [...parentPath, node] + let parent = parentPath.at(-1) ?? null // We want context nodes to be transparent in walks. This means that // whenever we encounter one, we immediately walk through its children and // furthermore we also don't update the parent. if (node.kind === 'context') { - walk(node.nodes, visit, parent, { ...context, ...node.context }) + walk(node.nodes, visit, parentPath, { ...context, ...node.context }) continue } let status = visit(node, { parent, + context, + path, replaceWith(newNode) { ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode])) // We want to visit the newly replaced node(s), which start at the @@ -113,7 +118,6 @@ export function walk( // will process this position (containing the replaced node) again. i-- }, - context, }) ?? WalkAction.Continue // Stop the walk entirely @@ -123,7 +127,7 @@ export function walk( if (status === WalkAction.Skip) continue if (node.kind === 'rule') { - walk(node.nodes, visit, node, context) + walk(node.nodes, visit, path, context) } } } From bc391bc08db305ba0bc82795b5eac29e353c13b0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 17 Oct 2024 13:12:48 -0400 Subject: [PATCH 06/22] Simplify nesting checks in compound variants --- packages/tailwindcss/src/variants.ts | 104 +++++++++++---------------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 4999439e6a7a..7f7a04e97124 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -212,22 +212,19 @@ export function createVariants(theme: Theme): Variants { let didApply = false - walk([ruleNode], (node) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // Throw out any candidates with variants using nested style rules + for (let parent of path.slice(0, -1)) { + if (parent.kind !== 'rule') continue + if (parent.selector[0] === '@') continue - didApply = false - return WalkAction.Stop - }) - - return didApply ? WalkAction.Skip : WalkAction.Stop + didApply = false + return WalkAction.Stop } // Replace `&` in target variant with `*`, so variants like `&:hover` @@ -240,9 +237,7 @@ export function createVariants(theme: Theme): Variants { // If the node wasn't modified, this variant is not compatible with // `not-*` so discard the candidate. - if (!didApply) { - return null - } + if (!didApply) return null }) variants.compound('group', (ruleNode, variant) => { @@ -250,44 +245,41 @@ export function createVariants(theme: Theme): Variants { // Name the group by appending the modifier to `group` class itself if // present. - let groupSelector = variant.modifier + let variantSelector = variant.modifier ? `:where(.group\\/${variant.modifier.value})` : ':where(.group)' let didApply = false - walk([ruleNode], (node) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // Throw out any candidates with variants using nested style rules + for (let parent of path.slice(0, -1)) { + if (parent.kind !== 'rule') continue + if (parent.selector[0] === '@') continue - didApply = false - return WalkAction.Stop - }) - - return didApply ? WalkAction.Skip : WalkAction.Stop + didApply = false + return WalkAction.Stop } // For most variants we rely entirely on CSS nesting to build-up the final // selector, but there is no way to use CSS nesting to make `&` refer to // just the `.group` class the way we'd need to for these variants, so we // need to replace it in the selector ourselves. - node.selector = node.selector.replaceAll('&', groupSelector) + let selector = node.selector.replaceAll('&', variantSelector) // When the selector is a selector _list_ we need to wrap it in `:is` // to make sure the matching behavior is consistent with the original // variant / selector. - if (segment(node.selector, ',').length > 1) { - node.selector = `:is(${node.selector})` + if (segment(selector, ',').length > 1) { + selector = `:is(${selector})` } - node.selector = `&:is(${node.selector} *)` + node.selector = `&:is(${selector} *)` // Track that the variant was actually applied didApply = true @@ -295,9 +287,7 @@ export function createVariants(theme: Theme): Variants { // If the node wasn't modified, this variant is not compatible with // `group-*` so discard the candidate. - if (!didApply) { - return null - } + if (!didApply) return null }) variants.suggest('group', () => { @@ -311,44 +301,41 @@ export function createVariants(theme: Theme): Variants { // Name the peer by appending the modifier to `peer` class itself if // present. - let peerSelector = variant.modifier + let variantSelector = variant.modifier ? `:where(.peer\\/${variant.modifier.value})` : ':where(.peer)' let didApply = false - walk([ruleNode], (node) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // Throw out any candidates with variants using nested style rules + for (let parent of path.slice(0, -1)) { + if (parent.kind !== 'rule') continue + if (parent.selector[0] === '@') continue - didApply = false - return WalkAction.Stop - }) - - return didApply ? WalkAction.Skip : WalkAction.Stop + didApply = false + return WalkAction.Stop } // For most variants we rely entirely on CSS nesting to build-up the final // selector, but there is no way to use CSS nesting to make `&` refer to // just the `.group` class the way we'd need to for these variants, so we // need to replace it in the selector ourselves. - node.selector = node.selector.replaceAll('&', peerSelector) + let selector = node.selector.replaceAll('&', variantSelector) // When the selector is a selector _list_ we need to wrap it in `:is` // to make sure the matching behavior is consistent with the original // variant / selector. - if (segment(node.selector, ',').length > 1) { - node.selector = `:is(${node.selector})` + if (segment(selector, ',').length > 1) { + selector = `:is(${selector})` } - node.selector = `&:is(${node.selector} ~ *)` + node.selector = `&:is(${selector} ~ *)` // Track that the variant was actually applied didApply = true @@ -356,9 +343,7 @@ export function createVariants(theme: Theme): Variants { // If the node wasn't modified, this variant is not compatible with // `peer-*` so discard the candidate. - if (!didApply) { - return null - } + if (!didApply) return null }) variants.suggest('peer', () => { @@ -463,22 +448,19 @@ export function createVariants(theme: Theme): Variants { let didApply = false - walk([ruleNode], (node) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // Throw out any candidates with variants using nested style rules + for (let parent of path.slice(0, -1)) { + if (parent.kind !== 'rule') continue + if (parent.selector[0] === '@') continue - didApply = false - return WalkAction.Stop - }) - - return didApply ? WalkAction.Skip : WalkAction.Stop + didApply = false + return WalkAction.Stop } // Replace `&` in target variant with `*`, so variants like `&:hover` @@ -491,9 +473,7 @@ export function createVariants(theme: Theme): Variants { // If the node wasn't modified, this variant is not compatible with // `has-*` so discard the candidate. - if (!didApply) { - return null - } + if (!didApply) return null }) variants.suggest('has', () => { From 22673e552d55f324dc63c91ddaa8d99c5ad6153a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 14:01:31 -0400 Subject: [PATCH 07/22] Filter the kinds of rules compound variants support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right now there are only two possibilities: - `false` — this means a variant never compounds - `selector` — this means a variant only compounds if it does not produce at-rules --- packages/tailwindcss/src/candidate.test.ts | 26 ------ packages/tailwindcss/src/candidate.ts | 22 +---- packages/tailwindcss/src/variants.ts | 99 +++++++++++++++++++--- 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index a0a84ce5c647..b8c11cbee96e 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -109,7 +109,6 @@ it('should parse a simple utility with a variant', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, @@ -137,12 +136,10 @@ it('should parse a simple utility with stacked variants', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, { - "compounds": true, "kind": "static", "root": "focus", }, @@ -166,7 +163,6 @@ it('should parse a simple utility with an arbitrary variant', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", @@ -194,7 +190,6 @@ it('should parse a simple utility with a parameterized variant', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "functional", "modifier": null, "root": "data", @@ -226,7 +221,6 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia "root": "flex", "variants": [ { - "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -234,7 +228,6 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia }, "root": "group", "variant": { - "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", @@ -265,7 +258,6 @@ it('should parse a simple utility with a parameterized variant and a modifier', "root": "flex", "variants": [ { - "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -273,7 +265,6 @@ it('should parse a simple utility with a parameterized variant and a modifier', }, "root": "group", "variant": { - "compounds": true, "kind": "functional", "modifier": null, "root": "aria", @@ -308,7 +299,6 @@ it('should parse compound group with itself group-group-*', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "compound", "modifier": { "kind": "named", @@ -316,17 +306,14 @@ it('should parse compound group with itself group-group-*', () => { }, "root": "group", "variant": { - "compounds": true, "kind": "compound", "modifier": null, "root": "group", "variant": { - "compounds": true, "kind": "compound", "modifier": null, "root": "group", "variant": { - "compounds": true, "kind": "static", "root": "hover", }, @@ -353,7 +340,6 @@ it('should parse a simple utility with an arbitrary media variant', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "arbitrary", "relative": false, "selector": "@media(width>=123px)", @@ -478,7 +464,6 @@ it('should parse a utility with a modifier and a variant', () => { }, "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, @@ -895,7 +880,6 @@ it('should parse a static variant starting with @', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "static", "root": "@lg", }, @@ -922,7 +906,6 @@ it('should parse a functional variant with a modifier', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "functional", "modifier": { "kind": "named", @@ -957,7 +940,6 @@ it('should parse a functional variant starting with @', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "functional", "modifier": null, "root": "@", @@ -989,7 +971,6 @@ it('should parse a functional variant starting with @ and a modifier', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "functional", "modifier": { "kind": "named", @@ -1153,7 +1134,6 @@ it('should parse arbitrary properties with a variant', () => { "value": "red", "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, @@ -1179,12 +1159,10 @@ it('should parse arbitrary properties with stacked variants', () => { "value": "red", "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, { - "compounds": true, "kind": "static", "root": "focus", }, @@ -1206,13 +1184,11 @@ it('should parse arbitrary properties that are important and using stacked arbit "value": "red", "variants": [ { - "compounds": true, "kind": "arbitrary", "relative": false, "selector": "& p", }, { - "compounds": true, "kind": "arbitrary", "relative": false, "selector": "@media(width>=123px)", @@ -1250,7 +1226,6 @@ it('should parse a variant containing an arbitrary string with unbalanced parens "root": "flex", "variants": [ { - "compounds": true, "kind": "functional", "modifier": null, "root": "string", @@ -1298,7 +1273,6 @@ it('should parse candidates with a prefix', () => { "root": "flex", "variants": [ { - "compounds": true, "kind": "static", "root": "hover", }, diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 9738c9b0f8c8..91d6e1bb60bf 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -100,9 +100,6 @@ export type Variant = kind: 'arbitrary' selector: string - // If true, it can be applied as a child of a compound variant - compounds: boolean - // Whether or not the selector is a relative selector // @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector relative: boolean @@ -116,9 +113,6 @@ export type Variant = | { kind: 'static' root: string - - // If true, it can be applied as a child of a compound variant - compounds: boolean } /** @@ -138,9 +132,6 @@ export type Variant = root: string value: ArbitraryVariantValue | NamedVariantValue | null modifier: ArbitraryModifier | NamedModifier | null - - // If true, it can be applied as a child of a compound variant - compounds: boolean } /** @@ -157,9 +148,6 @@ export type Variant = root: string modifier: ArbitraryModifier | NamedModifier | null variant: Variant - - // If true, it can be applied as a child of a compound variant - compounds: boolean } export type Candidate = @@ -511,7 +499,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia return { kind: 'arbitrary', selector, - compounds: true, relative, } } @@ -546,7 +533,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia return { kind: 'static', root, - compounds: designSystem.variants.compounds(root), } } @@ -557,7 +543,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia root, modifier: modifier === null ? null : parseModifier(modifier), value: null, - compounds: designSystem.variants.compounds(root), } } @@ -570,7 +555,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia kind: 'arbitrary', value: decodeArbitraryValue(value.slice(1, -1)), }, - compounds: designSystem.variants.compounds(root), } } @@ -579,7 +563,6 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia root, modifier: modifier === null ? null : parseModifier(modifier), value: { kind: 'named', value }, - compounds: designSystem.variants.compounds(root), } } @@ -588,14 +571,15 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia let subVariant = designSystem.parseVariant(value) if (subVariant === null) return null - if (subVariant.compounds === false) return null + + // These two variants must be compatible when compounded + if (!designSystem.variants.compoundsWith(root, subVariant)) return null return { kind: 'compound', root, modifier: modifier === null ? null : { kind: 'named', value: modifier }, variant: subVariant, - compounds: designSystem.variants.compounds(root), } } } diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 7f7a04e97124..27e13ae7e23f 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -12,6 +12,8 @@ type VariantFn = ( type CompareFn = (a: Variant, z: Variant) => number +type CompoundKind = false | Array<'selector'> + export class Variants { public compareFns = new Map() public variants = new Map< @@ -20,7 +22,13 @@ export class Variants { kind: Variant['kind'] order: number applyFn: VariantFn - compounds: boolean + + // The kind of rules that are allowed in this compound variant + compoundsWith: CompoundKind + + // The kind of rules that are generated by this variant + // Determines whether or not a compound variant can use this variant + compounds: CompoundKind } >() @@ -42,9 +50,15 @@ export class Variants { static( name: string, applyFn: VariantFn<'static'>, - { compounds, order }: { compounds?: boolean; order?: number } = {}, + { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, ) { - this.set(name, { kind: 'static', applyFn, compounds: compounds ?? true, order }) + this.set(name, { + kind: 'static', + applyFn, + compoundsWith: false, + compounds: compounds ?? ['selector'], + order, + }) } fromAst(name: string, ast: AstNode[]) { @@ -58,17 +72,29 @@ export class Variants { functional( name: string, applyFn: VariantFn<'functional'>, - { compounds, order }: { compounds?: boolean; order?: number } = {}, + { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, ) { - this.set(name, { kind: 'functional', applyFn, compounds: compounds ?? true, order }) + this.set(name, { + kind: 'functional', + applyFn, + compoundsWith: false, + compounds: compounds ?? ['selector'], + order, + }) } compound( name: string, applyFn: VariantFn<'compound'>, - { compounds, order }: { compounds?: boolean; order?: number } = {}, + { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, ) { - this.set(name, { kind: 'compound', applyFn, compounds: compounds ?? true, order }) + this.set(name, { + kind: 'compound', + applyFn, + compoundsWith: ['selector'], + compounds: compounds ?? ['selector'], + order, + }) } group(fn: () => void, compareFn?: CompareFn) { @@ -90,8 +116,39 @@ export class Variants { return this.variants.get(name)?.kind! } - compounds(name: string) { - return this.variants.get(name)?.compounds! + compoundsWith(parent: string, child: string | Variant) { + let parentInfo = this.variants.get(parent) + let childInfo = + typeof child === 'string' + ? this.variants.get(child) + : child.kind === 'arbitrary' + ? { compounds: ['selector'] as CompoundKind } + : this.variants.get(child.root) + + // One of the variants don't exist + if (!parentInfo || !childInfo) return false + + // The parent variant is not a compound variant + if (parentInfo.kind !== 'compound') return false + + // The variant `parent` may _compound with_ `child` if `parent` supports the + // rules that `child` generates. We instead use static registration metadata + // about what `parent` and `child` support instead of trying to apply the + // variant at runtime to see if the rules are compatible. + + // The `child` variant cannot compound *ever* + if (childInfo.compounds === false) return false + + // The `parent` variant cannot compound *ever* + // This shouldn't ever happen because `kind` is `compound` + if (parentInfo.compoundsWith === false) return false + + // Any rule that `child` may generate must be supported by `parent` + for (let compound of childInfo.compounds) { + if (!parentInfo.compoundsWith.includes(compound)) return false + } + + return true } suggest(name: string, suggestions: () => string[]) { @@ -154,8 +211,15 @@ export class Variants { kind, applyFn, compounds, + compoundsWith, order, - }: { kind: T; applyFn: VariantFn; compounds: boolean; order?: number }, + }: { + kind: T + applyFn: VariantFn + compoundsWith: CompoundKind + compounds: CompoundKind + order?: number + }, ) { let existing = this.variants.get(name) if (existing) { @@ -169,6 +233,7 @@ export class Variants { kind, applyFn, order, + compoundsWith, compounds, }) } @@ -191,7 +256,7 @@ export function createVariants(theme: Theme): Variants { function staticVariant( name: string, selectors: string[], - { compounds }: { compounds?: boolean } = {}, + { compounds }: { compounds?: CompoundKind } = {}, ) { variants.static( name, @@ -240,6 +305,12 @@ export function createVariants(theme: Theme): Variants { if (!didApply) return null }) + variants.suggest('not', () => { + return Array.from(variants.keys()).filter((name) => { + return variants.compoundsWith('not', name) + }) + }) + variants.compound('group', (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null @@ -292,7 +363,7 @@ export function createVariants(theme: Theme): Variants { variants.suggest('group', () => { return Array.from(variants.keys()).filter((name) => { - return variants.get(name)?.compounds ?? false + return variants.compoundsWith('group', name) }) }) @@ -348,7 +419,7 @@ export function createVariants(theme: Theme): Variants { variants.suggest('peer', () => { return Array.from(variants.keys()).filter((name) => { - return variants.get(name)?.compounds ?? false + return variants.compoundsWith('peer', name) }) }) @@ -478,7 +549,7 @@ export function createVariants(theme: Theme): Variants { variants.suggest('has', () => { return Array.from(variants.keys()).filter((name) => { - return variants.get(name)?.compounds ?? false + return variants.compoundsWith('has', name) }) }) From 6a03a06ad4949dc186001cc64ce1255d48b7c1a8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 14:01:32 -0400 Subject: [PATCH 08/22] Register compound variants with the kinds of rules they support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t change what any of the rules support but makes it explicit --- packages/tailwindcss/src/candidate.test.ts | 8 ++++---- packages/tailwindcss/src/variants.ts | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index b8c11cbee96e..6db751060e21 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -209,7 +209,7 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', () => {}) + variants.compoundWith('group', ['selector'], () => {}) expect(run('group-[&_p]/parent-name:flex', { utilities, variants })).toMatchInlineSnapshot(` [ @@ -244,7 +244,7 @@ it('should parse a simple utility with a parameterized variant and a modifier', utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', () => {}) + variants.compoundWith('group', ['selector'], () => {}) variants.functional('aria', () => {}) expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants })) @@ -286,7 +286,7 @@ it('should parse compound group with itself group-group-*', () => { let variants = new Variants() variants.static('hover', () => {}) - variants.compound('group', () => {}) + variants.compoundWith('group', ['selector'], () => {}) expect(run('group-group-group-hover/parent-name:flex', { utilities, variants })) .toMatchInlineSnapshot(` @@ -1204,7 +1204,7 @@ it('should not parse compound group with a non-compoundable variant', () => { utilities.static('flex', () => []) let variants = new Variants() - variants.compound('group', () => {}) + variants.compoundWith('group', ['selector'], () => {}) expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 27e13ae7e23f..f25484dbff51 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -83,15 +83,16 @@ export class Variants { }) } - compound( + compoundWith( name: string, + compoundsWith: CompoundKind, applyFn: VariantFn<'compound'>, { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, ) { this.set(name, { kind: 'compound', applyFn, - compoundsWith: ['selector'], + compoundsWith, compounds: compounds ?? ['selector'], order, }) @@ -270,7 +271,7 @@ export function createVariants(theme: Theme): Variants { variants.static('force', () => {}, { compounds: false }) staticVariant('*', [':where(& > *)'], { compounds: false }) - variants.compound('not', (ruleNode, variant) => { + variants.compoundWith('not', ['selector'], (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null if (variant.modifier) return null @@ -311,7 +312,7 @@ export function createVariants(theme: Theme): Variants { }) }) - variants.compound('group', (ruleNode, variant) => { + variants.compoundWith('group', ['selector'], (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null // Name the group by appending the modifier to `group` class itself if @@ -367,7 +368,7 @@ export function createVariants(theme: Theme): Variants { }) }) - variants.compound('peer', (ruleNode, variant) => { + variants.compoundWith('peer', ['selector'], (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null // Name the peer by appending the modifier to `peer` class itself if @@ -514,7 +515,7 @@ export function createVariants(theme: Theme): Variants { staticVariant('inert', ['&:is([inert], [inert] *)']) - variants.compound('has', (ruleNode, variant) => { + variants.compoundWith('has', ['selector'], (ruleNode, variant) => { if (variant.modifier) return null let didApply = false From 8dc719c53c93b4dabcc37fd1f6af7ea084919d11 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 14:04:20 -0400 Subject: [PATCH 09/22] Let `not-*` variant handle simple conditional at rules --- packages/tailwindcss/src/variants.ts | 174 ++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 28 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index f25484dbff51..46070530ad65 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -12,7 +12,7 @@ type VariantFn = ( type CompareFn = (a: Variant, z: Variant) => number -type CompoundKind = false | Array<'selector'> +type CompoundKind = false | Array<'at-rule' | 'selector'> export class Variants { public compareFns = new Map() @@ -271,7 +271,79 @@ export function createVariants(theme: Theme): Variants { variants.static('force', () => {}, { compounds: false }) staticVariant('*', [':where(& > *)'], { compounds: false }) - variants.compoundWith('not', ['selector'], (ruleNode, variant) => { + function negateConditions(ruleName: string, conditions: string[]) { + return conditions.map((condition) => { + condition = condition.trim() + + let parts = segment(condition, ' ') + + // @media not {query} + // @supports not {query} + // @container not {query} + if (parts[0] === 'not') { + return parts.slice(1).join(' ') + } + + if (ruleName === 'container') { + // @container {query} + if (parts[0].startsWith('(')) { + return `not ${condition}` + } + + // @container {name} not {query} + else if (parts[1] === 'not') { + return `${parts[0]} ${parts.slice(2).join(' ')}` + } + + // @container {name} {query} + else { + return `${parts[0]} not ${parts.slice(1).join(' ')}` + } + } + + return `not ${condition}` + }) + } + + function negateSelector(selector: string) { + if (selector[0] === '@') { + let name = selector.slice(1, selector.indexOf(' ')) + let params = selector.slice(selector.indexOf(' ') + 1) + + if (name === 'media' || name === 'supports' || name === 'container') { + let conditions = segment(params, ',') + + // We don't support things like `@media screen, print` because + // the negation would be `@media not screen and print` and we don't + // want to deal with that complexity. + if (conditions.length > 1) return null + + conditions = negateConditions(name, conditions) + return `@${name} ${conditions.join(', ')}` + } + + return null + } + + if (selector.includes('::')) return null + + let selectors = segment(selector, ',').map((sel) => { + // Remove unncessary wrapping &:is(…) to reduce the selector size + if (sel.startsWith('&:is(') && sel.endsWith(')')) { + sel = sel.slice(5, -1) + } + + // Replace `&` in target variant with `*`, so variants like `&:hover` + // become `&:not(*:hover)`. The `*` will often be optimized away. + sel = sel.replaceAll('&', '*') + + return sel + }) + + return `&:not(${selectors.join(', ')})` + } + + variants.compoundWith('not', ['selector', 'at-rule'], (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null if (variant.modifier) return null @@ -280,27 +352,61 @@ export function createVariants(theme: Theme): Variants { walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue - - // Skip past at-rules, and continue traversing the children of the at-rule - if (node.selector[0] === '@') return WalkAction.Continue + if (node.nodes.length > 0) return WalkAction.Continue // Throw out any candidates with variants using nested style rules - for (let parent of path.slice(0, -1)) { + let atRules: Rule[] = [] + let styleRules: Rule[] = [] + + for (let parent of path) { if (parent.kind !== 'rule') continue - if (parent.selector[0] === '@') continue + if (parent.selector[0] === '@') { + atRules.push(parent) + } else { + styleRules.push(parent) + } + } - didApply = false - return WalkAction.Stop + if (atRules.length > 1) return WalkAction.Stop + if (styleRules.length > 1) return WalkAction.Stop + + let rules: Rule[] = [] + + for (let styleRule of styleRules) { + let selector = negateSelector(styleRule.selector) + if (!selector) { + didApply = false + return WalkAction.Stop + } + + rules.push(rule(selector, [])) } - // Replace `&` in target variant with `*`, so variants like `&:hover` - // become `&:not(*:hover)`. The `*` will often be optimized away. - node.selector = `&:not(${node.selector.replaceAll('&', '*')})` + for (let atRule of atRules) { + let selector = negateSelector(atRule.selector) + if (!selector) { + didApply = false + return WalkAction.Stop + } + + rules.push(rule(selector, [])) + } + + ruleNode.selector = '&' + ruleNode.nodes = rules // Track that the variant was actually applied didApply = true + + return WalkAction.Skip }) + // TODO: Tweak group, peer, has to ignore intermediate `&` selectors (maybe?) + if (ruleNode.selector === '&' && ruleNode.nodes.length === 1) { + ruleNode.selector = (ruleNode.nodes[0] as Rule).selector + ruleNode.nodes = (ruleNode.nodes[0] as Rule).nodes + } + // If the node wasn't modified, this variant is not compatible with // `not-*` so discard the candidate. if (!didApply) return null @@ -661,16 +767,22 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@supports ${value}`, ruleNode.nodes)] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)'], { - compounds: false, + compounds: ['at-rule'], + }) + staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)'], { + compounds: ['at-rule'], }) - staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)'], { compounds: false }) - staticVariant('contrast-more', ['@media (prefers-contrast: more)'], { compounds: false }) - staticVariant('contrast-less', ['@media (prefers-contrast: less)'], { compounds: false }) + staticVariant('contrast-more', ['@media (prefers-contrast: more)'], { + compounds: ['at-rule'], + }) + staticVariant('contrast-less', ['@media (prefers-contrast: less)'], { + compounds: ['at-rule'], + }) { // Helper to compare variants by their resolved values, this is used by the @@ -782,7 +894,7 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@media (width < ${value})`, ruleNode.nodes)] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) }, (a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints), @@ -804,7 +916,7 @@ export function createVariants(theme: Theme): Variants { (ruleNode) => { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) } @@ -817,7 +929,7 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) }, (a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints), @@ -875,7 +987,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) }, (a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths), @@ -903,7 +1015,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) variants.functional( '@min', @@ -920,7 +1032,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: false }, + { compounds: ['at-rule'] }, ) }, (a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths), @@ -933,19 +1045,25 @@ export function createVariants(theme: Theme): Variants { } } - staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: false }) - staticVariant('landscape', ['@media (orientation: landscape)'], { compounds: false }) + staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: ['at-rule'] }) + staticVariant('landscape', ['@media (orientation: landscape)'], { + compounds: ['at-rule'], + }) staticVariant('ltr', ['&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)']) staticVariant('rtl', ['&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)']) - staticVariant('dark', ['@media (prefers-color-scheme: dark)'], { compounds: false }) + staticVariant('dark', ['@media (prefers-color-scheme: dark)'], { + compounds: ['at-rule'], + }) staticVariant('starting', ['@starting-style'], { compounds: false }) - staticVariant('print', ['@media print'], { compounds: false }) + staticVariant('print', ['@media print'], { compounds: ['at-rule'] }) - staticVariant('forced-colors', ['@media (forced-colors: active)'], { compounds: false }) + staticVariant('forced-colors', ['@media (forced-colors: active)'], { + compounds: ['at-rule'], + }) return variants } From cc3e77448f0d248079b00cd46e4e99ae433ff006 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 13:46:26 -0400 Subject: [PATCH 10/22] Update tests --- .../__snapshots__/intellisense.test.ts.snap | 63 ++- packages/tailwindcss/src/index.test.ts | 6 + packages/tailwindcss/src/variants.test.ts | 389 +++++++++++++++++- 3 files changed, 451 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 75232242cd0d..a0727fe8356f 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -3859,7 +3859,68 @@ exports[`getVariants 1`] = ` "isArbitrary": true, "name": "not", "selectors": [Function], - "values": [], + "values": [ + "not", + "group", + "peer", + "first", + "last", + "only", + "odd", + "even", + "first-of-type", + "last-of-type", + "only-of-type", + "visited", + "target", + "open", + "default", + "checked", + "indeterminate", + "placeholder-shown", + "autofill", + "optional", + "required", + "valid", + "invalid", + "in-range", + "out-of-range", + "read-only", + "empty", + "focus-within", + "hover", + "focus", + "focus-visible", + "active", + "enabled", + "disabled", + "inert", + "has", + "aria", + "data", + "nth", + "nth-last", + "nth-of-type", + "nth-last-of-type", + "supports", + "motion-safe", + "motion-reduce", + "contrast-more", + "contrast-less", + "max", + "sm", + "min", + "@max", + "@", + "@min", + "portrait", + "landscape", + "ltr", + "rtl", + "dark", + "print", + "forced-colors", + ], }, { "hasDash": true, diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 753768531355..34419088687b 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2622,6 +2622,12 @@ describe('@variant', () => { ), ).toMatchInlineSnapshot(` "@layer utilities { + @media not foo { + .not-foo\\:flex { + display: flex; + } + } + @media foo { .foo\\:flex { display: flex; diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 97e358ef0117..353eb0939f45 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1687,16 +1687,33 @@ test('not', async () => { @slot; } } + + @variant device-hocus { + @media (hover: hover) { + &:hover, + &:focus { + @slot; + } + } + } + + @theme { + --breakpoint-sm: 640px; + } + @tailwind utilities; `, [ 'not-[:checked]:flex', 'not-hocus:flex', + 'not-device-hocus:flex', 'group-not-[:checked]:flex', 'group-not-[:checked]/parent-name:flex', 'group-not-checked:flex', 'group-not-hocus:flex', + // 'group-not-hover:flex', + // 'group-not-device-hocus:flex', 'group-not-hocus/parent-name:flex', 'peer-not-[:checked]:flex', @@ -1704,13 +1721,332 @@ test('not', async () => { 'peer-not-checked:flex', 'peer-not-hocus:flex', 'peer-not-hocus/sibling-name:flex', + + // Not versions of built-in variants + 'not-first:flex', + 'not-last:flex', + 'not-only:flex', + 'not-odd:flex', + 'not-even:flex', + 'not-first-of-type:flex', + 'not-last-of-type:flex', + 'not-only-of-type:flex', + 'not-visited:flex', + 'not-target:flex', + 'not-open:flex', + 'not-default:flex', + 'not-checked:flex', + 'not-indeterminate:flex', + 'not-placeholder-shown:flex', + 'not-autofill:flex', + 'not-optional:flex', + 'not-required:flex', + 'not-valid:flex', + 'not-invalid:flex', + 'not-in-range:flex', + 'not-out-of-range:flex', + 'not-read-only:flex', + 'not-empty:flex', + 'not-focus-within:flex', + 'not-hover:flex', + 'not-focus:flex', + 'not-focus-visible:flex', + 'not-active:flex', + 'not-enabled:flex', + 'not-disabled:flex', + 'not-inert:flex', + + 'not-ltr:flex', + 'not-rtl:flex', + 'not-motion-safe:flex', + 'not-motion-reduce:flex', + 'not-dark:flex', + 'not-print:flex', + 'not-supports-grid:flex', + 'not-has-checked:flex', + 'not-aria-selected:flex', + 'not-data-foo:flex', + 'not-portrait:flex', + 'not-landscape:flex', + 'not-contrast-more:flex', + 'not-contrast-less:flex', + 'not-forced-colors:flex', + 'not-nth-2:flex', + + 'not-sm:flex', + 'not-min-sm:flex', + 'not-min-[130px]:flex', + 'not-max-sm:flex', + 'not-max-[130px]:flex', ], ), ).toMatchInlineSnapshot(` - ".not-hocus\\:flex:not(:hover, :focus) { + ":root { + --breakpoint-sm: 640px; + } + + .not-first\\:flex:not(:first-child) { + display: flex; + } + + .not-last\\:flex:not(:last-child) { + display: flex; + } + + .not-only\\:flex:not(:only-child) { + display: flex; + } + + .not-odd\\:flex:not(:nth-child(odd)) { + display: flex; + } + + .not-even\\:flex:not(:nth-child(2n)) { + display: flex; + } + + .not-first-of-type\\:flex:not(:first-of-type) { + display: flex; + } + + .not-last-of-type\\:flex:not(:last-of-type) { + display: flex; + } + + .not-only-of-type\\:flex:not(:only-of-type) { + display: flex; + } + + .not-visited\\:flex:not(:visited) { + display: flex; + } + + .not-target\\:flex:not(:target) { + display: flex; + } + + .not-open\\:flex:not([open], :popover-open) { + display: flex; + } + + .not-default\\:flex:not(:default) { + display: flex; + } + + .not-checked\\:flex:not(:checked) { + display: flex; + } + + .not-indeterminate\\:flex:not(:indeterminate) { + display: flex; + } + + .not-placeholder-shown\\:flex:not(:placeholder-shown) { + display: flex; + } + + .not-autofill\\:flex:not(:autofill) { + display: flex; + } + + .not-optional\\:flex:not(:optional) { + display: flex; + } + + .not-required\\:flex:not(:required) { + display: flex; + } + + .not-valid\\:flex:not(:valid) { + display: flex; + } + + .not-invalid\\:flex:not(:invalid) { + display: flex; + } + + .not-in-range\\:flex:not(:in-range) { + display: flex; + } + + .not-out-of-range\\:flex:not(:out-of-range) { + display: flex; + } + + .not-read-only\\:flex:not(:read-only) { + display: flex; + } + + .not-empty\\:flex:not(:empty) { + display: flex; + } + + .not-focus-within\\:flex:not(:focus-within) { + display: flex; + } + + .not-hover\\:flex:not(:hover) { + display: flex; + } + + @media not (hover: hover) { + .not-hover\\:flex { + display: flex; + } + } + + .not-focus\\:flex:not(:focus) { + display: flex; + } + + .not-focus-visible\\:flex:not(:focus-visible) { + display: flex; + } + + .not-active\\:flex:not(:active) { + display: flex; + } + + .not-enabled\\:flex:not(:enabled) { + display: flex; + } + + .not-disabled\\:flex:not(:disabled) { + display: flex; + } + + .not-inert\\:flex:not([inert], [inert] *) { + display: flex; + } + + .not-has-checked\\:flex:not(:has(:checked)) { + display: flex; + } + + .not-aria-selected\\:flex:not([aria-selected="true"]) { display: flex; } + .not-data-foo\\:flex:not([data-foo]) { + display: flex; + } + + .not-nth-2\\:flex:not(:nth-child(2)) { + display: flex; + } + + @supports not (grid: var(--tw)) { + .not-supports-grid\\:flex { + display: flex; + } + } + + @media not (prefers-reduced-motion: no-preference) { + .not-motion-safe\\:flex { + display: flex; + } + } + + @media not (prefers-reduced-motion: reduce) { + .not-motion-reduce\\:flex { + display: flex; + } + } + + @media not (prefers-contrast: more) { + .not-contrast-more\\:flex { + display: flex; + } + } + + @media not (prefers-contrast: less) { + .not-contrast-less\\:flex { + display: flex; + } + } + + @media not (width < 640px) { + .not-max-sm\\:flex { + display: flex; + } + } + + @media not (width < 130px) { + .not-max-\\[130px\\]\\:flex { + display: flex; + } + } + + @media not (width >= 130px) { + .not-min-\\[130px\\]\\:flex { + display: flex; + } + } + + @media not (width >= 640px) { + .not-min-sm\\:flex { + display: flex; + } + } + + @media not (width >= 640px) { + .not-sm\\:flex { + display: flex; + } + } + + @media not (orientation: portrait) { + .not-portrait\\:flex { + display: flex; + } + } + + @media not (orientation: landscape) { + .not-landscape\\:flex { + display: flex; + } + } + + .not-ltr\\:flex:not(:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)) { + display: flex; + } + + .not-rtl\\:flex:not(:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)) { + display: flex; + } + + @media not (prefers-color-scheme: dark) { + .not-dark\\:flex { + display: flex; + } + } + + @media not print { + .not-print\\:flex { + display: flex; + } + } + + @media not (forced-colors: active) { + .not-forced-colors\\:flex { + display: flex; + } + } + + .not-hocus\\:flex:not(:hover, :focus) { + display: flex; + } + + .not-device-hocus\\:flex:not(:hover, :focus) { + display: flex; + } + + @media not (hover: hover) { + .not-device-hocus\\:flex { + display: flex; + } + } + .not-\\[\\:checked\\]\\:flex:not(:checked) { display: flex; } @@ -1759,8 +2095,19 @@ test('not', async () => { expect( await compileCss( css` - @variant custom-at-rule (@media foo); - @variant nested-selectors { + @variant nested-at-rules { + @media foo { + @media bar { + @slot; + } + } + } + @variant multiple-media-conditions { + @media foo, bar { + @slot; + } + } + @variant nested-style-rules { &:hover { &:focus { @slot; @@ -1774,9 +2121,39 @@ test('not', async () => { 'not-[+img]:flex', 'not-[~img]:flex', 'not-[:checked]/foo:flex', - 'not-[@media_print]:flex', - 'not-custom-at-rule:flex', - 'not-nested-selectors:flex', + 'not-nested-at-rules:flex', + 'not-nested-style-rules:flex', + 'not-multiple-media-conditions:flex', + 'not-starting:flex', + + // The following built-in variants don't have not-* versions because + // there is no sensible negative version of them. + + // These just don't make sense as not-* + 'not-force', + 'not-*', + + // These contain pseudo-elements + 'not-first-letter', + 'not-first-line', + 'not-marker', + 'not-selection', + 'not-file', + 'not-placeholder', + 'not-backdrop', + 'not-before', + 'not-after', + + // This is not a conditional at rule + 'not-starting:flex', + + // TODO: + // 'not-group-[...]:flex', + // 'not-group-*:flex', + // 'not-peer-[...]:flex', + // 'not-peer-*:flex', + // 'not-max-*:flex', + // 'not-min-*:flex', ], ), ).toEqual('') From 414bc16f289d73dfb5378e1cad7385a6233a12be Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 21 Oct 2024 17:36:19 -0400 Subject: [PATCH 11/22] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 872da98b6c43..5a586a761360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Aded `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) +- Improved support for custom variants with `group-*`, `peer-*`, `has-*`, and `not-*` ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) - _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742)) - _Upgrade (experimental)_: Migrate `@import` statements for relative CSS files to use relative path syntax (e.g. `./file.css`) ([#14755](https://github.com/tailwindlabs/tailwindcss/pull/14755)) @@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure JS theme keys containing special characters correctly produce utility classes (e.g. `'1/2': 50%` to `w-1/2`) ([#14739](https://github.com/tailwindlabs/tailwindcss/pull/14739)) - Always emit keyframes registered in `addUtilities` ([#14747](https://github.com/tailwindlabs/tailwindcss/pull/14747)) - Ensure loading stylesheets via the `?raw` and `?url` static asset query works when using the Vite plugin ([#14716](https://github.com/tailwindlabs/tailwindcss/pull/14716)) +- Fixed display of complex variants in Intellisense ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721)) - _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720)) - _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724)) From 22e561443000ff46e10c6b6a827f67c0d04f6c0d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 22 Oct 2024 11:12:36 -0400 Subject: [PATCH 12/22] Use bitfield enum --- packages/tailwindcss/src/candidate.test.ts | 8 +- packages/tailwindcss/src/variants.ts | 108 +++++++++++---------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 6db751060e21..baf0c500680e 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest' import { buildDesignSystem } from './design-system' import { Theme } from './theme' import { Utilities } from './utilities' -import { Variants } from './variants' +import { Compounds, Variants } from './variants' function run( candidate: string, @@ -209,7 +209,7 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia utilities.static('flex', () => []) let variants = new Variants() - variants.compoundWith('group', ['selector'], () => {}) + variants.compoundWith('group', Compounds.StyleRules, () => {}) expect(run('group-[&_p]/parent-name:flex', { utilities, variants })).toMatchInlineSnapshot(` [ @@ -244,7 +244,7 @@ it('should parse a simple utility with a parameterized variant and a modifier', utilities.static('flex', () => []) let variants = new Variants() - variants.compoundWith('group', ['selector'], () => {}) + variants.compoundWith('group', Compounds.StyleRules, () => {}) variants.functional('aria', () => {}) expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants })) @@ -286,7 +286,7 @@ it('should parse compound group with itself group-group-*', () => { let variants = new Variants() variants.static('hover', () => {}) - variants.compoundWith('group', ['selector'], () => {}) + variants.compoundWith('group', Compounds.StyleRules, () => {}) expect(run('group-group-group-hover/parent-name:flex', { utilities, variants })) .toMatchInlineSnapshot(` diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 46070530ad65..75a99857e471 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -12,7 +12,11 @@ type VariantFn = ( type CompareFn = (a: Variant, z: Variant) => number -type CompoundKind = false | Array<'at-rule' | 'selector'> +export const enum Compounds { + Never = 0, + AtRules = 1 << 0, + StyleRules = 1 << 1, +} export class Variants { public compareFns = new Map() @@ -24,11 +28,11 @@ export class Variants { applyFn: VariantFn // The kind of rules that are allowed in this compound variant - compoundsWith: CompoundKind + compoundsWith: Compounds // The kind of rules that are generated by this variant // Determines whether or not a compound variant can use this variant - compounds: CompoundKind + compounds: Compounds } >() @@ -50,13 +54,13 @@ export class Variants { static( name: string, applyFn: VariantFn<'static'>, - { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, + { compounds, order }: { compounds?: Compounds; order?: number } = {}, ) { this.set(name, { kind: 'static', applyFn, - compoundsWith: false, - compounds: compounds ?? ['selector'], + compoundsWith: Compounds.Never, + compounds: compounds ?? Compounds.StyleRules, order, }) } @@ -72,28 +76,28 @@ export class Variants { functional( name: string, applyFn: VariantFn<'functional'>, - { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, + { compounds, order }: { compounds?: Compounds; order?: number } = {}, ) { this.set(name, { kind: 'functional', applyFn, - compoundsWith: false, - compounds: compounds ?? ['selector'], + compoundsWith: Compounds.Never, + compounds: compounds ?? Compounds.StyleRules, order, }) } compoundWith( name: string, - compoundsWith: CompoundKind, + compoundsWith: Compounds, applyFn: VariantFn<'compound'>, - { compounds, order }: { compounds?: CompoundKind; order?: number } = {}, + { compounds, order }: { compounds?: Compounds; order?: number } = {}, ) { this.set(name, { kind: 'compound', applyFn, compoundsWith, - compounds: compounds ?? ['selector'], + compounds: compounds ?? Compounds.StyleRules, order, }) } @@ -123,7 +127,7 @@ export class Variants { typeof child === 'string' ? this.variants.get(child) : child.kind === 'arbitrary' - ? { compounds: ['selector'] as CompoundKind } + ? { compounds: Compounds.StyleRules } : this.variants.get(child.root) // One of the variants don't exist @@ -138,16 +142,14 @@ export class Variants { // variant at runtime to see if the rules are compatible. // The `child` variant cannot compound *ever* - if (childInfo.compounds === false) return false + if (childInfo.compounds === Compounds.Never) return false // The `parent` variant cannot compound *ever* // This shouldn't ever happen because `kind` is `compound` - if (parentInfo.compoundsWith === false) return false + if (parentInfo.compoundsWith === Compounds.Never) return false // Any rule that `child` may generate must be supported by `parent` - for (let compound of childInfo.compounds) { - if (!parentInfo.compoundsWith.includes(compound)) return false - } + if ((parentInfo.compoundsWith & childInfo.compounds) === 0) return false return true } @@ -217,8 +219,8 @@ export class Variants { }: { kind: T applyFn: VariantFn - compoundsWith: CompoundKind - compounds: CompoundKind + compoundsWith: Compounds + compounds: Compounds order?: number }, ) { @@ -257,7 +259,7 @@ export function createVariants(theme: Theme): Variants { function staticVariant( name: string, selectors: string[], - { compounds }: { compounds?: CompoundKind } = {}, + { compounds }: { compounds?: Compounds } = {}, ) { variants.static( name, @@ -268,8 +270,8 @@ export function createVariants(theme: Theme): Variants { ) } - variants.static('force', () => {}, { compounds: false }) - staticVariant('*', [':where(& > *)'], { compounds: false }) + variants.static('force', () => {}, { compounds: Compounds.Never }) + staticVariant('*', [':where(& > *)'], { compounds: Compounds.Never }) function negateConditions(ruleName: string, conditions: string[]) { return conditions.map((condition) => { @@ -343,7 +345,7 @@ export function createVariants(theme: Theme): Variants { return `&:not(${selectors.join(', ')})` } - variants.compoundWith('not', ['selector', 'at-rule'], (ruleNode, variant) => { + variants.compoundWith('not', Compounds.StyleRules | Compounds.AtRules, (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null if (variant.modifier) return null @@ -418,7 +420,7 @@ export function createVariants(theme: Theme): Variants { }) }) - variants.compoundWith('group', ['selector'], (ruleNode, variant) => { + variants.compoundWith('group', Compounds.StyleRules, (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null // Name the group by appending the modifier to `group` class itself if @@ -474,7 +476,7 @@ export function createVariants(theme: Theme): Variants { }) }) - variants.compoundWith('peer', ['selector'], (ruleNode, variant) => { + variants.compoundWith('peer', Compounds.StyleRules, (ruleNode, variant) => { if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null // Name the peer by appending the modifier to `peer` class itself if @@ -530,16 +532,16 @@ export function createVariants(theme: Theme): Variants { }) }) - staticVariant('first-letter', ['&::first-letter'], { compounds: false }) - staticVariant('first-line', ['&::first-line'], { compounds: false }) + staticVariant('first-letter', ['&::first-letter'], { compounds: Compounds.Never }) + staticVariant('first-line', ['&::first-line'], { compounds: Compounds.Never }) // TODO: Remove alpha vars or no? - staticVariant('marker', ['& *::marker', '&::marker'], { compounds: false }) + staticVariant('marker', ['& *::marker', '&::marker'], { compounds: Compounds.Never }) - staticVariant('selection', ['& *::selection', '&::selection'], { compounds: false }) - staticVariant('file', ['&::file-selector-button'], { compounds: false }) - staticVariant('placeholder', ['&::placeholder'], { compounds: false }) - staticVariant('backdrop', ['&::backdrop'], { compounds: false }) + staticVariant('selection', ['& *::selection', '&::selection'], { compounds: Compounds.Never }) + staticVariant('file', ['&::file-selector-button'], { compounds: Compounds.Never }) + staticVariant('placeholder', ['&::placeholder'], { compounds: Compounds.Never }) + staticVariant('backdrop', ['&::backdrop'], { compounds: Compounds.Never }) { function contentProperties() { @@ -562,7 +564,7 @@ export function createVariants(theme: Theme): Variants { ]), ] }, - { compounds: false }, + { compounds: Compounds.Never }, ) variants.static( @@ -572,7 +574,7 @@ export function createVariants(theme: Theme): Variants { rule('&::after', [contentProperties(), decl('content', 'var(--tw-content)'), ...v.nodes]), ] }, - { compounds: false }, + { compounds: Compounds.Never }, ) } @@ -621,7 +623,7 @@ export function createVariants(theme: Theme): Variants { staticVariant('inert', ['&:is([inert], [inert] *)']) - variants.compoundWith('has', ['selector'], (ruleNode, variant) => { + variants.compoundWith('has', Compounds.StyleRules, (ruleNode, variant) => { if (variant.modifier) return null let didApply = false @@ -767,21 +769,21 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@supports ${value}`, ruleNode.nodes)] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) staticVariant('contrast-more', ['@media (prefers-contrast: more)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) staticVariant('contrast-less', ['@media (prefers-contrast: less)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) { @@ -894,7 +896,7 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@media (width < ${value})`, ruleNode.nodes)] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) }, (a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints), @@ -916,7 +918,7 @@ export function createVariants(theme: Theme): Variants { (ruleNode) => { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) } @@ -929,7 +931,7 @@ export function createVariants(theme: Theme): Variants { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) }, (a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints), @@ -987,7 +989,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) }, (a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths), @@ -1015,7 +1017,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) variants.functional( '@min', @@ -1032,7 +1034,7 @@ export function createVariants(theme: Theme): Variants { ), ] }, - { compounds: ['at-rule'] }, + { compounds: Compounds.AtRules }, ) }, (a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths), @@ -1045,24 +1047,24 @@ export function createVariants(theme: Theme): Variants { } } - staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: ['at-rule'] }) + staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: Compounds.AtRules }) staticVariant('landscape', ['@media (orientation: landscape)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) staticVariant('ltr', ['&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)']) staticVariant('rtl', ['&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)']) staticVariant('dark', ['@media (prefers-color-scheme: dark)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) - staticVariant('starting', ['@starting-style'], { compounds: false }) + staticVariant('starting', ['@starting-style'], { compounds: Compounds.Never }) - staticVariant('print', ['@media print'], { compounds: ['at-rule'] }) + staticVariant('print', ['@media print'], { compounds: Compounds.AtRules }) staticVariant('forced-colors', ['@media (forced-colors: active)'], { - compounds: ['at-rule'], + compounds: Compounds.AtRules, }) return variants From 9f1a7d33970171a48d80076d505d30a8c3c59efe Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 22 Oct 2024 11:29:54 -0400 Subject: [PATCH 13/22] Compute compounds when using staticVariant --- packages/tailwindcss/src/variants.ts | 79 +++++++++++++++++----------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 75a99857e471..05183fd597f5 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -261,6 +261,37 @@ export function createVariants(theme: Theme): Variants { selectors: string[], { compounds }: { compounds?: Compounds } = {}, ) { + if (compounds === undefined) { + compounds = (() => { + let compounds = Compounds.Never + + for (let sel of selectors) { + if (sel[0] === '@') { + // Non-conditional at-rules are present so we can't compound + if ( + !sel.startsWith('@media') && + !sel.startsWith('@supports') && + !sel.startsWith('@container') + ) { + return Compounds.Never + } + + compounds |= Compounds.AtRules + continue + } + + // Pseudo-elements are present so we can't compound + if (sel.includes('::')) { + return Compounds.Never + } + + compounds |= Compounds.StyleRules + } + + return compounds + })() + } + variants.static( name, (r) => { @@ -532,16 +563,16 @@ export function createVariants(theme: Theme): Variants { }) }) - staticVariant('first-letter', ['&::first-letter'], { compounds: Compounds.Never }) - staticVariant('first-line', ['&::first-line'], { compounds: Compounds.Never }) + staticVariant('first-letter', ['&::first-letter']) + staticVariant('first-line', ['&::first-line']) // TODO: Remove alpha vars or no? - staticVariant('marker', ['& *::marker', '&::marker'], { compounds: Compounds.Never }) + staticVariant('marker', ['& *::marker', '&::marker']) - staticVariant('selection', ['& *::selection', '&::selection'], { compounds: Compounds.Never }) - staticVariant('file', ['&::file-selector-button'], { compounds: Compounds.Never }) - staticVariant('placeholder', ['&::placeholder'], { compounds: Compounds.Never }) - staticVariant('backdrop', ['&::backdrop'], { compounds: Compounds.Never }) + staticVariant('selection', ['& *::selection', '&::selection']) + staticVariant('file', ['&::file-selector-button']) + staticVariant('placeholder', ['&::placeholder']) + staticVariant('backdrop', ['&::backdrop']) { function contentProperties() { @@ -772,19 +803,11 @@ export function createVariants(theme: Theme): Variants { { compounds: Compounds.AtRules }, ) - staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)'], { - compounds: Compounds.AtRules, - }) - staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)'], { - compounds: Compounds.AtRules, - }) + staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)']) + staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)']) - staticVariant('contrast-more', ['@media (prefers-contrast: more)'], { - compounds: Compounds.AtRules, - }) - staticVariant('contrast-less', ['@media (prefers-contrast: less)'], { - compounds: Compounds.AtRules, - }) + staticVariant('contrast-more', ['@media (prefers-contrast: more)']) + staticVariant('contrast-less', ['@media (prefers-contrast: less)']) { // Helper to compare variants by their resolved values, this is used by the @@ -1047,25 +1070,19 @@ export function createVariants(theme: Theme): Variants { } } - staticVariant('portrait', ['@media (orientation: portrait)'], { compounds: Compounds.AtRules }) - staticVariant('landscape', ['@media (orientation: landscape)'], { - compounds: Compounds.AtRules, - }) + staticVariant('portrait', ['@media (orientation: portrait)']) + staticVariant('landscape', ['@media (orientation: landscape)']) staticVariant('ltr', ['&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)']) staticVariant('rtl', ['&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)']) - staticVariant('dark', ['@media (prefers-color-scheme: dark)'], { - compounds: Compounds.AtRules, - }) + staticVariant('dark', ['@media (prefers-color-scheme: dark)']) - staticVariant('starting', ['@starting-style'], { compounds: Compounds.Never }) + staticVariant('starting', ['@starting-style']) - staticVariant('print', ['@media print'], { compounds: Compounds.AtRules }) + staticVariant('print', ['@media print']) - staticVariant('forced-colors', ['@media (forced-colors: active)'], { - compounds: Compounds.AtRules, - }) + staticVariant('forced-colors', ['@media (forced-colors: active)']) return variants } From 92e8d31796d4b69978863391ec1dbbf1eb845778 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 22 Oct 2024 12:45:46 -0400 Subject: [PATCH 14/22] Add tests --- packages/tailwindcss/src/index.test.ts | 31 ++++++++++++++++++++++++++ packages/tailwindcss/src/index.ts | 2 ++ 2 files changed, 33 insertions(+) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 34419088687b..89fe806b82ee 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2212,6 +2212,37 @@ describe('@variant', () => { }" `) }) + + test('style-rules and at-rules', async () => { + let { build } = await compile(css` + @variant cant-hover (&:not(:hover), &:not(:active), @media not (any-hover: hover), @media not (pointer: fine)); + + @layer utilities { + @tailwind utilities; + } + `) + let compiled = build(['cant-hover:focus:underline']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + :is(.cant-hover\\:focus\\:underline:not(:hover), .cant-hover\\:focus\\:underline:not(:active)):focus { + text-decoration-line: underline; + } + + @media not (any-hover: hover) { + .cant-hover\\:focus\\:underline:focus { + text-decoration-line: underline; + } + } + + @media not (pointer: fine) { + .cant-hover\\:focus\\:underline:focus { + text-decoration-line: underline; + } + } + }" + `) + }) }) describe('body with @slot syntax', () => { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e02799e21144..290a9ab0b292 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -168,6 +168,8 @@ async function parseCss( let styleRuleSelectors: string[] = [] for (let selector of selectors) { + selector = selector.trim() + if (selector.startsWith('@')) { atRuleSelectors.push(selector) } else { From fa3effb85c18098a123cc40529a605b8dcd65ae8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 07:06:27 -0400 Subject: [PATCH 15/22] Refactor --- packages/tailwindcss/src/variants.ts | 60 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 05183fd597f5..0ab7599c134d 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -247,6 +247,35 @@ export class Variants { } } +export function compoundsForSelectors(selectors: string[]) { + let compounds = Compounds.Never + + for (let sel of selectors) { + if (sel[0] === '@') { + // Non-conditional at-rules are present so we can't compound + if ( + !sel.startsWith('@media') && + !sel.startsWith('@supports') && + !sel.startsWith('@container') + ) { + return Compounds.Never + } + + compounds |= Compounds.AtRules + continue + } + + // Pseudo-elements are present so we can't compound + if (sel.includes('::')) { + return Compounds.Never + } + + compounds |= Compounds.StyleRules + } + + return compounds +} + export function createVariants(theme: Theme): Variants { // In the future we may want to support returning a rule here if some complex // variant requires it. For now pure mutation is sufficient and will be the @@ -261,36 +290,7 @@ export function createVariants(theme: Theme): Variants { selectors: string[], { compounds }: { compounds?: Compounds } = {}, ) { - if (compounds === undefined) { - compounds = (() => { - let compounds = Compounds.Never - - for (let sel of selectors) { - if (sel[0] === '@') { - // Non-conditional at-rules are present so we can't compound - if ( - !sel.startsWith('@media') && - !sel.startsWith('@supports') && - !sel.startsWith('@container') - ) { - return Compounds.Never - } - - compounds |= Compounds.AtRules - continue - } - - // Pseudo-elements are present so we can't compound - if (sel.includes('::')) { - return Compounds.Never - } - - compounds |= Compounds.StyleRules - } - - return compounds - })() - } + compounds = compounds ?? compoundsForSelectors(selectors) variants.static( name, From 9e365df9f5c8f731fad499e0fffb16f89e8d2d36 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 07:06:38 -0400 Subject: [PATCH 16/22] Compute compunds for arbitrary variants --- packages/tailwindcss/src/variants.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 0ab7599c134d..980bda6d6c84 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -127,7 +127,9 @@ export class Variants { typeof child === 'string' ? this.variants.get(child) : child.kind === 'arbitrary' - ? { compounds: Compounds.StyleRules } + ? // This isn't strictly necessary but it'll allow us to bail quickly + // when parsing candidates + { compounds: compoundsForSelectors([child.selector]) } : this.variants.get(child.root) // One of the variants don't exist From 5b7e10442f99fbfb72bcb863c2194bc20e8b97ae Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 07:06:48 -0400 Subject: [PATCH 17/22] Compute compounds for `@variant` --- packages/tailwindcss/src/index.ts | 33 +++++++++++++++++----------- packages/tailwindcss/src/variants.ts | 22 +++++++++++++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 290a9ab0b292..72aab861d42f 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -22,6 +22,7 @@ import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' import { Theme, ThemeOptions } from './theme' import { segment } from './utils/segment' +import { compoundsForSelectors } from './variants' export type Config = UserConfig const IS_VALID_PREFIX = /^[a-z]+$/ @@ -178,19 +179,25 @@ async function parseCss( } customVariants.push((designSystem) => { - designSystem.variants.static(name, (r) => { - let nodes: AstNode[] = [] - - if (styleRuleSelectors.length > 0) { - nodes.push(rule(styleRuleSelectors.join(', '), r.nodes)) - } - - for (let selector of atRuleSelectors) { - nodes.push(rule(selector, r.nodes)) - } - - r.nodes = nodes - }) + designSystem.variants.static( + name, + (r) => { + let nodes: AstNode[] = [] + + if (styleRuleSelectors.length > 0) { + nodes.push(rule(styleRuleSelectors.join(', '), r.nodes)) + } + + for (let selector of atRuleSelectors) { + nodes.push(rule(selector, r.nodes)) + } + + r.nodes = nodes + }, + { + compounds: compoundsForSelectors([...styleRuleSelectors, ...atRuleSelectors]), + }, + ) }) return diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 980bda6d6c84..8d24fd9b0171 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -66,11 +66,25 @@ export class Variants { } fromAst(name: string, ast: AstNode[]) { - this.static(name, (r) => { - let body = structuredClone(ast) - substituteAtSlot(body, r.nodes) - r.nodes = body + let selectors: string[] = [] + + walk(ast, (node) => { + if (node.kind !== 'rule') return + if (node.selector === '@slot') return + selectors.push(node.selector) }) + + this.static( + name, + (r) => { + let body = structuredClone(ast) + substituteAtSlot(body, r.nodes) + r.nodes = body + }, + { + compounds: compoundsForSelectors(selectors), + }, + ) } functional( From 4cc3f16e5ff81ef306e1647a9998c328b0bdcc40 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 07:12:03 -0400 Subject: [PATCH 18/22] Compute compounds for `addVariant` --- packages/tailwindcss/src/compat/plugin-api.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 1e7cd9e197dd..54fd8e00a27a 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -8,7 +8,7 @@ import { withAlpha, withNegative } from '../utilities' import { inferDataType } from '../utils/infer-data-type' import { segment } from '../utils/segment' import { toKeyPath } from '../utils/to-key-path' -import { substituteAtSlot } from '../variants' +import { compoundsForSelectors, substituteAtSlot } from '../variants' import type { ResolvedConfig, UserConfig } from './config/types' import { createThemeFn } from './plugin-functions' @@ -92,9 +92,15 @@ export function buildPluginApi( addVariant(name, variant) { // Single selector or multiple parallel selectors if (typeof variant === 'string' || Array.isArray(variant)) { - designSystem.variants.static(name, (r) => { - r.nodes = parseVariantValue(variant, r.nodes) - }) + designSystem.variants.static( + name, + (r) => { + r.nodes = parseVariantValue(variant, r.nodes) + }, + { + compounds: compoundsForSelectors(typeof variant === 'string' ? [variant] : variant), + }, + ) } // CSS-in-JS object From fb0eabfb66598d95fb5110edcddf59948d2bf2a1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 07:22:38 -0400 Subject: [PATCH 19/22] Add tests --- packages/tailwindcss/src/intellisense.test.ts | 55 +++++++++++++++++++ packages/tailwindcss/src/variants.test.ts | 27 +++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index ee23be97d830..fd5623613920 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -377,3 +377,58 @@ test('Functional utilities from plugins are listed in hovers and completions', a expect(classNames).not.toContain('custom-3-unknown') }) + +test('Custom at-rule variants do not show up as a value under `group`', async () => { + let input = css` + @import 'tailwindcss/utilities'; + @variant variant-1 (@media foo); + @variant variant-2 { + @media bar { + @slot; + } + } + @plugin "./plugin.js"; + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + base, + content: '@tailwind utilities;', + }), + loadModule: async () => ({ + base: '', + module: plugin(({ addVariant }) => { + addVariant('variant-3', '@media baz') + addVariant('variant-4', ['@media qux', '@media cat']) + }), + }), + }) + + let variants = design.getVariants() + let v1 = variants.find((v) => v.name === 'variant-1')! + let v2 = variants.find((v) => v.name === 'variant-2')! + let v3 = variants.find((v) => v.name === 'variant-3')! + let v4 = variants.find((v) => v.name === 'variant-4')! + let group = variants.find((v) => v.name === 'group')! + let not = variants.find((v) => v.name === 'not')! + + // All the variants should exist + expect(v1).not.toBeUndefined() + expect(v2).not.toBeUndefined() + expect(v3).not.toBeUndefined() + expect(v4).not.toBeUndefined() + expect(group).not.toBeUndefined() + expect(not).not.toBeUndefined() + + // Group should not have variant-1, variant-2, or variant-3 + expect(group.values).not.toContain('variant-1') + expect(group.values).not.toContain('variant-2') + expect(group.values).not.toContain('variant-3') + expect(group.values).not.toContain('variant-4') + + // Not should have variant-1, variant-2, or variant-3 + expect(not.values).toContain('variant-1') + expect(not.values).toContain('variant-2') + expect(not.values).toContain('variant-3') + expect(not.values).toContain('variant-4') +}) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 353eb0939f45..a6c8a4d7f7c5 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1,5 +1,6 @@ import { expect, test } from 'vitest' import { compileCss, run } from './test-utils/run' +import { Compounds, compoundsForSelectors } from './variants' const css = String.raw @@ -3259,3 +3260,29 @@ test('variant order', async () => { }" `) }) + +test.each([ + // These are style rules + [['.foo'], Compounds.StyleRules], + [['&:is(:hover)'], Compounds.StyleRules], + + // These are conditional at rules + [['@media foo'], Compounds.AtRules], + [['@supports foo'], Compounds.AtRules], + [['@container foo'], Compounds.AtRules], + + // These are both + [['.foo', '@media foo'], Compounds.StyleRules | Compounds.AtRules], + + // These are never compoundable because: + // - Pseudo-elements are not compoundable + // - Non-conditional at-rules are not compoundable + [['.foo::before'], Compounds.Never], + [['@starting-style'], Compounds.Never], + + // The presence of a single non-compoundable selector makes the whole list non-compoundable + [['.foo', '@media foo', '.foo::before'], Compounds.Never], + [['.foo', '@media foo', '@starting-style'], Compounds.Never], +])('compoundsForSelectors: %s', (selectors, expected) => { + expect(compoundsForSelectors(selectors)).toBe(expected) +}) From a1ab176bc957f135966288d7884c4cdb9ca9f1ba Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 09:49:01 -0400 Subject: [PATCH 20/22] Update CHANGELOG.md Co-authored-by: Robin Malfait --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a586a761360..4a1d94d0eadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Aded `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) +- Added `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - Improved support for custom variants with `group-*`, `peer-*`, `has-*`, and `not-*` ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) - _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742)) From 6ac6829beef3fb319ad843e74ea586ac92d8676f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 23 Oct 2024 09:51:45 -0400 Subject: [PATCH 21/22] Apply suggestions from code review Co-authored-by: Robin Malfait --- packages/tailwindcss/src/candidate.test.ts | 2 +- packages/tailwindcss/src/index.ts | 2 +- packages/tailwindcss/src/intellisense.ts | 6 +++--- packages/tailwindcss/src/variants.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index baf0c500680e..05f87246ed67 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1204,7 +1204,7 @@ it('should not parse compound group with a non-compoundable variant', () => { utilities.static('flex', () => []) let variants = new Variants() - variants.compoundWith('group', ['selector'], () => {}) + variants.compoundWith('group', Compounds.StyleRules, () => {}) expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 72aab861d42f..743e713f23f5 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -171,7 +171,7 @@ async function parseCss( for (let selector of selectors) { selector = selector.trim() - if (selector.startsWith('@')) { + if (selector[0] === '@') { atRuleSelectors.push(selector) } else { styleRuleSelectors.push(selector) diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 41fddea5814c..815aa1cc04cb 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -80,7 +80,7 @@ export function getVariants(design: DesignSystem) { let selectors: string[] = [] // Produce v3-style selector strings in the face of nested rules - // This is more visible for things like group-*, not-*, etc… + // this is more visible for things like group-*, not-*, etc… walkDepth(node.nodes, (node, { path }) => { if (node.kind !== 'rule') return if (node.nodes.length > 0) return @@ -90,8 +90,8 @@ export function getVariants(design: DesignSystem) { // This won't actually happen, but it's here to make TypeScript happy if (a.kind !== 'rule' || b.kind !== 'rule') return 0 - let aIsAtRule = a.selector.startsWith('@') - let bIsAtRule = b.selector.startsWith('@') + let aIsAtRule = a.selector[0] === '@' + let bIsAtRule = b.selector[0] === '@' if (aIsAtRule && !bIsAtRule) return -1 if (!aIsAtRule && bIsAtRule) return 1 diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 8d24fd9b0171..39c089eb4dd6 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -335,7 +335,7 @@ export function createVariants(theme: Theme): Variants { if (ruleName === 'container') { // @container {query} - if (parts[0].startsWith('(')) { + if (parts[0][0] === '(') { return `not ${condition}` } @@ -377,7 +377,7 @@ export function createVariants(theme: Theme): Variants { if (selector.includes('::')) return null let selectors = segment(selector, ',').map((sel) => { - // Remove unncessary wrapping &:is(…) to reduce the selector size + // Remove unnecessary wrapping &:is(…) to reduce the selector size if (sel.startsWith('&:is(') && sel.endsWith(')')) { sel = sel.slice(5, -1) } From 48139ad4dbc151f104fa46795f6f877faed302f7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 24 Oct 2024 13:21:16 -0400 Subject: [PATCH 22/22] Update changelog --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d3438c7cc4..ce5c2a5dfdd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) +- Improved support for custom variants with `group-*`, `peer-*`, `has-*`, and `not-*` ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) + ### Changed - Don't convert underscores in the first argument to `var()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) @@ -17,14 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't migrate important modifiers inside conditional statements in Vue and Alpine (e.g. `
`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774)) - Ensure third-party plugins with `exports` in their `package.json` are resolved correctly ([#14775](https://github.com/tailwindlabs/tailwindcss/pull/14775)) - Ensure underscores in the `url()` function are never unescaped ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776)) +- Fixed display of complex variants in Intellisense ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Ensure `@import` statements for relative CSS files are actually migrated to use relative path syntax ([#14769](https://github.com/tailwindlabs/tailwindcss/pull/14769)) ## [4.0.0-alpha.29] - 2024-10-23 ### Added -- Added `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) -- Improved support for custom variants with `group-*`, `peer-*`, `has-*`, and `not-*` ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) - _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742)) - _Upgrade (experimental)_: Migrate `@import` statements for relative CSS files to use relative path syntax (e.g. `./file.css`) ([#14755](https://github.com/tailwindlabs/tailwindcss/pull/14755)) @@ -44,7 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure JS theme keys containing special characters correctly produce utility classes (e.g. `'1/2': 50%` to `w-1/2`) ([#14739](https://github.com/tailwindlabs/tailwindcss/pull/14739)) - Always emit keyframes registered in `addUtilities` ([#14747](https://github.com/tailwindlabs/tailwindcss/pull/14747)) - Ensure loading stylesheets via the `?raw` and `?url` static asset query works when using the Vite plugin ([#14716](https://github.com/tailwindlabs/tailwindcss/pull/14716)) -- Fixed display of complex variants in Intellisense ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743)) - _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721)) - _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720)) - _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))