mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Improve support for custom variants in group-*, peer-*, has-*, and not-* variants (#14743)
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
5a1c2e7480
commit
148d8707b9
@ -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()` and `theme()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776), [#14781](https://github.com/tailwindlabs/tailwindcss/pull/14781))
|
||||
@ -17,6 +22,7 @@ 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. `<div v-if="!border" />`) ([#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))
|
||||
- _Upgrade (experimental)_: Only generate Preflight compatibility styles when Preflight is used ([#14773](https://github.com/tailwindlabs/tailwindcss/pull/14773))
|
||||
- _Upgrade (experimental)_: Don't escape underscores when printing theme values migrated to CSS variables in arbitrary values (e.g. `m-[var(--spacing-1_5)]` instead of `m-[var(--spacing-1\_5)]`) ([#14778](https://github.com/tailwindlabs/tailwindcss/pull/14778))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -87,25 +87,30 @@ export function walk(
|
||||
parent: AstNode | null
|
||||
replaceWith(newNode: AstNode | AstNode[]): void
|
||||
context: Record<string, string>
|
||||
path: AstNode[]
|
||||
},
|
||||
) => void | WalkAction,
|
||||
parent: AstNode | null = null,
|
||||
parentPath: AstNode[] = [],
|
||||
context: Record<string, string> = {},
|
||||
) {
|
||||
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,11 +127,52 @@ export function walk(
|
||||
if (status === WalkAction.Skip) continue
|
||||
|
||||
if (node.kind === 'rule') {
|
||||
walk(node.nodes, visit, node, context)
|
||||
walk(node.nodes, visit, path, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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<string, string>
|
||||
replaceWith(newNode: AstNode[]): void
|
||||
},
|
||||
) => void,
|
||||
parentPath: AstNode[] = [],
|
||||
context: Record<string, string> = {},
|
||||
) {
|
||||
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<string>()
|
||||
|
||||
@ -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,
|
||||
@ -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",
|
||||
@ -214,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', Compounds.StyleRules, () => {})
|
||||
|
||||
expect(run('group-[&_p]/parent-name:flex', { utilities, variants })).toMatchInlineSnapshot(`
|
||||
[
|
||||
@ -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",
|
||||
@ -251,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', Compounds.StyleRules, () => {})
|
||||
variants.functional('aria', () => {})
|
||||
|
||||
expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants }))
|
||||
@ -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",
|
||||
@ -295,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', Compounds.StyleRules, () => {})
|
||||
|
||||
expect(run('group-group-group-hover/parent-name:flex', { utilities, variants }))
|
||||
.toMatchInlineSnapshot(`
|
||||
@ -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",
|
||||
@ -1204,7 +1185,6 @@ it('should parse arbitrary properties with a variant', () => {
|
||||
"value": "red",
|
||||
"variants": [
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "static",
|
||||
"root": "hover",
|
||||
},
|
||||
@ -1230,12 +1210,10 @@ it('should parse arbitrary properties with stacked variants', () => {
|
||||
"value": "red",
|
||||
"variants": [
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "static",
|
||||
"root": "hover",
|
||||
},
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "static",
|
||||
"root": "focus",
|
||||
},
|
||||
@ -1257,13 +1235,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)",
|
||||
@ -1279,7 +1255,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', Compounds.StyleRules, () => {})
|
||||
|
||||
expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`)
|
||||
})
|
||||
@ -1301,7 +1277,6 @@ it('should parse a variant containing an arbitrary string with unbalanced parens
|
||||
"root": "flex",
|
||||
"variants": [
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "functional",
|
||||
"modifier": null,
|
||||
"root": "string",
|
||||
@ -1349,7 +1324,6 @@ it('should parse candidates with a prefix', () => {
|
||||
"root": "flex",
|
||||
"variants": [
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "static",
|
||||
"root": "hover",
|
||||
},
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -2309,7 +2309,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;
|
||||
}
|
||||
|
||||
@ -2342,6 +2342,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', () => {
|
||||
@ -2752,6 +2783,12 @@ describe('@variant', () => {
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
@media not foo {
|
||||
.not-foo\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media foo {
|
||||
.foo\\:flex {
|
||||
display: flex;
|
||||
|
||||
@ -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]+$/
|
||||
@ -164,10 +165,39 @@ async function parseCss(
|
||||
|
||||
let selectors = segment(selector.slice(1, -1), ',')
|
||||
|
||||
let atRuleSelectors: string[] = []
|
||||
let styleRuleSelectors: string[] = []
|
||||
|
||||
for (let selector of selectors) {
|
||||
selector = selector.trim()
|
||||
|
||||
if (selector[0] === '@') {
|
||||
atRuleSelectors.push(selector)
|
||||
} else {
|
||||
styleRuleSelectors.push(selector)
|
||||
}
|
||||
}
|
||||
|
||||
customVariants.push((designSystem) => {
|
||||
designSystem.variants.static(name, (r) => {
|
||||
r.nodes = selectors.map((selector) => rule(selector, r.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
|
||||
|
||||
56
packages/tailwindcss/src/intellisense.bench.ts
Normal file
56
packages/tailwindcss/src/intellisense.bench.ts
Normal file
@ -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]'])
|
||||
})
|
||||
@ -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()
|
||||
@ -350,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')
|
||||
})
|
||||
|
||||
@ -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[0] === '@'
|
||||
let bIsAtRule = b.selector[0] === '@'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1687,16 +1688,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 +1722,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 +2096,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 +2122,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('')
|
||||
@ -2882,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)
|
||||
})
|
||||
|
||||
@ -12,6 +12,12 @@ type VariantFn<T extends Variant['kind']> = (
|
||||
|
||||
type CompareFn = (a: Variant, z: Variant) => number
|
||||
|
||||
export const enum Compounds {
|
||||
Never = 0,
|
||||
AtRules = 1 << 0,
|
||||
StyleRules = 1 << 1,
|
||||
}
|
||||
|
||||
export class Variants {
|
||||
public compareFns = new Map<number, CompareFn>()
|
||||
public variants = new Map<
|
||||
@ -20,7 +26,13 @@ export class Variants {
|
||||
kind: Variant['kind']
|
||||
order: number
|
||||
applyFn: VariantFn<any>
|
||||
compounds: boolean
|
||||
|
||||
// The kind of rules that are allowed in this compound variant
|
||||
compoundsWith: Compounds
|
||||
|
||||
// The kind of rules that are generated by this variant
|
||||
// Determines whether or not a compound variant can use this variant
|
||||
compounds: Compounds
|
||||
}
|
||||
>()
|
||||
|
||||
@ -42,33 +54,66 @@ export class Variants {
|
||||
static(
|
||||
name: string,
|
||||
applyFn: VariantFn<'static'>,
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
{ compounds, order }: { compounds?: Compounds; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'static', applyFn, compounds: compounds ?? true, order })
|
||||
this.set(name, {
|
||||
kind: 'static',
|
||||
applyFn,
|
||||
compoundsWith: Compounds.Never,
|
||||
compounds: compounds ?? Compounds.StyleRules,
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
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(
|
||||
name: string,
|
||||
applyFn: VariantFn<'functional'>,
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
{ compounds, order }: { compounds?: Compounds; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'functional', applyFn, compounds: compounds ?? true, order })
|
||||
this.set(name, {
|
||||
kind: 'functional',
|
||||
applyFn,
|
||||
compoundsWith: Compounds.Never,
|
||||
compounds: compounds ?? Compounds.StyleRules,
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
compound(
|
||||
compoundWith(
|
||||
name: string,
|
||||
compoundsWith: Compounds,
|
||||
applyFn: VariantFn<'compound'>,
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
{ compounds, order }: { compounds?: Compounds; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'compound', applyFn, compounds: compounds ?? true, order })
|
||||
this.set(name, {
|
||||
kind: 'compound',
|
||||
applyFn,
|
||||
compoundsWith,
|
||||
compounds: compounds ?? Compounds.StyleRules,
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
group(fn: () => void, compareFn?: CompareFn) {
|
||||
@ -90,8 +135,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'
|
||||
? // 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
|
||||
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 === Compounds.Never) return false
|
||||
|
||||
// The `parent` variant cannot compound *ever*
|
||||
// This shouldn't ever happen because `kind` is `compound`
|
||||
if (parentInfo.compoundsWith === Compounds.Never) return false
|
||||
|
||||
// Any rule that `child` may generate must be supported by `parent`
|
||||
if ((parentInfo.compoundsWith & childInfo.compounds) === 0) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
suggest(name: string, suggestions: () => string[]) {
|
||||
@ -154,8 +230,15 @@ export class Variants {
|
||||
kind,
|
||||
applyFn,
|
||||
compounds,
|
||||
compoundsWith,
|
||||
order,
|
||||
}: { kind: T; applyFn: VariantFn<T>; compounds: boolean; order?: number },
|
||||
}: {
|
||||
kind: T
|
||||
applyFn: VariantFn<T>
|
||||
compoundsWith: Compounds
|
||||
compounds: Compounds
|
||||
order?: number
|
||||
},
|
||||
) {
|
||||
let existing = this.variants.get(name)
|
||||
if (existing) {
|
||||
@ -169,6 +252,7 @@ export class Variants {
|
||||
kind,
|
||||
applyFn,
|
||||
order,
|
||||
compoundsWith,
|
||||
compounds,
|
||||
})
|
||||
}
|
||||
@ -179,6 +263,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
|
||||
@ -191,8 +304,10 @@ export function createVariants(theme: Theme): Variants {
|
||||
function staticVariant(
|
||||
name: string,
|
||||
selectors: string[],
|
||||
{ compounds }: { compounds?: boolean } = {},
|
||||
{ compounds }: { compounds?: Compounds } = {},
|
||||
) {
|
||||
compounds = compounds ?? compoundsForSelectors(selectors)
|
||||
|
||||
variants.static(
|
||||
name,
|
||||
(r) => {
|
||||
@ -202,92 +317,196 @@ 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 })
|
||||
|
||||
variants.compound('not', (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][0] === '(') {
|
||||
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 unnecessary 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', Compounds.StyleRules | Compounds.AtRules, (ruleNode, variant) => {
|
||||
if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null
|
||||
|
||||
if (variant.modifier) return null
|
||||
|
||||
let didApply = false
|
||||
|
||||
walk([ruleNode], (node) => {
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
if (node.kind !== 'rule') return WalkAction.Continue
|
||||
if (node.nodes.length > 0) 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 style rules
|
||||
let atRules: Rule[] = []
|
||||
let styleRules: Rule[] = []
|
||||
|
||||
// Throw out any candidates with variants using nested selectors
|
||||
if (didApply) {
|
||||
walk([node], (childNode) => {
|
||||
if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue
|
||||
|
||||
didApply = false
|
||||
return WalkAction.Stop
|
||||
})
|
||||
|
||||
return didApply ? WalkAction.Skip : WalkAction.Stop
|
||||
for (let parent of path) {
|
||||
if (parent.kind !== 'rule') continue
|
||||
if (parent.selector[0] === '@') {
|
||||
atRules.push(parent)
|
||||
} else {
|
||||
styleRules.push(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Replace `&` in target variant with `*`, so variants like `&:hover`
|
||||
// become `&:not(*:hover)`. The `*` will often be optimized away.
|
||||
node.selector = `&:not(${node.selector.replaceAll('&', '*')})`
|
||||
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, []))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (!didApply) return null
|
||||
})
|
||||
|
||||
variants.compound('group', (ruleNode, variant) => {
|
||||
variants.suggest('not', () => {
|
||||
return Array.from(variants.keys()).filter((name) => {
|
||||
return variants.compoundsWith('not', name)
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
// 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,60 +514,55 @@ 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', () => {
|
||||
return Array.from(variants.keys()).filter((name) => {
|
||||
return variants.get(name)?.compounds ?? false
|
||||
return variants.compoundsWith('group', name)
|
||||
})
|
||||
})
|
||||
|
||||
variants.compound('peer', (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
|
||||
// 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,27 +570,25 @@ 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', () => {
|
||||
return Array.from(variants.keys()).filter((name) => {
|
||||
return variants.get(name)?.compounds ?? false
|
||||
return variants.compoundsWith('peer', name)
|
||||
})
|
||||
})
|
||||
|
||||
staticVariant('first-letter', ['&::first-letter'], { compounds: false })
|
||||
staticVariant('first-line', ['&::first-line'], { compounds: false })
|
||||
staticVariant('first-letter', ['&::first-letter'])
|
||||
staticVariant('first-line', ['&::first-line'])
|
||||
|
||||
// TODO: Remove alpha vars or no?
|
||||
staticVariant('marker', ['& *::marker', '&::marker'], { compounds: false })
|
||||
staticVariant('marker', ['& *::marker', '&::marker'])
|
||||
|
||||
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'])
|
||||
staticVariant('file', ['&::file-selector-button'])
|
||||
staticVariant('placeholder', ['&::placeholder'])
|
||||
staticVariant('backdrop', ['&::backdrop'])
|
||||
|
||||
{
|
||||
function contentProperties() {
|
||||
@ -399,7 +611,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
]),
|
||||
]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.Never },
|
||||
)
|
||||
|
||||
variants.static(
|
||||
@ -409,7 +621,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
rule('&::after', [contentProperties(), decl('content', 'var(--tw-content)'), ...v.nodes]),
|
||||
]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.Never },
|
||||
)
|
||||
}
|
||||
|
||||
@ -458,27 +670,24 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
staticVariant('inert', ['&:is([inert], [inert] *)'])
|
||||
|
||||
variants.compound('has', (ruleNode, variant) => {
|
||||
variants.compoundWith('has', Compounds.StyleRules, (ruleNode, variant) => {
|
||||
if (variant.modifier) return null
|
||||
|
||||
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,14 +700,12 @@ 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', () => {
|
||||
return Array.from(variants.keys()).filter((name) => {
|
||||
return variants.get(name)?.compounds ?? false
|
||||
return variants.compoundsWith('has', name)
|
||||
})
|
||||
})
|
||||
|
||||
@ -609,16 +816,14 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
ruleNode.nodes = [rule(`@supports ${value}`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
|
||||
staticVariant('motion-safe', ['@media (prefers-reduced-motion: no-preference)'], {
|
||||
compounds: false,
|
||||
})
|
||||
staticVariant('motion-reduce', ['@media (prefers-reduced-motion: reduce)'], { compounds: false })
|
||||
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: false })
|
||||
staticVariant('contrast-less', ['@media (prefers-contrast: less)'], { compounds: false })
|
||||
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
|
||||
@ -730,7 +935,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
ruleNode.nodes = [rule(`@media (width < ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints),
|
||||
@ -752,7 +957,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
(ruleNode) => {
|
||||
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
}
|
||||
|
||||
@ -765,7 +970,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
|
||||
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints),
|
||||
@ -823,7 +1028,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
),
|
||||
]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths),
|
||||
@ -851,7 +1056,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
),
|
||||
]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
variants.functional(
|
||||
'@min',
|
||||
@ -868,7 +1073,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
),
|
||||
]
|
||||
},
|
||||
{ compounds: false },
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths),
|
||||
@ -881,19 +1086,19 @@ 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)'])
|
||||
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: false })
|
||||
staticVariant('dark', ['@media (prefers-color-scheme: dark)'])
|
||||
|
||||
staticVariant('starting', ['@starting-style'], { compounds: false })
|
||||
staticVariant('starting', ['@starting-style'])
|
||||
|
||||
staticVariant('print', ['@media print'], { compounds: false })
|
||||
staticVariant('print', ['@media print'])
|
||||
|
||||
staticVariant('forced-colors', ['@media (forced-colors: active)'], { compounds: false })
|
||||
staticVariant('forced-colors', ['@media (forced-colors: active)'])
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user